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;
}

Responses are currently closed.