2015-12-21

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

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

No comments: