for文でのpre-incrementとpost-incrementはどちらが早いか
Teaching Assistantをしていて、こういった質問をしばしば受ける。
このエントリタイトルの問題は周知の事実なので、書く必要もない気がするが、自分が伝える上でどのような例を出して説明すると、他人が理解しやすいかを残しておきたいので、綴ろうと思う。
結論から言えば、NRVOを考慮せず言語機能の範囲内で動作の仕組みを考えるとするならば、pre-incrementの方が早いと言えるはずである。これは、自分で、プリミティブ型ライクなクラスを作ると、post-incrementの仕組みを考える事になるので、分かりやすい。
struct Integer{ explicit Integer(int t):_data(t){} explicit Integer(const Integer&)=default; Integer& operator++() { ++_data; return *this; } Integer&& operator++(int) { Integer tmp(*this); _data+=1; return std::move(tmp); } template<class _Tp> const bool operator<(_Tp&& a)const{return _data<a;} int get()const noexcept{return _data;} private: int _data=0; };
operator post-incrementのoverloadでは評価後にincrementするという要件を満すために、新たにオブジェクトを生成する必要がある。このIntegerをfor文にpost-incrementとして突っ込むと、increment毎にオブジェクトが生成されるので、当然の事ながらpre-incrementより遅いと言える。
void f() { auto ar=std::make_array(1,2,3,4,5); // P0325R1 for(Integer i(0); i<ar.size(); ++i)ar[i.get()] /* do something */; for(Integer i(0); i<ar.size(); i++)ar[i.get()] /* do something */; }
因みに冒頭ではNRVO等を考慮しないと書いたが、NRVOが効くと以下のように、pre-incrementとpost-incrementによってのオブジェクト生成の差異がなくなる様子が伺える。
// for(Integer i(0); i<ar.size(); ++i)ar[i.get()];のアセンブリコード 006e 488D45D0 leaq -48(%rbp), %rax 0072 BE000000 movl $0, %esi 00 0077 4889C7 movq %rax, %rdi 007a E8000000 call _ZN7IntegerC1Ei 00 .L22: 007f 488D45E0 leaq -32(%rbp), %rax 0083 4889C7 movq %rax, %rdi 0086 E8000000 call _ZNKSt5arrayIiLm5EE4sizeEv 00 008b 488945D8 movq %rax, -40(%rbp) 008f 488D55D8 leaq -40(%rbp), %rdx 0093 488D45D0 leaq -48(%rbp), %rax 0097 4889D6 movq %rdx, %rsi 009a 4889C7 movq %rax, %rdi 009d E8000000 call _ZNK7IntegerltImEEKbOT_ 00 00a2 84C0 testb %al, %al 00a4 742C je .L21 00a6 488D45D0 leaq -48(%rbp), %rax 00aa 4889C7 movq %rax, %rdi 00ad E8000000 call _ZNK7Integer3getEv 00 00b2 4863D0 movslq %eax, %rdx 00b5 488D45E0 leaq -32(%rbp), %rax 00b9 4889D6 movq %rdx, %rsi 00bc 4889C7 movq %rax, %rdi 00bf E8000000 call _ZNSt5arrayIiLm5EEixEm 00 00c4 488D45D0 leaq -48(%rbp), %rax 00c8 4889C7 movq %rax, %rdi 00cb E8000000 call _ZN7IntegerppEv 00 00d0 EBAD jmp .L22
// for(Integer i(0); i<ar.size(); i++)ar[i.get()];のアセンブリコード 00d2 488D45D0 leaq -48(%rbp), %rax 00d6 BE000000 movl $0, %esi 00 00db 4889C7 movq %rax, %rdi 00de E8000000 call _ZN7IntegerC1Ei 00 .L24: 00e3 488D45E0 leaq -32(%rbp), %rax 00e7 4889C7 movq %rax, %rdi 00ea E8000000 call _ZNKSt5arrayIiLm5EE4sizeEv 00 00ef 488945D8 movq %rax, -40(%rbp) 00f3 488D55D8 leaq -40(%rbp), %rdx 00f7 488D45D0 leaq -48(%rbp), %rax 00fb 4889D6 movq %rdx, %rsi 00fe 4889C7 movq %rax, %rdi 0101 E8000000 call _ZNK7IntegerltImEEKbOT_ 00 0106 84C0 testb %al, %al 0108 7431 je .L23 010a 488D45D0 leaq -48(%rbp), %rax 010e 4889C7 movq %rax, %rdi 0111 E8000000 call _ZNK7Integer3getEv 00 0116 4863D0 movslq %eax, %rdx 0119 488D45E0 leaq -32(%rbp), %rax 011d 4889D6 movq %rdx, %rsi 0120 4889C7 movq %rax, %rdi 0123 E8000000 call _ZNSt5arrayIiLm5EEixEm 00 0128 488D45D0 leaq -48(%rbp), %rax 012c BE000000 movl $0, %esi 00 0131 4889C7 movq %rax, %rdi 0134 E8000000 call _ZN7IntegerppEi 00 0139 EBA8 jmp .L24
なににしろ、pre-increment的な動作をしているのには変わりない。
...とここまでの雰囲気からして、pre-incrementを褒め称えるかのような流れになっているが、タイトル通り、速度問題の観点をエントリにしているのであって、特にpre-incrementとpost-incrementについて優劣を付けようとしているわけではないという事にご配慮をお願いしたい。
時と場合によって、それぞれの優劣は付けられるものだと、僕は考えている。そもそも、時と場合によって必要だからこそ、言語規格にpre-incrementとpost-incrementの両方が導入されたはずである。
"元の値を必要としないときはpre-incrementの演算子を使おう"と促す著書*1もあるし、"pre-incrementの動作が絶対に必要である場合を除いて、必ずpost-incrementを使う習慣を身につけたほうが良い"と断言する著書*2もある。
*2:著書「ゲームエンジン・アーキテクチャ第二版」。本書の著者はUnchartedやThe Last of Usの開発元であるノーティドッグ社のエンジニアであり、その著者がpre-incrementを優先的に使う事が馴染んでい(た/る)当時、pre-incrementの使用を推奨したため、話題となった。[要出典]因みにpost-incrementを推すその理由を本書からざっくり読み取ると、「一式として用いた場合、pre-incrementは、値を書き換えて戻すため、increment処理が終わるまで戻す値が決まらないので、その間はデータ依存性の問題が発生し、深いパイプラインのCPUではストールを発生させる可能性がある。post-incrementは、increment処理と戻す値は別インスタンスになるため、並列処理を行う事ができる。よってデータ依存する事なく後の文を実行できるので、ストールの発生が起こりえないため、post-incrementが優秀。」としている。