gccでのi386とamd64におけるワードアライメントの動作の違い
Linux (Debian lenny)で、メモリダンプとかsocket経由の通信とかを行うラッパライブラリを試験していて、データを突っ込む構造体の中のワードアライメントが(amd64で)思ったのと違っていることに気づいて、少し試験してみた。昔のi386の時代は、4byteごとにアライメント境界が来るからそれにあわせて、という話があったけれど、そうではない様子。
環境はDebian lennyのパッケージそのままで、次のような感じ。
@i386 (32bit) % gcc -v Using built-in specs. Target: i486-linux-gnu Configured with: ../src/configure -v --with-pkgversion='Debian 4.3.2-1.1' --with-bugurl=file:///usr/share/doc/gcc-4.3/README.Bugs --enable-languages=c,c++,fortran,objc,obj-c++ --prefix=/usr --enable-shared --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --enable-nls --with-gxx-include-dir=/usr/include/c++/4.3 --program-suffix=-4.3 --enable-clocale=gnu --enable-libstdcxx-debug --enable-objc-gc --enable-mpfr --enable-targets=all --enable-cld --enable-checking=release --build=i486-linux-gnu --host=i486-linux-gnu --target=i486-linux-gnu Thread model: posix gcc version 4.3.2 (Debian 4.3.2-1.1) % uname -a Linux lenny-i386 2.6.26-1-686 #1 SMP Fri Mar 13 18:08:45 UTC 2009 i686 GNU/Linux @amd64 (64bit) % gcc -v Using built-in specs. Target: x86_64-linux-gnu Configured with: ../src/configure -v --with-pkgversion='Debian 4.3.2-1.1' --with-bugurl=file:///usr/share/doc/gcc-4.3/README.Bugs --enable-languages=c,c++,fortran,objc,obj-c++ --prefix=/usr --enable-shared --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --enable-nls --with-gxx-include-dir=/usr/include/c++/4.3 --program-suffix=-4.3 --enable-clocale=gnu --enable-libstdcxx-debug --enable-objc-gc --enable-mpfr --enable-cld --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu Thread model: posix gcc version 4.3.2 (Debian 4.3.2-1.1) % uname -a Linux lenny-amd64 2.6.26-2-amd64 #1 SMP Tue Mar 9 22:29:32 UTC 2010 x86_64 GNU/Linux
この環境で試験できるのは、64bitバイナリ@amd64、32bitバイナリ@amd64、32bitバイナリ@i386の3つ。ひとまず、さまざまな型でのメモリ上のサイズを見る。整数型では”long”だけは64bitの実装系によって内部表現が違うとかがあるのでgccではどうか、と。
type | 64bit@amd64 | 32bit@amd64 | 32bit@i386 |
---|---|---|---|
char | 1 | 1 | 1 |
short | 2 | 2 | 2 |
int | 4 | 4 | 4 |
long | 8 | 4 | 4 |
long long | 8 | 8 | 8 |
float | 4 | 4 | 4 |
double | 8 | 8 | 8 |
long double | 16 | 16 | 12 |
ということで、”long”と”long double”のみ32/64bitでサイズが異なる。当然ながら32bitバイナリをそれぞれi386とamd64のkernelで動くLinuxに持っていっても変化はない。
次に、ワード境界がどうなるかを調べる。利用する構造体は、サイズの判定ができるように適宜charを突っ込んだ並び。それぞれはさまれているcharはサイズ1byteだがワードアライメントの影響を受けて、構造体の中の(パッディングを含む)実(?)メモリサイズは長くなるはず。
- char a
- short b
- int c
- char d
- long e
- char f
- long long g
- char h
- float i
- char j
- double k
- char l
- long double m
というところで、32bitバイナリと64bitバイナリでの結果は次のようになった。
name | type | 32bit | 64bit |
---|---|---|---|
all | 64 | 96 | |
b-a | char | 2 | 2 |
c-b | short | 2 | 2 |
d-c | int | 4 | 4 |
e-d | char | 4 | 8 |
f-e | long | 4 | 8 |
g-f | char | 4 | 8 |
h-g | long long | 8 | 8 |
i-h | char | 4 | 4 |
j-i | float | 4 | 4 |
k-j | char | 4 | 8 |
l-k | double | 8 | 8 |
m-l | char | 4 | 16 |
これを見ると、32bitでは4byteごとにアライメントされているのは変わっていない様子。しかしながら、64bitではその直後に来るデータの実サイズを基準にアライメントされているようにみえる。たとえば、16bitのlong doubleの開始点は16bitの整数倍のメモリから、8bitのlongの開始点は8bitの整数倍のメモリから、という感じ。(もし全てが8/16byteの固定アライメント似合うようにパディングされているなら、float/j-iの部分の数値がおかしい)
これだけなら64bit側にあわせたアライメントになるようにstructの定義部分でいじってやればいいような気はするが、ターゲットはLinuxだけではないのでさまざまな処理系での違いを吸収するような定義は面倒に思える。ということで、ライブラリの中でこの部分を吸収するコードを入れる(パフォーマンスを考えると、自動ネゴシエーションの後に違っていれば吸収するコードにフォールバックする、あたり)ことにする。
試験に利用したコードは以下のような感じ。
#include#include typedef struct { char a; short b; int c; char d; long e; char f; long long g; char h; float i; char j; double k; char l; long double m; } var; int main() { var *obj = new var; // sizeof types std::cout << "sizeof(char) : " << sizeof(char) << std::endl << "sizeof(short) : " << sizeof(short) << std::endl << "sizeof(int) : " << sizeof(int) << std::endl << "sizeof(long) : " << sizeof(long) << std::endl << "sizeof(long long) : " << sizeof(long long) << std::endl << "sizeof(float) : " << sizeof(float) << std::endl << "sizeof(double) : " << sizeof(double) << std::endl << "sizeof(long double) : " << sizeof(long double) << std::endl << std::endl; // struct memory padding std::cout << "all : " << sizeof(var) << std::endl << "b-a (char) : " << (size_t)((char *)(&(obj->b)) - (char *)(&(obj->a))) << std::endl << "c-b (short) : " << (size_t)((char *)(&(obj->c)) - (char *)(&(obj->b))) << std::endl << "d-c (int) : " << (size_t)((char *)(&(obj->d)) - (char *)(&(obj->c))) << std::endl << "e-d (char) : " << (size_t)((char *)(&(obj->e)) - (char *)(&(obj->d))) << std::endl << "f-e (long) : " << (size_t)((char *)(&(obj->f)) - (char *)(&(obj->e))) << std::endl << "g-f (char) : " << (size_t)((char *)(&(obj->g)) - (char *)(&(obj->f))) << std::endl << "h-g (long long) : " << (size_t)((char *)(&(obj->h)) - (char *)(&(obj->g))) << std::endl << "i-h (char) : " << (size_t)((char *)(&(obj->i)) - (char *)(&(obj->h))) << std::endl << "j-i (float) : " << (size_t)((char *)(&(obj->j)) - (char *)(&(obj->i))) << std::endl << "k-j (char) : " << (size_t)((char *)(&(obj->k)) - (char *)(&(obj->j))) << std::endl << "l-k (double) : " << (size_t)((char *)(&(obj->l)) - (char *)(&(obj->k))) << std::endl << "m-l (char) : " << (size_t)((char *)(&(obj->m)) - (char *)(&(obj->l))) << std::endl << std::endl; delete obj; }