512x512 像素,每像素 10000 个采样,Intel C++ OpenMP 版本渲染时间为 18 分 36 秒。估计 Ruby 版本約需 351 天。

本人陆续移植了 C++代码至 Java、JavaScript、Lua、Python 和 Ruby,赵姐夫亦尝试了 F#。本文提供测试源代码、测试结果、简单分析、以及个人体会。

声明

首先,为免误会,再次重申,本测试有其局限,只能测试某一应用、某一实现的结果,并不能反映编程语言及其运行时的综合性能,亦无意尝试这样做。而实验环境也只限于某机器、某操作系统上,并不全面。而且,本测试只提供运行时间的结果,不考虑、不比较语言/平台间的技术性和非技术性优缺点,也没有测试运行期内存。世界上的软件应用林林总总,性能需求也完全不同,本测试只供参考。

由于本人第一次使用 Python 和 Ruby,若代码有不当之处,敬请告之。当然也非常乐见其他意见。

测试内容

本文测试程序为一个全局光照渲染器,是一个 CPU 运算密集的控制台应用程序 (console application),功能详见前文。在前文刊出后,本人进行了一点 profiling、优化,并把代码重新格式化。本渲染器除了有大量数学运算,亦会产生大量临时对象,并进行极多的方法调用 (非虚函数)。本测试有别于人工合成的测试 (synthetic tests,例如个别测试运算、字串操作、输入输出等),是一个有实际用途的程序。

移植时尽量维持原代码的逻辑,主要采用面向对象范式。优化方面,不进行人手内联函数 (inline function),但优化了一些不必要的重复运算。

测试配置

硬件: Intel Core i7 920@2.67Ghz(4 core, HyperThread), 12GB RAM

操作系统: Microsoft Windows 7 64-bit

测试名称

编译器/解译器

编译/运行选项

VC++

Visual C++ 2008 (32-bit)

/Ox /Ob2 /Oi /Ot /GL /FD /MD /GS- /Gy /arch:SSE /fp:fast

VC++_OpenMP

Visual C++ 2008 (32-bit)

/Ox /Ob2 /Oi /Ot /GL /FD /MD /GS- /Gy /arch:SSE /fp:fast /openmp

IC++

Intel C++ Compiler (32-bit)

/Ox /Og /Ob2 /Oi /Ot /Qipo /GA /MD /GS- /Gy /arch:SSE2 /fp:fast /Zi /QxHost

IC++_OpenMP

Intel C++ Compiler (32-bit)

/Ox /Og /Ob2 /Oi /Ot /Qipo /GA /MD /GS- /Gy /arch:SSE2 /fp:fast /Zi /QxHost /Qopenmp

GCC

GCC 4.3.4 in Cygwin (32-bit)

-O3 -march=native -ffast-math

GCC_OpenMP

GCC 4.3.4 in Cygwin (32-bit)

-O3 -march=native -ffast-math -fopenmp

C++/CLI

Visual C++ 2008 (32-bit), .Net Framework 3.5

/Ox /Ob2 /Oi /Ot /GL /FD /MD /GS- /fp:fast /Zi /clr /TP

C++/CLI_OpenMP

Visual C++ 2008 (32-bit), .Net Framework 3.5

/Ox /Ob2 /Oi /Ot /GL /FD /MD /GS- /fp:fast /Zi /clr /TP /openmp

C#

Visual C# 2008 (32-bit), .Net Framework 3.5

*C#_outref

Visual C# 2008 (32-bit), .Net Framework 3.5

F#

F# 2.0 (32-bit), .Net Framework 3.5

Java

Java SE 1.6.0_17

-server

JsChrome

Chrome 5.0.375.86

JsFirefox

Firefox 3.6

LuaJIT

LuaJIT 2.0.0-beta4 (32-bit)

Lua

LuaJIT (32-bit)

-joff

Python

Python 3.1.2 (32-bit)

*IronPython

IronPython 2.6 for .Net 4

*Jython

Jython 2.5.1

Ruby

Ruby 1.9.1p378

* 见本文最后的"7. 更新"一节

渲染的解像度为 256x256,每象素作 100 次采样。

结果及分析

下表中预设的相对时间以最快的单线程测试 (IC++) 作基准,用鼠标按列可改变基准。由于 Ruby 运行时间太长,只每象素作 4 次采样,把时间乘上 25。另外,因为各测试的渲染时间相差很远,所以用了两个棒形图去显示数据,分别显示时间少于 4000 秒和少于 60 秒的测试 (Ruby 是 4000 秒以外,不予显示)。

Test

Time(sec)

Relative time

IC++_OpenMP

2.861

0.19x

VC++_OpenMP

3.140

0.21x

GCC_OpenMP

3.359

0.23x

C++/CLI_OpenMP

5.147

0.35x

IC++

14.761

1.00x

VC++

17.632

1.19x

GCC

19.500

1.32x

C++/CLI

27.634

1.87x

Java

30.527

2.07x

C#_outref

44.220

3.00x

F#

47.172

3.20x

C#

48.194

3.26x

JsChrome

237.880

16.12x

LuaJIT

829.777

56.21x

Lua

1,227.656

83.17x

IronPython

2,921.573

197.93x

JsFirefox

3,588.778

243.13x

Python

3,920.556

265.60x

Jython

6,211.550

420.81x

Ruby

77,859.653

5,274.69x

C++/.Net/Java 组别

静态语言和动态语言在此测试下的性能不在同一数量级。先比较静态语言。

C++和.Net 的测试结果和上一篇博文相若,而 C#和 F#无显著区别。但是,C++/CLI 虽然同样产生 IL,于括管的.Net 平台上执行,其渲染时间却只是 C#/F#的 55% 左右。为什么呢?使用 ildasm 去反汇编 C++/CLI 和 C#的可执行文件后,可以发现,程序的热点函数 Sphere.Intersect() 在两个版本中,C++/CLI 版本的代码大小 (code size) 为 201 字节, C#则为 125 字节! C++/CLI 版本在编译时,已把函数内所有 Vec 类的方法调用全部内联,而 C#版本则使用 callvirt 调用 Vec 的方法。估计 JIT 没有把这函数进行内联,做成这个性能差异。另外,C++/CLI 版本使用了值类型,并使用指针 (代码中为引用) 作参数传送。若把 C#的版本的 Vec 方法改写为:

1

2

3

4

5

6

7

8

9

//class Vec

//{

//public static Vec operator +(Vec a, Vec b)

//}

struct Vec

{

void Add(ref Vec a,ref Vec b,out Vec c);

}

那么,struct 不用 GC,同时 ref/out 不用复制,其性能会比较高。但是代码会变得很难看:

1

2

3

4

5

6

7

// 原来用运算符重载 (operator overloading):

a = b * c + d;

// 改用 ref/out

Vec e;

Vec.Mul(ref b,ref, c,out e);

Vec.Add(ref e,ref d,out a);

为了维持让语言"正常"的使用方法,本实验不采用这种 API 风格 (更新: 加入了 C#_outref 测试,詳見文末)。

然而,托管代码 (C++/CLI) 的渲染时间,仅为原生非括管代码 (IC++) 的 1.91 倍,个人觉得.Net 的 JIT 已经非常不错。

另一方面,Java 的性能表现非常突出,只比 C++/CLI 稍慢一点,Java 版本的渲染时间为 C#/F#的 65% 左右。以前一直认为,C#不少设计会使其性能高于 Java,例如 C#的方法预设为非虚,Java 则预设为虚;又例如 C#支持 struct 作值类型 (value type),Java 则只有 class 引用类型 (reference type),后者必须使用 GC。但是,这个测试显示,Java VM 应该在 JIT 中做了大量优化,估计也应用了内联,才能使其性能逼近 C++/CLI。

纯 C++方面,Intel C++编译器最快,Visual C++慢一点点 (1.19x),GCC 再慢一点点 (1.32x)。这结果符合本人预期。 Intel C++的 OpenMP 版本和单线程比较,达 5.16 加速比 (speedup),对于 4 核 Hyper Threading 来说算是不错的结果。读者若有兴趣,也可以自行测试 C# 4.0 的并行新特性。

动态语言组别

首先,要说一句,Google 太强了,难以想像 JsChome 的渲染时间仅是 IC++的 16.12 倍,C#的 4.94 倍。我有信心用 JavaScript 继续写图形、物理方面的博文了。

以下比较各动态语言的相对时间,以 JsChrome 为基准。 Chrome 的 V8 JavaScript 引擎 (1.00x) 大幅抛离 Firefox 的 SpiderMonkey 引擎 (15.09x)。而 LuaJIT(3.49x) 和 Lua(5.16x) 则排第二和第三名。 Lua 的 JIT 版本是没有 JIT 的 68%,并没有想像中的快,但是也比 Python(16.48x) 快得多。曾听说过 Ruby 有效能问题,没想到问题竟然如此严重 (327.31x),其渲染时间差不多是 Python 的 20 倍。

我认为,本实验中,不同语言的性能差异,并非在于数值运算,而是对象生成及函数调用。我使用 Python 内建的 profiling 功能:

1

python -m profile smallpt.py

从结果发现,Vec 类共产生约 15 亿个实例,Vec 的方法调用约 17.5 亿次,intersect() 共调用 5.7 亿次,产生随机数 5.7 亿个,radiance() 调用 (即追踪的路径线段)6.5 百万次。这些庞大数字,放大了对象生成和函数调用的常数开销 (overhead)。

结语

也许本博文的意义不大 (yet-another-unfair-biased-performance-comparison-among-programming-languages),但对本人而言,此次实验加深了对各种语言性能的了解,或应该是消除了一些误解。简单总括运行性能方面的体验和感想:

C++和 VM 类静态语言可以大约只差 2~4 倍,JVM 和 CLR 差异不大。

C++和动态语言之比,则可以是 15~5000 倍,不同动态语言的差异很大。

一直以为 Lua(JIT) 会是最快的通用脚本语言,没想到此测试中败给 JavaScript(V8),或许应该多点研究嵌入 V8 引擎 (SWIG 能支持就最理想了)。

以为 Python 和 Ruby 的性能相差不远,但测试结果两者大相径庭。暂时不太了解 Ruby 的特长,或许之后再研究其优点是否能盖过其性能问题。

最后建议读者,若要为某应用挑选语言,又要顾及性能,那么应该自己做实验去比较。不要盲目相信一些流言或评测 (包括本文)。

更新

2010/7/7: 新增的 C#_outref 测试,按 noremorse 的建议,把 Vec 和 Ray 变作 struct,所有函数传送这两种对象改为 ref/ out。 源代码。

2010/7/8: 新增 IronPython 和 Jython。

2010/7/11: 园友 noremorse 撰文 《Swifter C#之 inline 还是不 inline,这是个问题》,以本例研究.Net Runtime 的内联机制。

出处:Milo 的游戏开发

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐