前置インクリメント vs 後置インクリメント

このアーティクルでは前置と後置、2種類のインクリメントについて考察と現状をまとめるものです。

 

長年の常識

従来(~2015)は前置インクリメント(++value)と後置インクリメント(value++)の挙動の違いを正しく完全に理解した上で前置インクリメントを使うべきだ。というのがC++プログラマの基本姿勢でした。

各インクリメントの標準的な実装を以下に示します。

 

後置インクリメントにはひと目で遅くなりそうな処理が見て取れますね。

前置インクリメントがインクリメント処理後、単純に自身の参照を返すのに対し、後置インクリメントではインクリメント前に一時オブジェクトの生成、そしてインクリメント後にはその前に生成した一時オブジェクトを値で返しています。

前置と後置では、単純にオブジェクトをコピーして返す分、普通に考えたら後置の方が遅いよね。というのが従来の認識でした。

「C++ Coding Standards -101のルール、ガイドライン、ベストプラクティス」の中でも、特に後置インクリメントの必然性が無い時は迷わず前置インクリメントを使うことが推奨されてきました。

元の値を必要としないときは前置形式の演算子を使おう __C++ Coding Standards (p50)

 

新たな主張

「ゲームエンジン・アーキテクチャ第二版」の中の一節を紹介します。

しかし、値が使われる場合、CPUのパイプラインでストールを生じさせないので、ポストインクリメントの方が優秀である。したがって、プレインクリメントの動作が絶対に必要である場合を除いて、必ずポストインクリメントを使う習慣を身につけたほうがよい。  __ゲームエンジンアーキテクチャ第二版 プレインクリメント vs ポストインクリメント

従来の主張と真逆の主張が展開されたのです。後置インクリメントの方が優秀なので出来る限りそちらを使うべきとされました。

ゲームエンジン・アーキテクチャの著者は、UnchartedやThe Last of Usの開発元であるノーティドッグ社のエンジニアです。その彼が後置インクリメントを使うべきと主張したことで大きな話題となりました。

後置インクリメントが優秀とされる理由は以下のとおりです。

  • 前置インクリメントは値を書き換えて戻すのでインクリメント処理が終了するまで戻す値が決まらない
  • それはデータ依存性を生み、深いパイプラインのCPUではストールを発生させる
  • 後置インクリメントはインクリメント処理と戻す値は別インスタンスなので並列に処理が可能
  • よってストールの発生が無い分、後置インクリメントが優秀

なるほど、といった感じです。

 

 考察を深めよう

今後は後置インクリメントに統一だ!今まで書いた前置インクリメントを全部置換しなくちゃ!と、急ぐ前に、本当にゲームエンジン・アーキテクチャの主張が正しいかは一考の余地があります。

深いパイプラインにおけるストールの可能性はデータ依存性があるかぎり発生しうる問題です。

この部分の主張は正しいです。

しかし、ゲームエンジン・アーキテクチャでは後置インクリメントに発生していたコピーコストについて言及がありません。

文脈を読み解く限り、「コピーコスト<ストールコスト」という前提があるようです。このインクリメントの項以外でもゲームエンジン・アーキテクチャではストールに対する嫌悪を書いてある箇所があり、著者はかなりストールコストに対してナイーブになっているように感じます。

一時オブジェクトの生成に伴うコストについて、好意的に解釈してみましょう。

例で示した実装の場合、後置インクリメントで変更前の値を返す際にRVOの最適化が期待されるので、前置インクリメントも後置インクリメントも(このインクリメントの結果として使う時の)オブジェクトの生成は1度きりになる可能性があります。厳密には直前のテンポラリオブジェクトが(oldという)名前付きオブジェクトなので、この最適化を期待するためにはコンパイラがNRVO(Named Return Value Optimization)に対応している必要があります。それでもほとんどのコンパイラはNRVOが効くのでコピーコストは低くなる可能性があります。

なお手元のmsvc(Visual Studio 2013 Professional)ではNRVOに対応していませんでした……。

ちゃんとNRVOが効くコンパイラ(今回はgcc)を使って実際のアセンブラコードを見てみましょう。

ユーザー定義の前置インクリメントと後置インクリメントを用意します。

前置インクリメント、後置インクリメントそれぞれの処理を呼び出す処理を書きます。

pre.cpp

post.cpp

これら2つの処理のアセンブラコードを出力します。

アセンブラコードを読むのが嫌いな人は行数だけ見ればいいです。

 

やっぱり後置インクリメントの方が遅いですね。

では次はコンパイラの最適化オプションをバッチリ有効にしたバージョンで見てみましょう。

 

 

なんということでしょう。

前置も後置もまったく同じ結果になりました。流石、昨今のコンパイラは優秀です。

この結果から考えると、確かに一時オブジェクトの生成に対するコストよりCPUがストールすることの方がコスト高だという結論を導くことはできそうです。

 

現実的な観点

我々は現実の世界で実際にコードを書くプログラマです。そこには机上の論理以外の様々な要素を考慮する必要があります。

まず、第一に、インクリメントがストールをもたらす為には、その結果を使う必要があります。結果を使おうとして初めてストールの可能性が生まれます。

ただ単に一行

と書いてもストールすることはありません。

平行して結果を使わないからです。

今回のゲームエンジン・アーキテクチャの主張は、後置インクリメントはインクリメントの結果を待たなくても、戻り値のオブジェクトは決定しているのでそのオブジェクトに対する処理はインクリメントの動作と並行して行えるよね。ならストールしないよね。というものです。

つまり後置インクリメントで戻される、インクリメント前のオブジェクトを使う必要があります。式の最中で。

あまり馴染みが無いシチュエーションだと感じる人も多いのではないでしょうか。もちろん、無理やり気味に、そう書くことは可能です。

しかし、今のコーディングプラクティスにおいて、一行のステートメント中で複数の処理を書くことは可読性を落とすことになります。しかも式中でインクリメントとその結果を利用するようなものなら、なおさら。

結果として、後置インクリメントであることがアドバンテージになるシチュエーションが訪れないのなら、単に一時オブジェクトの生成に対する懸念がある分、後置インクリメントの方が不利なのかなという気もします。

第二に、ストールのコストは肌で感じづらいという問題があります。

処理に直接関係するコストは、例えばアセンブラコードの出力を見れば把握できます。

余分なオブジェクトが生成されているな、とか、最適化されているな、とか。

しかし、あるコードを書いた時にその部分の処理によってCPUにストールが発生しているかというのは極めて把握が難しい問題です。なるべくCPUの効率を上げるコーディングの仕方というものはもちろん存在していて、でもそういったものは往々にして直感的ではなく、可読性や保守性を下げるものです。

CPUのアーキテクチャへの理解と、現実にコードを書く我々、この2つを勘案した上で、実際に前置インクリメントを使うべきか、後置インクリメントを使うべきか、また今の時代そういうことは気にしないべきか、考える必要があります。

私は、後置インクリメントを使っている事を指摘した際に「だってゲームエンジン・アーキテクチャに書いてあったからなんとなく」という答えをするプログラマが増えない事だけを願っています。

 

まとめ

  • 前置インクリメントを使うべきという主張の理由を理解しよう
  • 後置インクリメントを使うべきという主張の意図を汲もう
  • 時代の変化とともに柔軟に再考しよう

 

 


コメントを残す

メールアドレスが公開されることはありません。

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">