楽しいスタート2:for_each vs累積

問題の声明



私たちの目標は、コンパイラーに対するコードの最適化をどれだけ信頼できるかを理解することであり、そのタスクを容易にする(複雑にする)ことは可能ですか?



なぜすべてが同じなのですか?



コードを最適化するようコンパイラーに依頼して、測定を繰り返しましょう(前の記事を参照 )。 コンパイラーが夢中にならないように、合計の出力をコードに追加します。



cout << "sum=" << sum << endl;
      
      





累積してオプションをコンパイルします。



 sum = accumulate(vec.begin(), vec.end(), 0);
      
      





全コード
 #include <iostream> #include <sys/time.h> #include <iomanip> #include <vector> #include <algorithm> #include <functional> using namespace std; typedef int A; const int COUNT = 1000 * 1000 * 100; int main () { vector<A>vec(COUNT); generate(begin(vec), end(vec), std::rand); A sum = 0; struct timespec tm1, tm2; clock_gettime(CLOCK_REALTIME, &tm1); sum = accumulate(vec.begin(), vec.end(), A(0)); clock_gettime(CLOCK_REALTIME, &tm2); cout << "accumulate" << endl; double t1 = 1000.0 * tm1.tv_sec + tm1.tv_nsec / (1000.0 * 1000); double t2 = 1000.0 * tm2.tv_sec + tm2.tv_nsec / (1000.0 * 1000); cout << "t=" << setprecision(5) << t2 -t1 << " ms" << endl; cout << "sum=" << sum << endl; return 0; };
      
      





最適化を最大限に行う:



 $ g++ -std=c++11 main.cpp -o test -O3 && ./test $ duration 33.995 ms mseconds
      
      





この結果をfor_eachの結果と比較します。



 for_each(vec.begin(), vec.end(), [&sum](int i) { sum += i; });
      
      





 $ g++ -std=c++11 main.cpp -o test -O3 && ./test $ duration 34.21 ms mseconds
      
      





明示的なループオプションでも同様の結果が得られます。

最適化後の速度が同じになったのはなぜですか? この質問に答えるために、STLを見て、for_each関数が何であるかを見てみましょう。



 template<typename _InputIterator, typename _Function> for_each(_InputIterator __first, _InputIterator __last, _Function __f) { for (; __first != __last; ++__first) __f(*__first); return__f; }
      
      





for_eachを見るとわかるように、これはloopです。唯一の最適化は、コンパイラがfor_each関数をインラインにすることです:



 for (; __first != __last; ++__first) [&sum](int i) { sum += i; }(*__first);
      
      





ここでは、コンパイラがラムダをインラインにするのが妥当と思われます。 ベクトル反復子は本質的にポインターであるため、最終的に、最終的なアセンブラーコードは次のようになります。



 .L4: addq $1, %rax paddd (%rcx), %xmm0 addq $16, %rcx cmpq %r9, %rax jb .L4
      
      





手動でコードを書いたとしても、私はもっとうまくやっていなかっただろう 。 小さなプログラムと非常に少ない変数があるため、コンパイラはそれらをレジスタに配置しました。 不要なコピーや関数呼び出しはありません-優れています。2つの要素を同時に追加することもできます。



これで、最終コードの速度が「手動」ループ最適化、さらにはregisterキーワードによっても影響を受けない理由が明らかになりました。



速度は常に同じですか?



intの代わりに、sizeof(int)のサイズの単純な古典を要約しましょう:



 class A { int v = 0; public: A() {} A(int i); A(const A& a); operator int(); A & operator +=(const A& a); A operator +(const A& a); };
      
      





更新 0xd34df00d、kmu1990のヒントに感謝します-+ =演算子はリンクを返す必要がありますが、この場合は重要ではありません。演算子の結果は使用しません。



そして、その実装を別のファイルに配置します。



a.cpp
 A::A(int i) : v(i) {} A::A(const A &a) : v(av) {} A::operator int() { return v; } A & A::operator +=(const A &a){ v += av; return *this; } AA::operator +(const A &a) { return av + v; }
      
      







ここで、for_eachを使用したオプション:



 for_each(vec.begin(), vec.end(), [&](A i) { sum += i; });
      
      





コンパイルして実行:



 $ g++ -std=c++11 main.cpp a.cpp -o test -O3 && ./test $ duration 372.84 ms mseconds
      
      





そして単純なループ:



 for(int i = 0; i < COUNT; ++i) { sum += vec[i]; };
      
      





以下を開始します。



 $ g++ -std=c++11 main.cpp a.cpp -o test -O3 && ./test $ duration 240.57 ms mseconds
      
      





サイクルはまだ高速ですか? 実際-いいえ、for_eachの正しいバージョンは次のようになります。



 for_each(vec.begin(), vec.end(), [&](A &i) { sum += i; });
      
      





次に:



 $ g++ -std=c++11 main.cpp a.cpp -o test -O3 && ./test $ duration 240.8 ms mseconds
      
      





実際、コンパイラーは、私たちが書いたコピー演算子でどのような善行をしているのかわからないため、単に引数のコピーを削除する権利を持っていません。



for_eachの速度はサイクルの速度と同じでしたが、オプションを持つ累算は次のとおりです。



 sum = accumulate(vec.begin(), vec.end(), A(0));
      
      





まだ遅れている:



 $ g++ -std=c++11 main.cpp a.cpp -o test -O3 && ./test $ duration 410.52 ms mseconds
      
      





なぜそう for_eachを使用したバリアントのアセンブラコードを見てみましょう。



 .L12: leaq 160(%rsp), %rsi leaq 112(%rsp), %rdi movq %rbx, %rdx call _ZN1ApLERKS_ addq $4, %rbx cmpq %rbp, %rbx jne .L12
      
      





そして、accumulateを使用してオプションコードと比較します。



 .L7: leaq 144(%rsp), %rsi leaq 192(%rsp), %rdi movq %rbx, %rdx call _ZN1AplERKS_ movl 192(%rsp), %eax addq $4, %rbx cmpq %rbx, %rbp movl %eax, 144(%rsp) jne .L7
      
      





これは、コンパイラが合計を割り当てるよりも「+ = "演算子から軽いコードを生成するという事実によるものです。



 template<typename _InputIterator, typename _Tp> accumulate(_InputIterator __first, _InputIterator __last, _Tp __init) { for (; __first != __last; ++__first) __init = __init + *__first; return __init; }
      
      





したがって、不必要な移動操作。

ちなみに、合計を計算する特殊な関数は、ループやfor_eachを使用するよりも悪い結果をもたらすのは奇妙に思えませんか?



おわりに



コンパイラは人間ほど賢くありません。 そして、コードを簡単に変更するだけで、コンパイラーは混乱し、私たちが望むものや期待するものをまったく提供できなくなります。



保証された結果を取得したい場合は、すべてを自分で行うか、コンパイラがそこで改善したものと悪化したものを毎回確認する必要があります。 この規格は、ループと同様にfor_eachが最適化されることを保証していません。これは、移植可能なコードを記述する場合に重要です。



速度が重要でない場合は、常にSTLを選択してください。 コードは読みやすく、 平均してSTLコードは高速です。



All Articles