本の虫

著者:江添亮
ブログ: http://cpplover.blogspot.jp/
メール: boostcpp@gmail.com
Twitter: https://twitter.com/EzoeRyou
GitHub: https://github.com/EzoeRyou

アマゾンの江添のほしい物リストを著者に送るとブログ記事のネタになる

筆者にブログのネタになる品物を直接送りたい場合、住所をメールで質問してください。

とても賢いコンパイラーの逆襲

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つぐらいのコンパイラーやビルドシステムを使っている環境でライブラリをアップデートして、動かなかった場合の依存関係を全部解決するのは難しいので、とりあえず問題を解決した我々を糾弾しないでくれ。