64bit環境で通るテストが32bit環境で通らなかったことのメモ

大した話ではないのだがメモ。

Jubatusのビルドが64bit環境(x86_64)では通るのだが、32bit環境(i686)では通らなかったので、いろいろと調べていた。二つの環境は以下のとおりで、ビルドにはg++が用いられていた(どちらもvagrantUbuntu 12.04を用いている)。

$ uname -a
Linux vagrant-ubuntu 3.2.0-32-generic #51-Ubuntu SMP Wed Sep 26 21:33:09 UTC 2012 x86_64 x86_64 x86_64 GNU/Linux

$ g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/4.6/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu/Linaro 4.6.3-1ubuntu5' --with-bugurl=file:///usr/share/doc/gcc-4.6/README.Bugs --enable-languages=c,c++,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.6 --enable-shared --enable-linker-build-id --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.6 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --enable-plugin --enable-objc-gc --disable-werror --with-arch-32=i686 --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)
$ uname -a
Linux precise32 3.2.0-23-generic-pae #36-Ubuntu SMP Tue Apr 10 22:19:09 UTC 2012 i686 athlon i386 GNU/Linux

$ g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/gcc/i686-linux-gnu/4.6/lto-wrapper
Target: i686-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu/Linaro 4.6.3-1ubuntu5' --with-bugurl=file:///usr/share/doc/gcc-4.6/README.Bugs --enable-languages=c,c++,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.6 --enable-shared --enable-linker-build-id --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.6 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --enable-plugin --enable-objc-gc --enable-targets=all --disable-werror --with-arch-32=i686 --with-tune=generic --enable-checking=release --build=i686-linux-gnu --host=i686-linux-gnu --target=i686-linux-gnu
Thread model: posix
gcc version 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)

もともとサポートされているのは64bitのUbuntuRHELなので、動かしたいなら自分で何とかするしかない。どうやらテストでこけているようで、テスト結果やコンパイラの警告を見ると、原因の一つはsize_tとuint64_tを同じものとみなして用いていたためだった(g++で-Wallオプションをつけても、関数の32bit長の引数に64bit長の値を渡しても警告がでないことを始めて知った)。もう一つは、どうやら計算結果の値自体が二つの環境で異なることが原因のようだった。

32bit環境でテストが失敗しているのは、以下の場所。
https://github.com/jubatus/jubatus/blob/c97cd385ab1a3a4b7625d74eaddcf684d87eee11/src/stat/mixable_stat_test.cpp#L41

[----------] 1 test from mixable_stat_test
[ RUN      ] mixable_stat_test.mixed_entropy
../src/stat/mixable_stat_test.cpp:41: Failure
Value of: p.mixed_entropy()
  Actual: -1.6588293239028218e-17
Expected: e_e
Which is: 0
[  FAILED  ] mixable_stat_test.mixed_entropy (1 ms)
[----------] 1 test from mixable_stat_test (1 ms total)

doubleの値の比較で、その誤差もDBL_EPSILON以下なのだが、問題は先に述べたように64bit環境ではテストが通ることである。なお、gtestのASSERT_DOUBLE_EQは4ULP以内であることを求められるらしい(なのでテストを通すだけなら、ASSERT_NEARを使って許容できる誤差を指定すれば良いが、なんだか気持ち悪い)。

ということで、該当場所だけ切り出して見てみることにした。用いたコードは以下。

#include <cstdio>
#include <cmath>

double mixed_entropy(double e, double n) {
  if (n == 0.0) {
    return 0.0;
  }
  return log(n) - e / n;
}

int main(int argc, char** argv) {
  double n = 3;
  double e_d = 3 * log(3);
  double e_e = - e_d / 3 + log(3);

  double d = 0.0;

  d = e_e;
  fprintf(stdout, "d = %g\n", d);

  d = (n == 0.0) ? 0.0 : (log(n) - e_d / n);
  fprintf(stdout, "d = %g\n", d);

  d = mixed_entropy(e_d, n);
  fprintf(stdout, "d = %g\n", d);

  return 0;
}

https://gist.github.com/y-tag/5127284

やっていることは非常に単純で、 log(3) - (3 * log(3)) / 3 = 0となる計算を三通り行っている。一つ目と三つ目がJubatusのテストに相当するもので、二つ目は比較のために追加しておいた。これを64bit/32bitで、最適化なし/あり(-O2)の4通りを試してみた。

64bit最適化なし

$ g++ -o mixed_entropy mixed_entropy.cc -Wall
$ file mixed_entropy
mixed_entropy: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0xbe62c1afe97c19748e2b4833c9dd364dfc374190, not stripped
$ ./mixed_entropy 
d = 0
d = 0
d = 0

64bit最適化あり(-O2)

$ g++ -o mixed_entropy mixed_entropy.cc -Wall -O2
$ file mixed_entropy
mixed_entropy: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x8366a118eeb230a9f47333860b4b8420154eb07e, not stripped
$ ./mixed_entropy 
d = 0
d = 0
d = 0

32bit最適化なし

$ g++ -o mixed_entropy mixed_entropy.cc -Wall -m32
$ file mixed_entropy
mixed_entropy: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x084dd508b45190267b1784c8f2852092a085e29e, not stripped
$ ./mixed_entropy 
d = 7.4051e-17
d = -1.65883e-17
d = -1.65883e-17

32bit最適化あり(-O2)

$ g++ -o mixed_entropy mixed_entropy.cc -Wall -m32 -O2
$ file mixed_entropy
mixed_entropy: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0xde2ff7db9369c204b6a658224a5123e9ef17e72a, not stripped
$ ./mixed_entropy 
d = 0
d = 0
d = -1.65883e-17

g++でコンパイルすると、32bitの場合は結果が0とならない場合があった。これによって先に述べた32bit環境でのテストが失敗しているようだ。

比較としてclang(llvm)で同じようにやってみた。

$ clang++ -v
Ubuntu clang version 3.0-6ubuntu3 (tags/RELEASE_30/final) (based on LLVM 3.0)
Target: x86_64-pc-linux-gnu
Thread model: posix

64bit最適化なし

$ clang++ -o mixed_entropy mixed_entropy.cc -Wall
$ file mixed_entropy
mixed_entropy: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x30bf4081a60fe446a1aef61cd32c5891076f4fa1, not stripped
$ ./mixed_entropy 
d = 0
d = 0
d = 0

64bit最適化あり(-O2)

$ clang++ -o mixed_entropy mixed_entropy.cc -Wall -O2
$ file mixed_entropy
mixed_entropy: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x06d00baf787e1dfd4eb41b50d0475773fe527ef7, not stripped
$ ./mixed_entropy 
d = 0
d = 0
d = 0

32bit最適化なし

$ clang++ -o mixed_entropy mixed_entropy.cc -Wall -m32
$ file mixed_entropy
mixed_entropy: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0xc09935d66e58df9a96e28a5db6768d0780d15ab0, not stripped
$ ./mixed_entropy
d = 0
d = 0
d = 0

32bit最適化あり(-O2)

$ clang++ -o mixed_entropy mixed_entropy.cc -Wall -m32 -O2
$ file mixed_entropy
mixed_entropy: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x326d06160c7bbc3b28c1bca9c0296afcf8a728db, not stripped
$ ./mixed_entropy 
d = 0
d = 0
d = 0

clangでコンパイルした場合は、すべての結果が正しく0となった。

これだと「やってみた」というところに留まってしまうので、出力されたアセンブラを見てみたのだが、残念ながらアセンブラを理解することができなかった。そんなわけでここまで。

2013/3/11 追記

[twitter:@tkng]さんにご指摘いただいたように、x86浮動小数点計算の精度が80bitで行われていることに関係しているようだ。g++で-ffloat-storeオプションをつけると期待通りの結果になった。

32bit最適化なし

$ g++ -o mixed_entropy mixed_entropy.cc -Wall -m32 -ffloat-store
$ file mixed_entropy
mixed_entropy: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x79827eadda9ce03809f6cb9d5946756d93f638b2, not stripped
$ ./mixed_entropy 
d = 0
d = 0
d = 0

32bit最適化あり(-O2)

$ g++ -o mixed_entropy mixed_entropy.cc -Wall -m32 -O2 -ffloat-store
$ file mixed_entropymixed_entropy: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x112c34eebd1447b77ff6af302601c9ec91ee4226, not stripped
$ ./mixed_entropy
d = 0
d = 0
d = 0