カテゴリー別アーカイブ: Compiler

ブレークポイントコード

これを見てピンと来た人は何人いるだろうか。

 

ああ、x86系CPU環境でブレークポイントと同じ効果をもたらすアセンブラコードだなーと思った人。正解ですが本質的ではありません。

ああ、闇夜のc++とか言いながらついにネタ切れでc++の話以外を書き始めたなーと思った人。正解だし本質的です。

xcodeやVisual Studioで以下のコードを実行してみましょう。

int 3というアセンブラ命令はデバッガへのトラップ用の割り込みを発生させるので、5行目で勝手にデバッガがブレークします。

そこからステップトレースも、再実行も可能です。

マクロを作っておくと何かと便利です。

より汎用的に使うためにglibでは環境毎にマクロ定義を切り替えてます。

glibのgbacktrace.hを覗いてみましょう。

環境依存ついでに、より環境依存な話をしておくとmsvc向けに__debugbreak();という命令もあります。やってることはint 3と一緒なので覚える必要はないでしょう。

環境依存なので3DS向けゲームの開発しかしてない人には無用の知識です。

まとめ

  1. c++について書くことが思いつかなくなった
  2. __asm int 3;はブレークポイント発生コード
  3. マクロ化しておくと使い勝手がいい
  4. 汎用的な実装がglibで提供されている

 

ローカルな文字列テーブル

関数内で使うローカルな文字列テーブル(配列)の定義には注意が必要です。

1月~12月までの月を数値で渡すと、英語の文字列を取得できる関数monthToEnglishを作ってみましょう。

例.第一引数に5を渡すとMayという文字列が第二引数のバッファに格納される

無事、完成しました。

有効範囲のエラー処理など細かい部分は端折ってますが適切な値を渡す限りは要件に見合った挙動をします。

さてこの関数ですが、大いなる無駄処理が走る可能性があります。

最適化を有効にした上で、アセンブラを見てみましょう。

41行目~に注目です。

MONTH_ENGLISH_TABLEはこの関数が呼び出される度にスタック上にコピーされます。ちなみにconst を constexprに変えても出力結果は同じです。

スタック上に値を生成したいという目的が無いのならば、このコードはこの関数が呼ばれる度にただただ不要な処理を走らせることになるのであまりよろしくありません。

例えばテクスチャ名のリスト、モーション名のリスト、気軽にローカルで定義することは良くあるかと思います。

そういう時に、gcc 4.9.2 で -O3ですら、前述のような(プログラマの意図に対して)無駄な処理が走ることは覚えておく価値があると思います。

この問題は定義にstaticを付けることで簡単に回避できます。

この場合のアセンブラコードを見てみましょう。

見違えるほどスッキリしました。

このように、関数ローカルな場所に文字列テーブルを作る時は呼吸をするようにstaticを付けるというプラクティスは悪くありません。

まとめ

  1. ローカルな文字列テーブルの定義は無駄なコストが生じる可能性がある
  2. ローカルな文字列テーブルにはstaticをつけよう

Empty Base Optimization

「空のクラス」を定義します。

Sky classという意味ではありませんよ。Empty classという意味です。

メンバ変数を持たず継承もしていない、とにかくデータとしての要素の無いクラスです。

このEmptyClassのサイズはいくつでしょうか?

Emptyという位なのでゼロなのかと思いきや、sizeofで計るとゼロ以外の数値になります。

Wandboxで見てみましょう

sizeof(Empty) = 1ですね。

なぜか?その答えはある名曲に隠されています。

言葉はいつも奥の方から後ろに虚しさ連れて教えてくれた
けれどこんなにもからっぽになったのに僕は歩き出した

__ゆず「からっぽ」

からっぽに見えても、みんな何かを抱えて生きているのです。ゼロなんてありえないんですね。C++のクラスも同じなんですね。

納得感を得たところで、改めて言うと、C++の型は全くデータメンバが無い状態でも単独でインスタンスが作られる場合必ずサイズを持ちます。

C++のテクニカルな理由で、「空のクラス」でも、「独立したオブジェクト」のサイズは0にできないのです。

__Effective C++ 第3版

「テクニカルな理由」というのは、インスタンス化された際にオブジェクトのアドレスをとれるようにする為です。サイズ0ではアドレスが決まりませんからね。

このようにc++では単独でインスタンス化されるオブジェクトは例外なくサイズを持ちます。

例えばメンバ変数の一部として定義された場合もサイズを持ちます。

この場合でも&Hoge::eを決めなければいけないのでeはサイズを持ちます。結果として sizeof(Hoge) > sizeof(int) となります。

たった1byteとはいえ無駄なメモリを消費することになります。

コピー禁止をmix-inするboost::noncpyable等はまさにメンバを持たない空のクラスですね。コピー禁止するたびにデータサイズが増加したらたまったものではありません。

そんなわけで、規格では、空の型が別の型を継承していて、継承元の型が既にサイズを持っている場合や、逆に空の型の継承先がサイズを持っている場合はEmptyClass自体の追加のサイズは無くしても良いことになっています。

 

型を継承した時に、追加のサイズを持たせない最適化のことをEBO(Empty Base Optimization)と言います。

EBOの効かないコンパイラなんてほとんどありませんが、場合によっては型サイズの肥大化やアライメントのズレの原因になるので注意する必要があります。

EBOを意識した実装

コンパイラが空のクラスを最適化してくれることは理解しました。

プログラマもEBOが効くコードを意識して書きましょう。

先のboost::noncopyableも空のクラスでしたが、他にも空のクラスを定義するシチュエーションは数多くあります。

例えばファンクタなんかも、往々にしてoperator()だけ定義して、メンバ変数も特に無いというケースがあるかと思います。

スマートポインタのデリータやコンテナのアロケータを渡して実装を決めるテンプレートクラスもEBOを意識した設計にすることでメモリ効率が上がります。

以下が一例になります。オリジナルのコンテナクラスにテンプレートパラメータとしてアロケータを渡す実装で考えてみましょう。

まずはEBOを意識しない設計。

普通にAllocatorをメンバとして持っています。

続いてEBOを意識した設計。

Allocatorを継承した設計になっています。

 

サイズを計ってみましょう。Wandboxで確認

このようにちょっと意識することでメモリ効率の良いクラス設計ができるようになります。

 

まとめ

  1. C++では単独でインスタンス化されるオブジェクトは全て1以上のサイズを持つ
  2. サイズを持つ理由はアドレスを決定できるようにするため
  3. 他にサイズを持っている型と継承関係を持つことで空の型のサイズは最適化される
  4. この最適化をEBOという
  5. ちょっとEBOを意識したクラス設計にすると「解ってる感」が演出されて良い

 

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

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

 

長年の常識

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

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

 

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

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

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

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

元の値を必要としないときは前置形式の演算子を使おう

__C++ Coding Standards (p50)

 

新たな主張

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

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

__ゲームエンジンアーキテクチャ第二版 プレインクリメント vs ポストインクリメント

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

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

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

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

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

 

 考察を深めよう

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

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

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

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

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

実際にコンソールゲーム機のアーキテクチャにはストールのコストが引くほど高いものもあり、それこそPS2の時代はとにかくCPUの稼働効率を高めるようなコードを意識して書かないと描画ポリゴンが倍は違うというような状況でした。XBOX360の頃でさえ、シーングラフ(ゲームシーンの描画を効率的に管理したりする構造)を作っても、その走査や判定のコストの方が高い為、単純にシーンにある全オブジェクトを順番に描画するだけの方が速かったという経験があります。

 

 一時オブジェクト生成のコスト考察

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

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

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

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

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

pre.cpp

post.cpp

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

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

 

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

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

 

 

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

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

これは例として示したコードのようにインクリメントの実装詳細まで一つのコンパイル単位に含まれている関係で、コンパイラが副作用がないことを把握しながらインライン展開まで行える為、とにかく結果だけ合うように最速のパフォーマンスを組み上げられる為です。

もう一歩進んで、Hoge::operator++()とHoge::operator++(int)を別のcppに移しました。

それではアセンブラコードを見てみましょう。

まずはhoge.cpp自体のアセンブラコード。

pre.cpp

post.cpp

やはりコード次第では、最適化オプションを有効にしていても、前置インクリメントにアドバンテージがあるケースがありますね。

hoge.cppなんてコンパイラにとったら何のヒントも無いコンパイル単位なので、素直に処理するしかないので当たり前といえば当たり前です。

しかし、世の中には、リンク時最適化というものがあり、オブジェクトコード生成後に再度ファイルを走査し、リンクのタイミングでインライン展開されるような場合もあるので、この例で一概に前置が有利と結論付けることはできません。

ストールコストの考察

ストールが発生するシチュエーションを考えます。

ストールは本来、このインクリメントに限らず、データ依存性を持つあらゆるシチュエーションで発生する恐れがあります。条件分岐、値の更新など、結果を待たなければ先の処理が行えないケースが発生するたびストールは起こりえます。分岐予測など、ストールが実質的に無くなるような工夫ももちろん行われているのですが、予測が外れた場合には、事前準備の甲斐なく、やっぱりストールします。

まず、インクリメントの外、インクリメント結果を使う瞬間に発生するストールについてですが、ただ単に一行

と書く場合、前置と後置で何も変わりません。

次の行以降で使う値はインクリメント後の値なので、前置後置関係無く、値の更新が完了しないと何も始まらないからです。

この例の場合、while文のtest exprに入った段階でiの内容が確定している為、インクリメントの結果を待たずに次の処理を進めることができます。こういうようなシチュエーションでは前置と後置でストールによるコストが変わってきます。

インクリメントの中で発生するストールもについても意識する必要がります。

前述のとおり、ストールはどこでも発生する可能性があります。

このストールがインクリメント演算の中で発生した場合、インクリメント演算の度にCPUはストールしてしまいます。しかし、後置インクリメントであれば、インクリメント演算の結果を待たず次の処理へ移れる為、有利と言えるでしょう。

最悪の場合、インクリメントの中でストールするわ、インクリメントの外でストールするわと、春先の有楽町と同じくらいストールで溢れかえってしまいます。実際にCPUがストールしまくる状態のことを「海外セレブ効果(Celebrity Fashion)」と呼びます。嘘です。

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

逆に、コードの流れの中で、インクリメントした後の結果しか使わないのであれば、前置でも後置でもCPUのコストは変わりません。仮にインクリメント演算子の中で70,000サイクルの処理がかかるとしても、インクリメント後の値しか使わないのであれば、前置でも後置でも等しくその負荷が計上されることになります。

 

計測サンプル

ダラダラとアセンブラコード貼ってる暇があったら実測値をだしてみろよ!投資家は数字でしか動かねーんだよ!というYコンビネーターマインドの方もいるかもしれません。

以下のサイトではインクリメントのコストを複数のコンパイラで比較検証されていますので一例として参考にして頂ければと思います。

インクリメントの前後置速度比較(vc++/gcc/clang)|他人の空似

典型的なfor文を前置後置それぞれのインクリメントを使用する形で作成。
インクリメント対象をunsigned int/iterator/適当に重めのクラスの三種それぞれを対象。
また、最適化により返り値計算が消えることを考えforの真偽判定でインクリメントの返り値を使用するものも用意。
以上をそれぞれ100,000,000回実行をさらに10回繰り返しかかった時間の平均をとる。
(中略)

vc++ gcc clang
int前置インクリメント 46ms 61ms 41ms
int後置インクリメント 51ms 44ms 53ms
クラス前置インクリメント 320ms 47ms 316ms
クラス後置インクリメント 2206ms 388ms 446ms

__インクリメントの前後置速度比較(vc++/gcc/clang)|他人の空似

全計測の結果は元サイトよりご確認下さい。

なお、余談ではございますが闇夜のC++のサイト運営に2億くらい出してもいいよというエンジェル投資家の方がいらっしゃいましたら、ご連絡下さい。

 

現実的な観点

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

結論を出す前に我々が向き合わなければいけない現実的な考慮事項は4つあります。

まず第1に、一時オブジェクトの検証結果ですが、インライン展開が効かない、前置インクリメントにアドバンテージのあるシチュエーションにするには、リンク時最適化の効かないコンパイラ上で、ユーザー定義のインクリメントオペレータ演算子をcpp側に書く必要があります。
インクリメント演算子もデクリメント演算子もクラス書くたびに必ず用意してます(しかもヘッダーファイルじゃなくcpp側に!)という人は別として、一般的に最も多く呼び出されるインクリメント演算子は組み込みのint、もしくはSTLのイテレータに対してではないでしょうか。
その場合、やはり先程の検証でもあったように、コンパイラの素晴らしい最適化により、前置と後置のコストの違いは限りなく少なくなります。

 

第2に、インクリメント演算によって発生するなんらかのストールを回避するにはインクリメント前の値を使うようなコードを心がける必要があります。
例えば、インクリメント演算子を式中で使ったり、インクリメント後もインクリメント前の値に対する処理を書くという感じです。
しかし可読性の観点からすると、あまり推奨されるようなコードではありません。

 

第3に、ストールのコストは肌で感じづらいという問題があります。
処理に直接関係するコストは、例えばアセンブラコードの出力を見れば把握できます。
余分なオブジェクトが生成されているな、とか、最適化されているな、とか。
しかし、あるコードを書いた時にその部分の処理によってCPUにストールが発生しているかというのは極めて把握が難しい問題です。

 

第4に、しかし、それでも、いかんともしがたく、アーキテクチャに引っ張られることがあるという点です。
中盤でも触れましたが、一部のアーキテクチャにおいてはストールの発生を抑えなければロクに処理が動かないというケースがあるのも事実です。そういうアーキテクチャと向き合わなければならない場合、全く一切選択の余地なく、可読性うんぬん以前に、方針を決めざるをえない場合もあります。

 

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

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

 

まとめ

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

 

 

Return Value Optimization

オブジェクトの生成と破棄には莫大なコストがかかることがあります。

CPUによるメモリ割り当て、コンストラクタ・デストラクタの挙動等、オブジェクトが一つ生成され、破棄されるまでのコストは無視できません。あなたの目の届かないところで提供されたサードパーティ製のライブラリのクラスでは、コンストラクタ内で自分のメンバからCRC値を生成して勝手にツイートする実装になっているかもしれません。

コンパイラも、そんな我々の気持ちを汲んで、なるべく一時オブジェクトの生成を抑制=最適化しようとしてくれます。

そんな最適化の一つが今回説明するRVO(Return Value Optimization)です。

関数の戻り値を値で返す時に発生する一時オブジェクトをコンパイラは可能な限り消し去ります。

 

一切なんの最適化も行われないと仮定すると上記のコードは3回Hogeオブジェクトを生成することになります。

 

しかし、実際には1度しかコンストラクタ・デストラクタは呼ばれません。

RVOが効く為です。

現代の主要なコンパイラにとってこの最適化は当たり前となっており、gccでもclangでも、armccでもRVOは効きます。

Wandboxで際に実行してみると、コンストラクタ・デストラクタがそれぞれ一度しか呼ばれないことが確認できます。

RVOによってコンパイラに生成されるコードの詳細についてはEfficient C++の第四章「戻り値の最適化」にも説明があります。

Named Constructor Idiom

この最適化を念頭に入れておけば、値を返す関数を可読性の高いコンストラクタ呼び出しとして使うことができます。

 

これを Named Constructor Idiomと言います。

More C++ Idiomsにも載っていますので参照すると良いでしょう。

 

まとめ

  1. コンパイラは可能な限り一時オブジェクトの生成を抑制する
  2. 戻り値の最適化によって一時オブジェクトの生成が抑制されることをRVOという
  3. RVOを利用して余計なコストを気にせず可読性を上げる手法をNamed Constructor Idiomという

メソッド内でのthis最適化

順調に動いていたはずのプログラムが突然ダンプを吐いてクラッシュする・・・。

まさに悲劇としか言えません。

ある調査機関によると成人男性の約89%がこの不幸に出会っているという報告もあります。[要出典]

最も多く、そして最も簡単に発生する原因としてnullポインタアクセスが挙げられます。以下のコードを見てみましょう。

このプログラムは実行するとすぐさまクラッシュします。当然ですね。

問題はどこでアクセスエラーが発生するか?です。

正解は5行目の

の部分なのですが、なぜメソッド呼び出し

ではなく、その内部の処理まで進むのでしょうか。

理由はc++のアーキテクチャにあります。

あるクラスのメソッド、あるインスタンスのメソッド呼び出しとは、内部的には通常の関数にインスタンスのポインタを渡しているだけなのです。暗黙の引数としてthisが渡されるイメージです。

ですので、先ほど提示したコードは、C言語的な擬似コードで書くと

というコードに近いイメージになります。

これならアクセスエラーが発生する場所が

だという事がよりイメージしやすいのではないかと思います。

C++でコーディングしていると、ポインタを操作する前にnullチェックをするコードを書くケースが多いのですが、上記を踏まえ、こんなことを考えるかもしれません。

 

そうだ!メソッドの中でチェックしよう!

 

 

と書くかわりに

こんな感じでメソッドの中でチェックすれば、呼び出し側が、色々な場所で呼び出すかわりにnullチェックしなくて済む。と思うかもしれません。

 

 

しかし、このコードは再びアクセスエラーになる可能性があります。

なぜか?

一見、ちゃんとnullチェックもしているように見えます。

 

C++の規格ではメソッド中でthisがnullではないことが保証されているため優秀なコンパイラは単にメソッド内でのthisへのnull判定コードを最適化で消し去ります。

これが今回のアーティクルのタイトルにもなっているメソッド内でのthisの最適化です。

コンパイラオプションによってこの最適化を抑制することもできるのですが、規格に準じたコードではないので書くべきではありません。

 

コラム

行儀よく普通にコーディングしてるとメソッドの中でthisを判定したいことは少ないと思います。こんな最適化に関する知識、それほど重要じゃないと思われるかもしれません。

あるゲームエンジンを書いていた時のことです。

そのゲームエンジンはオブジェクトを生成すると同時に色々な準備を同時に行う必要があったのでファクトリによって生成されました。生成と破棄の詳細についてユーザは知る必要がないのでインスタンスを破棄する時、ユーザーは単に破棄用のメソッドを呼びます。

不特定多数のゲームプログラマが使うようなシステムを実装する場合、ゲームエンジンを書いているプログラマは「とにかく簡単に完結に」 という心理になります。

delete演算子にnullポインタを渡しても支障が無いので、この破棄処理もobjがnullかどうか破棄の前に判定させることを強要しない作りにしたほうがシンプルで良いと考えるのです。

そして……メソッドの中でthisによる判定を書いてしまうことが……あるのです……。

あなたがもし、ゲームエンジンの実装を任されているエンジニアで、なおかつdeleteがnullポインタを許容する事実を知っている場合は、この最適化の事を覚えておいて損はないでしょう。

そして、もしよりシンプルでブレの無いエンジンを目指すなら、そもそもオブジェクトの生成はスマートポインタで返すべきかもしれません。(ハードスペックと使っているコンパイラが許すなら)

 

 

まとめ

  1. メソッド中ではthisがnullじゃないことは規格で保証されている
  2. 故にメソッド中でのthisそのものに対する判定は最適化される場合がある
  3. コンパイラオプションで抑制できるとしてもメソッド内でthis判定するのは規格外なので良くない