web-dev-qa-db-ja.com

このC ++プログラムが非常に高速なのはなぜですか?

Python、Ruby、JavaScript、C++のさまざまなインタープリター/コンパイラーのパフォーマンスを比較するために、少しベンチマークを作成しました。予想どおり、(最適化された)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++コードが他のすべてのものよりも3桁以上高速である理由を誰かが説明できるかどうか疑問に思っています。

C++ベンチマークは、コンパイル時に結果が事前計算されるのを防ぐために、コマンドラインパラメーターを使用します。

以下に、意味的に同等であるさまざまな言語ベンチマークのソースコードを配置しました。また、最適化されたC++コンパイラ出力用のアセンブリコードを提供しました(gccを使用)。最適化されたアセンブリを見ると、コンパイラはベンチマークの2つのループを1つのループにマージしたように見えますが、それでもISまだループがあります!

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

ルビー:

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
76
Sven Hager

オプティマイザーは、内側のループと後続の行がノーオペレーションであることを確認し、それを排除しました。残念ながら、外側のループも削除できませんでした。

Node.jsの例は、最適化されていないC++の例よりも高速であり、 V8 (ノードのJITコンパイラー)が少なくとも1つのループを除去できたことを示しています。ただし、その最適化にはオーバーヘッドがあります(JITコンパイラと同様)最適化の機会とプロファイルに基づく再最適化の機会と、そうするためのコストとのバランスをとる必要があるためです。

102
ecatmur

アセンブリの完全な分析は行いませんでしたが、内部ループのループ展開を行ったように見え、内部の減算とともにnopであることがわかりました。

アセンブリは、外側に到達するまでカウンターをインクリメントするだけの外側ループを実行するようです。それを最適化することさえできたかもしれませんが、それはそれをしなかったようです。

21
wich

最適化した後にJITコンパイル済みコードをキャッシュする方法はありますか、またはプログラムを実行するたびにコードを再最適化する必要がありますか?

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

6
aoeu256

ループには多くの反復がありますが、プログラムはおそらくインタープリター/ JVM/Shell/etc。の起動時間のオーバーヘッドを回避するのに十分なほど長く実行されていません。環境によっては、これらは大きく異なる場合があります-場合によっては*咳* Java *咳*が実際のコードに近づくまでに数秒かかります。

理想的には、各コード内で実行のタイミングを計るでしょう。これをすべての言語で正確に行うのは難しいかもしれませんが、前後のティックでクロック時間を出力することは、timeを使用するよりも優れており、おそらくスーパーここで正確なタイミング。

(これは、C++の例がこれほど高速である理由とは実際には関係ないと思いますが、他の結果の変動の一部を説明できる可能性があります。:))。

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

内側のループは、「s + = inner; j = inner;」と同等です。これは、優れた最適化コンパイラーが実行できます。変数jはループの後に削除されるため、コード全体は次と同等です。

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

繰り返しますが、優れた最適化コンパイラーはsへの2つの変更を削除してから変数iを削除できますが、何も残っていません。それが起こったようです。

このような最適化が行われる頻度と、それが実際の利益になるかどうかを判断するのは、あなた次第です。

2
gnasher729