配列の値初期化は鬼門なり

自作の構文解析器が 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 ということが分かった.たぶん

これかな.
ちなみに,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

*1:少なくとも ISO/IEC 14882:2003 (C++03) には配列の値初期化に関する記述がある.

*2:実際には分類器の実装に関係するところなので,分類器も影響を受けるはずなのだけど,分類器に付属するテストセットでは問題がなかったので気が付かなかった.valgrind では怒られるので同様の問題が発生する可能性がある.

*3:A note on Platt's probabilistic outputs for support vector machines, Mach Learn 2007.