読者です 読者をやめる 読者になる 読者になる

gccで指定できるアラインメントのサイズには限界がある

C言語におけるデータのアラインメントについての小ネタです.

アラインメント自体については,
データ型のアラインメントとは何か,なぜ必要なのか?
にとても詳しいです.

問題設定

4バイトの変数を16個の配列で確保するとします.

   int a[16];

もし各レベルキャッシュのキャッシュラインサイズが64バイトなら,
この配列aを1ラインに収めることができるはずです.

事例1: 配列サイズを定数で指定

__atribute__ ((aligned(64))) を使って,アラインメントを64バイト境界に指定してみましょう.

alignment_stack_const.c

#include <stdio.h>
#include <stdint.h>

int
main()
{
  uintptr_t addr;
  int __attribute__ ((aligned(64))) a[16];
  addr = (uintptr_t)a;
  printf("a = %p\n", (void *)a);
  printf("%p %% 64 == %d\n", (void *)a, addr % 64);
  return 0;
}

(ポインタを整数に変換する部分は Cのポインタを整数に変換する - bkブログ を参照)

これをコンパイルして実行すると,aは確かに64バイト境界にアラインされていることが分かります.

$ gcc alignment_stack_const.c

$ ./a.out 
a = 0xbffd3c40
0xbffd3c40 % 64 == 0

$ ./a.out 
a = 0xbf80ef40
0xbf80ef40 % 64 == 0

$ ./a.out 
a = 0xbfa08640
0xbfa08640 % 64 == 0
...

事例2: 配列サイズを変数で指定

比較的新しいCの規約では,配列長を変数で指定することができます.
この機能を使って配列を確保してみます.

alignment_stack_var.c

#include <stdio.h>
#include <stdint.h>

int
main()
{
  uintptr_t addr;
  int n = 16;
  int __attribute__ ((aligned(64))) a[n];
  addr = (uintptr_t)a;
  printf("a = %p\n", (void *)a);
  printf("%p %% 64 == %d\n", (void *)a, addr % 64);
  return 0;
}

先程のソースとは,「16」という配列長を「n」という変数を介して指定している部分以外変わりませんが,
これをコンパイルして実行すると,64バイト境界にアラインメントがとれない場合があります.
*1
*2

$ gcc alignment_stack_var.c

$ ./a.out 
a = 0xbf8e5130
0xbf8e5130 % 64 == 48

$ ./a.out 
a = 0xbfb98d60
0xbfb98d60 % 64 == 32

$ ./a.out 
a = 0xbfd47d80
0xbfd47d80 % 64 == 0
...

何に気をつければよいか

実はアラインメントのサイズには,コンパイラ・リンカが定める最大値があります.

gcc の定義済みマクロ, "__BIGGEST_ALIGNMENT__" を確認することで,
お使いの環境での最大値が分かります.

$ cpp -dD alignment_stack_var.c |grep __BIGGEST_ALIGNMENT__
#define __BIGGEST_ALIGNMENT__ 16

自分の環境では __BIGGEST_ALIGNMENT__ の値が 16 になっていたため,
いつでも64バイトのアラインメントがとれることを期待してはいけないようでした.

一方で, alignment_stack_var.c をコンパイルして実行すると,最低でも16バイト境界にアライン
されていることは確認できるはずです.

*1:少し後述もしますが,環境によってはいつでもアラインメントがとれる場合もあり得ます.

*2:最適化オプション -O? つけると,このくらいのコードなら n が定数に展開されてしまい,先のコードと同じになってしまいます.実験時には最適化オプションなしでどうぞ.