为什么这个C ++程序如此之快?


76

我写了一个基准测试来比较Python,Ruby,JavaScript和C ++的不同解释器/编译器的性能。不出所料,事实证明(优化的)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 ++基准测试使用命令行参数,以防止在编译时预先计算结果。

下面,我放置了不同语言基准的源代码,这些源代码在语义上应该是等效的。另外,我提供了用于优化的C ++编译器输出的汇编代码(使用gcc)。当查看优化的程序集时,似乎编译器将基准测试中的两个循环合并为一个循环,但是仍然存在一个循环!

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);

蟒蛇:

s = 0
outer = 1000
inner = 1000000

for _ in xrange(outer):
    for _ in xrange(inner):
        s += 1
    s -= inner
print s

红宝石:

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

35
好吧,我想这就是解释语言的开销。最好从脚本中进行计时,以避免花费时间启动和关闭解释器本身。
约瑟夫·曼斯菲尔德

7
“解释语言的开销”增加了以下事实:几种解释语言还具有一个JIT编译器,可以在运行时优化代码。您可能需要对一些可以保证运行更长的代码进行基准测试(分解成大量数字?找到第n个质数?),以查看比较结果是如何摆脱的……
abiessu 2014年

4
看起来更干净。
edmz 2014年

2
好吧,这不是一个嵌套循环,也许它删除了内部循环?那肯定会有所作为
harold 2014年

14
似乎您要了-O3,而编译器不得不修剪

Answers:


103

优化器已经确定内部循环以及随后的行是无操作的,并消除了它。不幸的是,它也没有设法消除外部循环。

请注意,node.js示例比未优化的C ++示例要快,这表明V8(节点的JIT编译器)已设法消除了至少一个循环。但是,它的优化有一些开销,因为(像任何JIT编译器一样)它必须在优化机会和配置文件引导的重新优化与这样做的成本之间取得平衡。


2
谢谢,这似乎是解决方案。当执行优化的可执行文件时,在增加第一个参数(“外部”计数器)时,会观察到运行时间的增加,但是如果我对第二个参数进行了增加,则运行时间将增加。
Sven Hager 2014年

21

我没有对程序集进行完整的分析,但是看起来它确实对内部循环进行了循环展开,并且发现与内部减法一起使用是可以的。

该程序集似乎只执行外部循环,该循环只会增加一个计数器,直到到达外部。它甚至可以优化它,但似乎并没有这样做。


6

在对JIT编译的代码进行优化之后,是否可以缓存它,还是必须在每次运行程序时重新对其进行优化?

如果我是用Python编写的,我会尝试缩小代码的大小,以便获得代码工作的“俯视图”。像尝试这样写(更容易阅读IMO):

for i in range(outer):
    innerS = sum(1 for _ in xrange(inner))
    s += innerS
    s -= innerS

甚至 s = sum(inner - inner for _ in xrange(outer))


2
for (uint32_t i = 0; i < outer; ++i) {
    for (uint32_t j = 0; j < inner; ++j)
        ++s;
    s -= inner;
}

内部循环等效于“ s + =内部; j =内部;”,这是一个好的优化编译器可以做到的。由于变量j在循环之后消失了,所以整个代码等效于

for (uint32_t i = 0; i < outer; ++i) {
    s += inner;
    s -= inner;
}

再一次,一个好的优化编译器可以删除对s的两个更改,然后删除变量i,并且什么也没剩下。看来就是这样。

现在由您决定这种优化发生的频率,以及它是否对现实生活有好处。


2

即使循环具有大量的迭代,程序可能仍无法长时间运行以逃避解释器/ JVM / shell / etc。等启动时间的开销。在某些环境中,这些变化可能会很大-在某些情况下*咳嗽* Java *咳嗽*可能需要几秒钟的时间才能接近实际代码。

理想情况下,您应该在每段代码中安排执行时间。跨所有语言准确地执行此操作可能很棘手,但是即使在前后用记号打印时钟时间也比使用更好time,并且可以完成此工作,因为您可能不关心此处的超精确计时。

(我想这与C ++示例如此之快的原因并不完全相关,但是它可以解释其他结果中的某些可变性。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.