配列の値初期化は鬼門なり
自作の構文解析器が MacPorts から導入可能になっていた.登録して下さったのは,以前分類器・学習器を登録して下さった方と同じ方で,いつもありがたい限り.そこで早速 MacOS 10.5 サーバに入れてみたのだけど,思いがけず,Apple gcc-4.0 のバグに遭遇したのでメモ.
結論から書くと,Xcode 3.1.4 付属の gcc-4.0 (Apple Inc. build 5493) は配列の値初期化を行わない.規格通りでない動作という意味でバグと考えられるが*1,このバグのため,Leopard など古い OSX 上の Xcode で導入される gcc-4.0 で構文解析器をコンパイルすると,構文解析器のモデルファイルが壊れてしまう*2.追記: 対応した版を更新済み.
// vinit.cc #include <cstdio> #include <cstdlib> #include <cassert> #include <algorithm> int main (int argc, char ** argv) { if (argc < 2) return 1; long N = std::strtol (argv[1], NULL, 10); while (--N > 0) { asm ("# new start"); int * p = new int[N] (); asm ("# new end"); assert (std::count (p, p + N, 0) == N); delete [] p; } return 0; }
こんなコードを書いて(間が抜けた感じだが,説明のために最小限まで単純化している),
> g++-4.0 -v Using built-in specs. Target: i686-apple-darwin9 Configured with: /var/tmp/gcc/gcc-5493~1/src/configure --disable-checking -enable-werror --prefix=/usr --mandir=/share/man --enable-languages=c,objc,c++,obj-c++ --program-transform-name=/^[cg][^.-]*$/s/$/-4.0/ --with-gxx-include-dir=/include/c++/4.0.0 --with-slibdir=/usr/lib --build=i686-apple-darwin9 --with-arch=apple --with-tune=generic --host=i686-apple-darwin9 --target=i686-apple-darwin9 Thread model: posix gcc version 4.0.1 (Apple Inc. build 5493) > g++-4.0 vinit.cc > ./a.out 1000 Assertion failed: (std::count (p, p + N, 0) == N), function main, file vinit.cc, line 14. zsh: abort ./a.out 1000 > g++-4.2 -v Using built-in specs. Target: i686-apple-darwin9 Configured with: /var/tmp/gcc_42/gcc_42-5577~1/src/configure --disable-checking --enable-werror --prefix=/usr --mandir=/usr/share/man --enable-languages=c,objc,c++,obj-c++ --program-transform-name=/^[cg][^.-]*$/s/$/-4.2/ --with-slibdir=/usr/lib --build=i686-apple-darwin9 --with-gxx-include-dir=/usr/include/c++/4.0.0 --host=i686-apple-darwin9 --target=i686-apple-darwin9 Thread model: posix gcc version 4.2.1 (Apple Inc. build 5577) > g++-4.2 vinit.cc > ./a.out 1000
実行してみると,確かに,gcc-4.0 では初期化されていない.初期化が行われていなくてもプログラム自体は普通に実行できてしまうという点で見つけにくいバグだが,こういうときは valgrind で確認するのが常套手段.
> g++-4.0 -g -m64 vinit.cc // -m64 for 64-bit compiled valgrind > valgrind --leak-check=full --track-origins=yes ./a.out 1000 ==92981== Memcheck, a memory error detector ==92981== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al. ==92981== Using Valgrind-3.9.0.SVN and LibVEX; rerun with -h for copyright info ==92981== Command: ./a.out 1000 ==92981== ==92981== Conditional jump or move depends on uninitialised value(s) ==92981== at 0x100000EC4: std::iterator_traits<int*>::difference_type std::count<int*, int>(int*, int*, int const&) (bits/stl_algo.h:422) ==92981== by 0x100000E3A: main (vinit.cc:14) ==92981== Uninitialised value was created by a heap allocation ==92981== at 0x10000FE0F: malloc (vg_replace_malloc.c:274) ==92981== by 0x1000687F5: operator new(unsigned long) (in /usr/lib/libstdc++.6.0.4.dylib) ==92981== by 0x1000688D0: operator new[](unsigned long) (in /usr/lib/libstdc++.6.0.4.dylib) ==92981== by 0x100000E13: main (vinit.cc:12) ==92981== ==92981== ==92981== HEAP SUMMARY: ==92981== in use at exit: 648 bytes in 8 blocks ==92981== total heap usage: 1,007 allocs, 999 frees, 1,998,648 bytes allocated ==92981== ==92981== LEAK SUMMARY: ==92981== definitely lost: 0 bytes in 0 blocks ==92981== indirectly lost: 0 bytes in 0 blocks ==92981== possibly lost: 0 bytes in 0 blocks ==92981== still reachable: 0 bytes in 0 blocks ==92981== suppressed: 648 bytes in 8 blocks ==92981== ==92981== For counts of detected and suppressed errors, rerun with: -v ==92981== ERROR SUMMARY: 499500 errors from 1 contexts (suppressed: 0 from 0)
-track-origins=yes
をつけておくと,どこで new したメモリか分かるので便利.こんな感じで,gdb より前に valgrind でデバッグすることも最近は多い.
さらに,アセンブリを比べてみる.asm
で該当箇所をコメントで囲って g++ でアセンブリを出力.
> g++-4.0 -fverbose-asm -g -S vinit.cc > less vinit.s (中略) # new start LM6: movl -16(%ebp), %eax # N, N.43 sall $2, %eax #, D.10921 movl %eax, (%esp) # D.10921, call L__Znam$stub # movl %eax, -12(%ebp) # D.10893, p LM7: # new end > g++-4.2 -fverbose-asm -g -m64 -S vinit.cc > less vinit.s (中略) # new start LM6: movl -16(%ebp), %esi # N, N.43 leal 0(,%esi,4), %eax #, D.11321 movl %eax, (%esp) # D.11321, call __Znam # movl %eax, -36(%ebp) # D.11322, D.11294 movl -36(%ebp), %eax # D.11294, D.11295 movl %eax, -32(%ebp) # D.11295, D.11296 leal -1(%esi), %eax #, D.11324 movl %eax, -28(%ebp) # D.11324, D.11297 jmp L14 # L15: movl -32(%ebp), %eax # D.11296, movl $0, (%eax) #, addl $4, -32(%ebp) #, D.11296 decl -28(%ebp) # D.11297 L14: cmpl $-1, -28(%ebp) #, D.11297 jne L15 #, movl -36(%ebp), %eax # D.11294, movl %eax, -12(%ebp) #, p LM7: # new end
-fverbose-asm
を使えばソースコードを注釈付けできる(この例ではたいして役には立っていなが).というわけで,アセンブリまで落とすと処理の違いが一目瞭然.g++-4.2 の出力するアセンブリでは L15 で値初期化(この場合0初期化)されている.
さらに,gcc のソースコードを読んで該当箇所特定・・・といきたかったが,対応する版のソースコードがどこにあるか良く分からなかった.gcc/cp/init.c であるのは間違いないので,ChangeLog を読むという手もあるが.
値初期化については,古いコンパイラで規格通りの動作をしないコンパイラがあるという話は聞いていたが,gcc 4.0 でもダメだったとは.値初期化は前もバグに遭遇したことがあるし,鬼門だな.自前で 0 クリアしたほうが良いのかな.
次の更新の時は明示的に値初期化するようにしよう.それまでの回避策としては,gcc-4.2 でコンパイルするように Portfile を編集すれば ok かな(自分がさっさと更新すれば良いのだけど).
[追記] とりいそぎ,全て更新しておいた.これで大丈夫なはず.構文解析器はなんちゃって確率値の出力にも対応(実装済みの sigmoid fitting*3 のコードを追加するまでは確率値とは呼べない).
肝心のバグの方は,別環境だが gcc 4.1.2 では値初期化されていて,gcc 3.4.6 ではダメだった.もう少し深追いしてみたら,Apple が fork した gcc 4.0.1 までダメで,gcc 4.0.2 (September 28, 2005 Release) 以降なら ok ということが分かった.たぶん
- Diff of /branches/gcc-4_0-branch/gcc/cp/init.c (r102865 to r103533)
- Bug 23491 - [4.0/4.1 Regression] new declarator with constant expression gives "error: invalid use of array with unspecified bounds"
これかな.
ちなみに,clang は 2.9 (Apr 6, 2011 Release) 以降では大丈夫のようだ.
ただし,clang 3.0 以前では,template class の中で値初期化しようとすると,コンパイラに怒られる.
// vinit_.cc #include <cstdio> #include <cstdlib> #include <cassert> #include <algorithm> template <typename T> class A { public: int * alloc (long N) { return new int[N] (); } }; // template class A <int>; int main (int argc, char ** argv) { if (argc < 2) return 1; long N = std::strtol (argv[1], NULL, 10); A <int> a; while (--N > 0) { asm ("# new start"); // int * p = new int[N] (); int * p = a.alloc (N); asm ("# new end"); assert (std::count (p, p + N, 0) == N); delete [] p; } return 0; }
> clang++ -v clang version 3.0 (tags/RELEASE_30/final) Target: x86_64-apple-darwin9.8.0 Thread model: posix > clang++ vinit_.cc > clang++ vinit.cc vinit.cc:10:33: error: array 'new' cannot have initialization arguments int * alloc (long N) { return new int[N] (); } ^ vinit.cc:20:17: note: in instantiation of member function 'A<int>::alloc' requested here int * p = a.alloc (N); > clang++ -v clang version 3.1 (branches/release_31) Target: x86_64-apple-darwin9.8.0 Thread model: posix > clang++ vinit.cc > ./a.out 1000
上で書いた gcc のバグと同じ挙動.lib/Sema/SemaExprCXX.cpp で,3.1 では Array 'new' can't have any initializers except empty parentheses. となっているが 3.0 では Array 'new' can't have any initializers. と例外を認めていない.以下の r150682 (16 Feb 2012) で修正されたようだ.
[llvm-project] Diff of /cfe/trunk/lib/Sema/SemaExprCXX.cpp