とても賢いコンパイラーの逆襲
The Hacks of Life: The Dangers of Super Smart Compilers
Clangの最適化が未定義の挙動を検出してコード片を消し去ってしまったことに引っかかった開発者の嘆き。
今日初めて、RenderFarmのDSF render(global scenaryを作成するのに使っている内部ツール)をClangで最適化コンパイルして実行した。
結果はsegfaultだった。これは驚きだ(そして自身消失だ)。というのも、最適化していないデバッグビルドは問題なく動くし、GCCでコンパイルされた最適化ビルドも正しく動く。-O0ではバグがない(つまり#if DEVコードのバグではない)ので、「最適化は何をやっているんだ」の時間だ。
大量のprintfと試行錯誤の結果、最適化は以下のようなコード片を丸ごとすっ飛ばしていることが判明した。
for(vector<mesh_mash_vertex_t>::iterator pts = ioBorder.vertices.begin(); pts != ioBorder.vertices.end(); ++pts) if(pts->buddy == NULL) { /* とても重要な処理 */ }
とても重要な処理はすっ飛ばされていて、実際、とても重要だった。
さて、なぜだ。buddyはポインターではない。スマートハンドルである。そこで、operator ==は単にポインターを比較しているのではない。コードをさらに深く探って見てみよう。ハンドルはポインターのラッパーであった。operator *は*m_ptrを返す。operator ==はnullとの比較が動くように特別に定義されている。
template < class DSC, bool Const > inline bool operator==(const CC_iterator<DSC, Const> &rhs, Nullptr_t CGAL_assertion_code(n)) { CGAL_assertion( n == NULL); return &*rhs == NULL; }
もちろん、Clangは筆者よりとても賢いので、このコードについて物申すことがある。
合法なC++のコードでは、リファレンスはnullポインターを束縛することはできない。比較は常にfalseと評価されると推定できる。
やれやれ、これが問題だ。このoperator ==は、他の多くのコードと同じく、&*を使ってラッパーから生のポインターを得ている。&と*はお互いに打ち消しあうので、生ポインターが得られる。
ただし、Clangはとても賢いので、「ふむ、もし&*rhs == NULLの場合、*rhsはどうなる? NULLリファレンスではないか(rhsがNULLでそれをデリファレンスした場合だ)。そして、NULLリファレンスは違法なので、これは起こりようがない。このコードは*rhsが評価された瞬間に未定義の挙動となる。このコードは未定義の挙動であるからして(*rhsがnullオブジェクトである状況が存在すればだが、そんな状況は存在しない)、コンパイラーは何でもできるぞ! もし、*rhsがnullオブジェクトではないのならば、&*rhsはNULLと同一になることはない。したがって結果はfalseだ。さて、一方がfalseでもう一方が未定義ならば、関数全体を以下のように書きかえられる」
template < class DSC, bool Const > inline bool operator==(const CC_iterator<DSC, Const> &rhs, Nullptr_t CGAL_assertion_code(n)) { return false; /* ほら、直してやったぜ */ }
そして、Clangはまさにこれをしている。 つまり、if(pts->buddy == NULL)がif(false)になったので、重要な処理は絶対に実行されない。短期的な修正は以下だ。
for(vector<mesh_mash_vertex_t>::iterator pts = ioBorder.vertices.begin(); pts != ioBorder.vertices.end(); ++pts) if(pts->buddy == CDT::Vertex_handle()) { /* do really important stuff */ }
これで、operator ==は2つのハンドルを比較するものが使われる。
template < class DSC, bool Const1, bool Const2 > inline bool operator!=(const CC_iterator<DSC, Const1> &rhs, const CC_iterator<DSC, Const2> &lhs) { return &*rhs != &*lhs; }
これも違法な未定義の挙動なのだが(&*をnullポインターに使うのは違法)、Clangは気が付かないようで、最適化はこのコードを消せない。このコードはポインター比較になった。我々の勝ちだ。
新しいバージョンのCGALはこの問題を修正していて、operator ->()が生ポインターを返すのでそちらをつかうようになっている。
Clangの援護をすると、プログラムの実行時間はseffaultを起こすまでは確かに早かった。
すべてのライブラリを最新版にアップデートしないことを笑うかもしれないが、3つか4つぐらいのコンパイラーやビルドシステムを使っている環境でライブラリをアップデートして、動かなかった場合の依存関係を全部解決するのは難しいので、とりあえず問題を解決した我々を糾弾しないでくれ。