为什么这个C ++程序非常快速?
我写了一个基准来比较Python,Ruby,JavaScript和C ++的不同解释器/编译器的性能。 正如所料,事实certificate,(优化后的)C ++击败了脚本语言,但是这样做的因素却非常高。
结果是:
sven@jet:~/tmp/js$ time node bla.js # * JavaScript with node * 0 real 0m1.222s user 0m1.190s sys 0m0.015s sven@jet:~/tmp/js$ time ruby foo.rb # * Ruby * 0 real 0m52.428s user 0m52.395s sys 0m0.028s sven@jet:~/tmp/js$ time python blub.py # * Python with CPython * 0 real 1m16.480s user 1m16.371s sys 0m0.080s sven@jet:~/tmp/js$ time pypy blub.py # * Python with PyPy * 0 real 0m4.707s user 0m4.579s sys 0m0.028s sven@jet:~/tmp/js$ time ./cpp_non_optimized 1000 1000000 # * C++ with -O0 (gcc) * 0 real 0m1.702s user 0m1.699s sys 0m0.002s sven@jet:~/tmp/js$ time ./cpp_optimized 1000 1000000 # * C++ with -O3 (gcc) * 0 real 0m0.003s # (!!!) <---------------------------------- WHY? user 0m0.002s sys 0m0.002s
我想知道是否有人可以提供一个解释为什么优化的C ++代码比其他所有事情快三个数量级。
C ++基准testing使用命令行参数来防止在编译时预先计算结果。
下面,我放置了不同语言基准的源代码,这些基准在语义上应该是等价的。 另外,我提供了优化的C ++编译器输出(使用gcc)的汇编代码。 在查看优化程序集时,似乎编译器将基准testing中的两个循环合并为一个循环,但是仍然存在一个循环!
JavaScript的:
var s = 0; var outer = 1000; var inner = 1000000; for (var i = 0; i < outer; ++i) { for (var j = 0; j < inner; ++j) { ++s; } s -= inner; } console.log(s);
python:
s = 0 outer = 1000 inner = 1000000 for _ in xrange(outer): for _ in xrange(inner): s += 1 s -= inner print s
ruby:
s = 0 outer = 1000 inner = 1000000 outer_end = outer - 1 inner_end = inner - 1 for i in 0..outer_end for j in 0..inner_end s = s + 1 end s = s - inner end puts s
C ++:
#include <iostream> #include <cstdlib> #include <cstdint> int main(int argc, char* argv[]) { uint32_t s = 0; uint32_t outer = atoi(argv[1]); uint32_t inner = atoi(argv[2]); for (uint32_t i = 0; i < outer; ++i) { for (uint32_t j = 0; j < inner; ++j) ++s; s -= inner; } std::cout << s << std::endl; return 0; }
汇编(当用gcc -S -O3 -std = c ++ 0x编译上述C ++代码时):
.file "bar.cpp" .section .text.startup,"ax",@progbits .p2align 4,,15 .globl main .type main, @function main: .LFB1266: .cfi_startproc pushq %r12 .cfi_def_cfa_offset 16 .cfi_offset 12, -16 movl $10, %edx movq %rsi, %r12 pushq %rbp .cfi_def_cfa_offset 24 .cfi_offset 6, -24 pushq %rbx .cfi_def_cfa_offset 32 .cfi_offset 3, -32 movq 8(%rsi), %rdi xorl %esi, %esi call strtol movq 16(%r12), %rdi movq %rax, %rbp xorl %esi, %esi movl $10, %edx call strtol testl %ebp, %ebp je .L6 movl %ebp, %ebx xorl %eax, %eax xorl %edx, %edx .p2align 4,,10 .p2align 3 .L3: # <--- Here is the loop addl $1, %eax # <--- cmpl %eax, %ebx # <--- ja .L3 # <--- .L2: movl %edx, %esi movl $_ZSt4cout, %edi call _ZNSo9_M_insertImEERSoT_ movq %rax, %rdi call _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ popq %rbx .cfi_remember_state .cfi_def_cfa_offset 24 popq %rbp .cfi_def_cfa_offset 16 xorl %eax, %eax popq %r12 .cfi_def_cfa_offset 8 ret .L6: .cfi_restore_state xorl %edx, %edx jmp .L2 .cfi_endproc .LFE1266: .size main, .-main .p2align 4,,15 .type _GLOBAL__sub_I_main, @function _GLOBAL__sub_I_main: .LFB1420: .cfi_startproc subq $8, %rsp .cfi_def_cfa_offset 16 movl $_ZStL8__ioinit, %edi call _ZNSt8ios_base4InitC1Ev movl $__dso_handle, %edx movl $_ZStL8__ioinit, %esi movl $_ZNSt8ios_base4InitD1Ev, %edi addq $8, %rsp .cfi_def_cfa_offset 8 jmp __cxa_atexit .cfi_endproc .LFE1420: .size _GLOBAL__sub_I_main, .-_GLOBAL__sub_I_main .section .init_array,"aw" .align 8 .quad _GLOBAL__sub_I_main .local _ZStL8__ioinit .comm _ZStL8__ioinit,1,1 .hidden __dso_handle .ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2" .section .note.GNU-stack,"",@progbits
优化器已经发现内部循环和随后的行是一个空操作,并将其消除。 不幸的是,它还没有设法消除外部循环。
请注意,node.js示例比未优化的C ++示例更快,这表明V8 (节点的JIT编译器)设法消除了至less一个循环。 然而,它的优化有一些开销,因为(像任何JIT编译器一样),它必须平衡优化和configuration文件引导重新优化的机会与这样做的成本。
我没有对程序集做一个完整的分析,但是看起来像循环展开了内部循环,并且发现它与内部的减法一起是一个nop。
该组件似乎只做外部循环,只增加计数器,直到外部达到。 它甚至可以优化掉,但似乎没有这样做。
有没有办法在优化后cachingJIT编译后的代码,还是每次运行程序时都要重新优化代码?
如果我正在用Python编写代码,我会尽量减小代码的大小,以获取代码所做的“开销”视图。 喜欢尝试写这个(更容易阅读国际海事组织):
for i in range(outer): innerS = sum(1 for _ in xrange(inner)) s += innerS s -= innerS
甚至s = sum(inner - inner for _ in xrange(outer))
for (uint32_t i = 0; i < outer; ++i) { for (uint32_t j = 0; j < inner; ++j) ++s; s -= inner; }
内部循环相当于“s + = inner; j = inner”,这是一个优秀的编译器可以做到的。 由于variablesj在循环后不见了,整个代码就相当于
for (uint32_t i = 0; i < outer; ++i) { s += inner; s -= inner; }
同样,一个好的优化编译器可以删除对s的两个变化,然后删除variablesi,而且什么也没有留下。 这似乎是发生了什么事。
现在由您来决定这样的优化发生的频率,以及是否有任何实际的生活好处。
尽pipe循环有很多迭代,但程序运行起来可能还不够长,无法解释解释器/ JVM / shell /等的启动时间。 在某些环境中,这些可能会大量变化 – 在某些情况下,*咳嗽* Java *咳嗽*需要几秒钟才能到达您的实际代码附近。
理想情况下,你会在每段代码中执行。 在所有语言中准确地做到这一点可能会非常棘手,但是即使在打印出前后时钟的时间,也比使用time
打印时钟的时间要快,并且可以完成这项工作,因为您可能并不关心超精确的时间。
(我想这并没有真正涉及为什么C + +的例子是如此之快 – 但它可以解释其他结果中的一些变化)。