EigenのOne Definition Rule違反

Eigenライブラリの利用法に関するバグで、丸二日間くらいはまった。 正しい原因にたどり着くのが難しいため、備忘録として残しておく。

Eigenは数値演算の高速かつ使いやすいライブラリで、かつtemplateを使ったヘッダファイルだけで構成されているため、あらゆる環境でビルドしやすく重宝されている。 最適化演算ライブラリのCeres, OpenCVなどでも使われている。

ところが、その有用性が徒となるケースがあった。

Eigenのようなヘッダファイルを使ったアプリケーションが実行時にクラッシュなどで落ちて原因が分からない場合は、今回のケースを疑ってみて欲しい。

エラー内容

今回遭遇したバグは、コンパイル・リンクは問題無いが、Ceres solverの最適化実行時にアプリがクラッシュ。 障害モジュールの名前:StackHash_**** 例外コード:c0000374 と出る(Windows) さらに、Linuxでビルドしたものも同じようにCeres solver実行時に落ちた。 最終的に解決したのだが、これは予想外の原因で落ちていたことが分かった。

原因

まず、C++にはOne Definition Ruleというものがあり、関数でも何でも同じ名前では定義(実装)は一つしか取れない。

One Definition Rule - Wikipedia

Eigenはヘッダファイルのみで構成されるおり、Eigenを使っているプログラム間で同じ定義が出来てしまう可能性がある。例えばEigenを使っているCeres solverのライブラリを使い、さらに自分で作った本体でもEigenを使っていると、コンパイルはライブラリと本体で独立に行っているため、同じ関数に二つの定義が出来てしまうことになる。 リンカエラーになるケースは良いが、今回はそうならなかったケースだ。

どういう状況で問題があるかというと、

  1. バージョンの異なるライブラリ(今回はEigen)を複数インクルードしていて、そのバージョン間でコードが異なっている時

  2. バージョンは同じライブラリ(Eigen)を複数インクルードしていて、インクルードしたそれぞれの呼び出し間で、コンパイルに関わる環境が変わっている時

今回自分のケースは2だ。Eigenを使っているCeres solverライブラリをビルドし、Ceres solverを使っている本体側でもEigenを使っていた。

独立にビルド済みのライブラリを使っている場合は、リンク時に一つのライブラリインスタンスしかリンクしないので通常問題にならない。

同じバージョンのEigenを使えば問題無いかと思っていたら、本体側のコードでAVX命令を使っているため、本体側のみがコンパイラオプションとしてAVX有効にしていたために起こった。

実はEigenのコードは、AVXやSSEといったSIMDコンパイルオプションが設定されていると、高速な実装のためにSIMD命令を使うのだが、その際メモリアロケーションを自前で書いたものを使うようになっている。 それが、Core下のutil\Memory.hに定義されているもので、handmade_aligned_mallocという関数をインラインで定義している。 SIMDを使ったことが無い人にはチンプンカンプンかもしれないが、これはSIMD命令を使うときに良く使うテクニックで、SIMD命令に必要なメモリアラインメントを揃えるために大きめにmalloc()しておいて、アラインメントされたポインタを返すというものだ。解放する時は演算に使ったポインタではなく、ちゃんと元のmalloc()したポインタを使ってfree()しないといけない。

/* ----- Hand made implementations of aligned malloc/free and realloc ----- */

/** \internal Like malloc, but the returned pointer is guaranteed to be 16-byte aligned.
  * Fast, but wastes 16 additional bytes of memory. Does not throw any exception.
  */
inline void* handmade_aligned_malloc(std::size_t size)
{
  void *original = std::malloc(size+EIGEN_DEFAULT_ALIGN_BYTES);
  if (original == 0) return 0;
  void *aligned = reinterpret_cast<void*>((reinterpret_cast<std::size_t>(original) & ~(std::size_t(EIGEN_DEFAULT_ALIGN_BYTES-1))) + EIGEN_DEFAULT_ALIGN_BYTES);
  *(reinterpret_cast<void**>(aligned) - 1) = original;
  return aligned;
}

/** \internal Frees memory allocated with handmade_aligned_malloc */
inline void handmade_aligned_free(void *ptr)
{
  if (ptr) std::free(*(reinterpret_cast<void**>(ptr) - 1));
}

解決方法

この二つの実装の齟齬によって今回のAppCrushは引き起こされた。 解決策は以下である。

Windowsの場合、ライブラリをDLLにすることで、Exportされた関数以外は外からは見えないのでconflictが起きない。 Linuxの場合や静的ライブラリを使いたい場合は、万事ハッピーな解決策はあまり無くて、Eigenをバージョンが全く同じで、同じコンパイル環境でビルドする。 もしくは、別のバージョンを使いたければ、それぞれのEigenに異なるnamespaceを付けることだ。

以下が参考になる。

http://eigen.tuxfamily.narkive.com/fweQWUaX/eigen-and-the-one-definition-rule