本文作者在開發Dynym項目,這是一個動態語言的通用運行時。在開發時,作者以其 他語言的運行速度作為基礎比較語言的運行速度,因此發現了一些小秘密。迭代計算斐波那契數列是測試各種語言執行速度的常見方法。作者以不同的語言進行測 試,最終發現C語言要比Python編寫的計算斐波那契數列快278.5倍。在底層開發,以及專注性能的應用程序中,選擇是顯而易見的。而為什麼會有如此 大的運行性能差距呢。作者進一步研究了程序的反匯編代碼,發現差別出在內存的訪問次數,以及預測的CPU指令的正確性方面。
原作者注:在本文最開始,我並沒說明進行模2^64的計算——我當然明白那些不是“正確的”斐波那契數列,其實我不是想分析大數,我只是想探尋編譯器產生的代碼和計算機體系結構而已。
最近,我一直在開發Dynvm——一個通用的動態語言運行時。就像其他任何好的語言運行時項目一樣,開發是由基准測試程 序驅動的。因此,我一直在用基准測試程序測試各種由不同語言編寫的算法,以此對其典型的運行速度有一個感覺上的認識。一個經典的測試就是迭代計算斐波那契 數列。為簡單起見,我以2^64為模,用兩種語言編寫實現了該算法。
用Python語言實現如下:
- SZ = 2**64
- i = 0
- a, b = 1, 0
- while i < n:
- t = b
- b = (b+a) % SZ
- a = t
- i = i + 1
- return b
用C語言實現如下:
- #include <stdio.h>
- #include <stdlib.h>
- typedef unsigned long ulong;
- int main(int argc, char *argv[])
- {
- ulong n = atoi(argv[1]);
- ulong a = 1;
- ulong b = 0;
- ulong t;
- for(ulong i = 0; i < n; i++) {
- t = b;
- b = a+b;
- a = t;
- }
- printf("%lu\n", b);
- return 0;
- }
用其他語言實現的代碼示例,我已放在github上。
Dynvm包含一個基准測試程序框架,該框架可以允許在不同語言之間對比運行速度。在一台Intel i7-3840QM調頻到1.2 GHz)機器上,當 n=1,000,000 時,對比結果如下:
- =======================================================
- 語言 時間 (秒)
- =======================================================
- Java 0.133
- C Language 0.006
- CPython 0.534
- Javascript V8 0.284
很明顯,C語言是這裡的老大,但是java的結果有點誤導性,因為大部分的時間是由JIT編譯器啟動~120ms)占用的。當n=100,000,000時,結果變得更明朗:
- =======================================================
- 語言 時間秒)
- =======================================================
- Java 0.300
- C Language 0.172
- CPython 47.909
- Javascript V8 24.179
在這裡,我們探究下為什麼C語言在2013年仍然很重要,以及為什麼編程世界不會完全“跳槽”到Python或者 V8/Node。有時你需要原始性能,但是動態語言仍在這方面艱難掙扎著,即使對以上很簡單的例子而言。我個人相信這種情況會克服掉,通過幾個項目我們能 在這方面看到很大的希望:JVM、V8、PyPy、LuaJIT等等,但在2013年我們還沒有到達“目的地”。
然而,我們無法回避這樣的問題:為什麼差距如此之大?在C語言和Python之間有278.5倍的性能差距!最不可思議的地方是,從語法角度講,以上例子中的C語言和Python內部循環基本上一模一樣。
為了找到問題的答案,我搬出了反匯編器,發現了以下現象:
- 0000000000400480 <main>:
- 247 400480: 48 83 ec 08 sub $0x8,%rsp
- 248 400484: 48 8b 7e 08 mov 0x8(%rsi),%rdi
- 249 400488: ba 0a 00 00 00 mov $0xa,%edx
- 250 40048d: 31 f6 xor %esi,%esi
- 251 40048f: e8 cc ff ff ff callq 400460 <strtol@plt>
- 252 400494: 48 98 cltq
- 253 400496: 31 d2 xor %edx,%edx
- 254 400498: 48 85 c0 test %rax,%rax
- 255 40049b: 74 26 je 4004c3 <main+0x43>
- 256 40049d: 31 c9 xor %ecx,%ecx
- 257 40049f: 31 f6 xor %esi,%esi
- 258 4004a1: bf 01 00 00 00 mov $0x1,%edi
- 259 4004a6: eb 0e jmp 4004b6 <main+0x36>
- 260 4004a8: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
- 261 4004af: 00
- 262 4004b0: 48 89 f7 mov %rsi,%rdi
- 263 4004b3: 48 89 d6 mov %rdx,%rsi
- 264 4004b6: 48 83 c1 01 add $0x1,%rcx
- 265 4004ba: 48 8d 14 3e lea (%rsi,%rdi,1),%rdx
- 266 4004be: 48 39 c8 cmp %rcx,%rax
- 267 4004c1: 77 ed ja 4004b0 <main+0x30>
- 268 4004c3: be ac 06 40 00 mov $0x4006ac,%esi
- 269 4004c8: bf 01 00 00 00 mov $0x1,%edi
- 270 4004cd: 31 c0 xor %eax,%eax
- 271 4004cf: e8 9c ff ff ff callq 400470 <__printf_chk@plt>
- 272 4004d4: 31 c0 xor %eax,%eax
- 273 4004d6: 48 83 c4 08 add $0x8,%rsp
- 274 4004da: c3 retq
- 275 4004db: 90 nop