アライメントの勉強をしていて、気になったので実験してみた。
この記事の内容は、下の記事からほとんど抜粋してまとめたもの。めちゃくちゃいい記事だった。
でも、写しているので(僕が)カッコ悪い。
データ型のアラインメントとは何か,なぜ必要なのか?
まず、アライメントとはCPUのメモリにおける制約である。
アライメントに従わないデータにCPUがアクセスする際には、幾分かのオーバーヘッドが発生する。
アライメントを跨ぐデータを読む際には二度メモリにアクセスせねばならんし、読む際なんかは四度もアクセスしないといけない。(かもしれない。)
アライメントを揃えなかったことによって、キャッシュラインをまたいだりしたら大変。
キャッシュ全体(通常、32~256byteらしい)をキャッシュメモリに読み込むという大きなオーバーヘッドが発生する。
また、アライメントに寛容なCPUとそうでないCPUが存在する。
x86を筆頭とするCISCではアライメントに揃えられていないデータにアクセスすることは一応可能。(ただし、場合によってはだめみたい。)
armを筆頭とするRISCでは、そもそもアライメント違反すると処理が止まってしまったりするみたい。もしくは予期せぬ動作を引き起こす可能性もある。
RISCは回路を単純化することで命令を高速化し、回路を単純化することで得られた領域をキャッシュに割り振ることを信条としているので、当然アライメントに沿っていないデータにアクセスするような複雑な回路はつくらない。
h8系のCPUでは、アライメントに揃っていないデータにアクセスする際に下位のアドレスビットを無視していたこともあるらしい。
C言語にはアライメントに関する制約はないが、C言語を動作させるCPUには存在するので、C言語も仕方なくこれの影響を受けることになる。
アライメントは制約であって機能ではない。C言語がわざわざ制約を言語規格に取り入れる意味はない。
・基本的にプリミティブ型の変数は、そのデータ型のサイズのアライメントを持つ。(ただし、このサイズがデータバスより大きい場合はデータバスのサイズになることが多い。例えば、僕の32bit環境の8byteであるdoubleのアライメントは4byteだった。)
・配列や構造体といった集成体型のアライメントは、そのデータ型が持つメンバの中で最も大きなアライメントを持つものと等しくなる。
・集成体型の各メンバのアライメントは、そのメンバ自身のアライメントに等しくなる。(要素が構造体の場合は、構造体の中の一番大きなアライメントを持つメンバのアライメントに等しくなる。)
・sizeof(配列) / sizeof(配列を構成している要素)としたときに、配列の要素数を得るために構造体の最後にパティングが挿入される場合がある。
構造体のパディングをなくしたいがために、コンパイラに妙なオプションを付ける人がいるらしい。
これは、速度低下や予期せぬ動作などを引き起こすのでやめたほうがよい。
そもそも構造体のメンバは正しく並べれば、メンバの間のパディングはすべて無くすことができる。(アライメントの大きいデータ型から並べれば良い。ただし、構造体の最後にあるパディングはsizeofの為に削除できない。)
これは予備知識だが、mallocなどのメモリ管理関数から返されるポインタの値はその環境でもっとも大きなアライメントをもつプリミティブ型のアライメントに揃えられている。
大抵の場合は8のようだ。(doubleなど)
ここで、上記サイトに乗っていた練習問題を解いてみたい。
次の構造体・共用体のサイズ(正味サイズではなく、パディングを含むもの、つまりsizeof演算子が返す値を答えよ)とアライメントを答えなさい。
struct S1 { short s; unsigned char uc[3]; }; struct S2 { char string[10]; unsigned char uc; }; struct S3 { double d; long long ll; char c; }; // sizeof(共用体) で説明した例 typedef union U { char string[17]; double d[2]; } U_t;
答えは次のようになる。
構造体のアライメントが4の倍数だと覚えていた人も多いかもしれないが、実際にはそのメンバに依存する。
S1のサイズは6でアライメントは2。(構造体の正味サイズは2+1*3=5だが、構造体全体のアライメントが2(short)なので、サイズは2の倍数に切り上げられて6になる。) S2のサイズは11でアライメントは1。(構造体の正味サイズは1*10+1=11。構造体全体のアライメントも1(char, unsigned char)なのでOK。) S3のサイズは24でアライメントは8。(構造体のサイズは8+8+1=17だが、構造体全体のアライメントが8(double, long long)なので、サイズは8の倍数に切り上げられて24になる。) Uのサイズは24でアライメントは8。(構造体のサイズは1*17だが、構造体全体のアライメントが8(double)なので、サイズは8の倍数に切り上げられて24になる。)
アライメントについては、他にもこのような誤解があるみたいだ。
・構造体のサイズは、メンバの中で最大のサイズのものの倍数だ。
→構造体のサイズは、メンバの中で最大のアライメントのものの倍数だ。
・構造体メンバのオフセットは、そのメンバのサイズの倍数だ。
→構造体メンバのオフセットは、そのメンバのアライメントの倍数だ。
ここで、自分の環境のアライメントについて調査するプログラムを書いてみた。
offsetofマクロは、第一引数にとった構造体における第二引数のメンバのオフセットを調べます。(ただし、戻り値はポインタなので注意。)
alignmentofマクロは、offsetofマクロを内部で使用して、第一引数にとった構造体のサイズを調べます。(これも、offsetofを使っているので戻り値はポインタ。)
#include<stdio.h> #define offsetof(type, member) (char*)&((type*)0)->member #define alignmentof(type) offsetof(struct { char a; type b; }, b) typedef struct { char c; short s; int i; long l; long long ll; float f; double d; } test; int main(void){ test t = {1, 2, 3, 4, 5, 6, 7}; printf("******** SHOW PRIMITIVE DATA SIZE ********\n"); printf("char : %ld\n", sizeof(char)); printf("short : %ld\n", sizeof(short)); printf("int : %ld\n", sizeof(int)); printf("long : %ld\n", sizeof(long)); printf("long long : %ld\n", sizeof(long long)); printf("float : %ld\n", sizeof(float)); printf("double : %ld\n", sizeof(double)); printf("offsetof : c = %p, s = %p, i = %p, l = %p, ll = %p, f = %p, d = %p\n", \ offsetof(test, c), offsetof(test, s), offsetof(test, i), offsetof(test, l), offsetof(test, ll), offsetof(test, f), offsetof(test, d)); printf("alignmentof(test) = %p\n", alignmentof(test)); return 0; }
ソースコードはtest.cと名付けて、これを次のようにビルド・実行した。
警告が出ているが、大抵の場合はintからlongへ正常にキャストが行われるので大丈夫だと思う。
pogin様より、警告を抑制する指定子を教えていただきました。
size_tの場合は指定子にzdを使用すると、32ビットと64ビットの両方で正常にビルドできるみたいです。
shiguren@ubuntu:~/デスクトップ/Test$ gcc -m32 test.c -o test_32 test.c: In function ‘main’: test.c:20:27: warning: format ‘%ld’ expects argument of type ‘long int’, but argument 2 has type ‘unsigned int’ [-Wformat=] printf("char : %ld\n", sizeof(char)); ~~^ %d test.c:21:27: warning: format ‘%ld’ expects argument of type ‘long int’, but argument 2 has type ‘unsigned int’ [-Wformat=] printf("short : %ld\n", sizeof(short)); ~~^ %d test.c:22:27: warning: format ‘%ld’ expects argument of type ‘long int’, but argument 2 has type ‘unsigned int’ [-Wformat=] printf("int : %ld\n", sizeof(int)); ~~^ %d test.c:23:27: warning: format ‘%ld’ expects argument of type ‘long int’, but argument 2 has type ‘unsigned int’ [-Wformat=] printf("long : %ld\n", sizeof(long)); ~~^ %d test.c:24:27: warning: format ‘%ld’ expects argument of type ‘long int’, but argument 2 has type ‘unsigned int’ [-Wformat=] printf("long long : %ld\n", sizeof(long long)); ~~^ %d test.c:25:27: warning: format ‘%ld’ expects argument of type ‘long int’, but argument 2 has type ‘unsigned int’ [-Wformat=] printf("float : %ld\n", sizeof(float)); ~~^ %d test.c:26:27: warning: format ‘%ld’ expects argument of type ‘long int’, but argument 2 has type ‘unsigned int’ [-Wformat=] printf("double : %ld\n", sizeof(double)); ~~^ %d shiguren@ubuntu:~/デスクトップ/Test$ gcc test.c -o test_64 shiguren@ubuntu:~/デスクトップ/Test$ ./test_32 ******** SHOW PRIMITIVE DATA SIZE ******** char : 1 short : 2 int : 4 long : 4 long long : 8 float : 4 double : 8 offsetof : c = (nil), s = 0x2, i = 0x4, l = 0x8, ll = 0xc, f = 0x14, d = 0x18 alignmentof(test) = 0x4 shiguren@ubuntu:~/デスクトップ/Test$ ./test_64 ******** SHOW PRIMITIVE DATA SIZE ******** char : 1 short : 2 int : 4 long : 8 long long : 8 float : 4 double : 8 offsetof : c = (nil), s = 0x2, i = 0x4, l = 0x8, ll = 0x10, f = 0x18, d = 0x20 alignmentof(test) = 0x8
アライメントの知識は奥が深く面白かった。
自作OSのメモリ管理アルゴリズムに活かしたいなぁ。