512x512畫素,每畫素10000個取樣,Intel C++ OpenMP版本渲染時間為18分36秒。估計Ruby版本約需351天。
前篇博文把一個C++全域性光照渲染器移植至C#,比較C++和C#之效能。刊出後,園友們不吝指出箇中問題,例如嗷嗷發現C++實現裡的隨機產生器採用了比較複雜的執行時函式,造成Visual C++和Intel C++的巨大差異;趙姐夫發現C#版本用class竟然比struct快等等。修改這些問題後,園友QiaoJie亦提出,可同時測試C++/CLI,檢測其所產生的IL程式碼,在同樣的.Net平臺上執行,看看是否比C#優勝。很多網友也提供了寶貴意見,未能盡錄,唯有以努力撰文作為答謝。本人陸續移植了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秒以外,不予顯示)。
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方法改寫為:
//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不用複製,其效能會比較高。但是程式碼會變得很難看:
// 原來用運算子過載(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功能:
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的特長,或許之後再研究其優點是否能蓋過其效能問題。
最後建議讀者,若要為某應用挑選語言,又要顧及效能,那麼應該自己做實驗去比較。不要盲目相信一些流言或評測(包括本文)。
附錄: JavaScript版本測試
警告: 建議使用Chrome。Firefox可能會慢得無法響應。
Run Stop更新
- 2010/7/7: 新增的C#_outref測試,按noremorse的建議,把Vec和Ray變作struct,所有函式傳送這兩種物件改為ref/ out。 原始碼。
- 2010/7/8: 新增IronPython和Jython。
- 2010/7/8: 園友貓糧撰文《AS3的光線跟蹤極限測試》,看來AS3效能不太好。