Rokiのチラ裏

学生による学習のログ

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もある。

*1:著書「C++ Coding Standards」

*2:著書「ゲームエンジンアーキテクチャ第二版」。本書の著者はUnchartedやThe Last of Usの開発元であるノーティドッグ社のエンジニアであり、その著者がpre-incrementを優先的に使う事が馴染んでい(た/る)当時、pre-incrementの使用を推奨したため、話題となった。[要出典]因みにpost-incrementを推すその理由を本書からざっくり読み取ると、「一式として用いた場合、pre-incrementは、値を書き換えて戻すため、increment処理が終わるまで戻す値が決まらないので、その間はデータ依存性の問題が発生し、深いパイプラインのCPUではストールを発生させる可能性がある。post-incrementは、increment処理と戻す値は別インスタンスになるため、並列処理を行う事ができる。よってデータ依存する事なく後の文を実行できるので、ストールの発生が起こりえないため、post-incrementが優秀。」としている。