アライメントを揃える方法|行列積高速化#15
この記事は、以下の記事を分割したものです。
[元の記事]行列積計算を高速化してみる
一括で読みたい場合は、元の記事をご覧ください。
アセンブラ拡張命令セット(SSE2やAVX2など)では、メモリのアドレスがブロック境界にあるかどうか(アライメントされているか)で使用する命令が異なります。例えば、SSE2命令セットの場合、アライメントされていることが前提にあればMOVAPS命令が使用できますが、アライメントが不明の場合はMOVUPS命令を使用せざるを得ません。しかし、一般にMOVAPSの方が高速です。
そこで、可能な限りアライメントを揃えておくと、MOVAPS命令が使用できるため、高速処理が期待できます。また、アライメントが揃っている前提になるため、アセンブラコードの場合分けも不要になります。
15-1. アライメントの確認方法
アライメントの確認は、ポインタのアドレスを整数化すると確認できます。
アライメントの確認方法
(1)ポインタアドレスを非負整数にキャストする
(2)下位ビットが0であることを確認する
8B境界であれば下位3ビットが0に、16B境界では下位4ビットが0に、32B境界であれば下位5ビットが0になっています。
例えば、次のようなコードをソースコードに挿入します。
printf("Ptr=0x%x, Align16=0x%x, Align32=0x%x\n",(uint64_t)ptr,((uint64_t)ptr)&0xf,((uint64_t)ptr)&0x1f);
このコードでは、16B境界と32B境界を確かめるために、0xf, 0x1fとのANDを計算しています。AND計算の結果が0になれば、アライメントされていることを示しています。実際の結果は、次のように表示されます。
Ptr=0x9c501000, Align16=0x0, Align32=0x0
上記の結果より、16B境界にも32B境界にもアライメントされていることがわかります。
15-2. アライメントする方法
GCCのマニュアルによると、malloc関数やrealloc関数で割り付けられるメモリのアドレスは、32bitシステムでは8の倍数に、64bitシステムでは16の倍数になるように割り付けられるそうです。つまり、アライメントされています。
しかし、AVX2命令で必要なのは、32B境界へのアライメントです。
助かることに、標準C11からは、任意の境界にアライメントできるメモリ割り付け関数aligned_allocが追加されています。
void * aligned_alloc (size_t alignment, size_t size)
第一引数alignmentにはアライメント境界値(16や32など)を指定します。第二引数sizeには割り付けるメモリサイズをバイト単位で指定します。ただし、sizeはalignmentの倍数になっていなければ行けません。もし、倍数になっていなければNULLポインタが返却されてきます。
15-3. 行列積計算におけるアライメント
今回の行列積プログラムでは、L2キャッシュにデータを配置する目的、行列積カーネル関数でシーケンシャルアクセスを実現する目的、さらに転置指定を吸収する目的で、行列Aと行列Bのバッファに詰め替えています。入力される行列A,Bのアライメントは確定できませんが、行列積関数内部でメモリ割り付けを行うため、このバッファのアライメントは境界に合わせておくことが可能です。
このバッファのアライメントを揃えておくと、行列積カーネル関数内で行列Aと行列Bは必ずアライメントされていることを前提できます。そのおかげで、気兼ねなくMOVAPD命令を使うことができます。
変更点はほんの少しで、メモリ割り付けをcalloc関数からaligned_alloc関数に変更するだけです。
変更前
double* A2 = calloc( MYBLAS_BLOCK_M*MYBLAS_BLOCK_K, sizeof(double) );
double* B2 = calloc( MYBLAS_BLOCK_K*MYBLAS_BLOCK_N, sizeof(double) );
変更後
double* A2 = aligned_alloc( ALIGNMENT_B, MYBLAS_BLOCK_M*MYBLAS_BLOCK_K*sizeof(double) ); // C11 standard
double* B2 = aligned_alloc( ALIGNMENT_B, MYBLAS_BLOCK_K*MYBLAS_BLOCK_N*sizeof(double) ); // C11 standard
ここで、ALIEGNMENT_Bはマクロ定義で32に指定しています。また、ブロックサイズとタイルサイズの設定は下記の通りでした。
#define MYBLAS_BLOCK_M 128
#define MYBLAS_BLOCK_N 64
#define MYBLAS_BLOCK_K 128
#define MYBLAS_TILE_M 32
#define MYBLAS_TILE_N 32
#define MYBLAS_TILE_K 32
#define ALIGNMENT_B 32 // for AVX
ブロックサイズを全て2^nにしているため、バッファA2もバッファB2もメモリサイズは必ず32の倍数になります。だから、aligned_alloc関数のメモリ割付は必ず成功します。
また、L1キャッシュブロッキングループで、バッファA2とB2はタイルサイズごとに切り取って利用しますが、バッファがアライメントされていれば、タイルの先頭アドレスも必ずアライメントされています。なぜなら、タイルサイズが32の倍数になっているため、32x32分ずつポインタシフトしてもアドレスは常に32B境界上にあるためです。
15-4. 計算速度のチェック
コンパイラは、コンパイル単位ではアライメントが揃っていることを判断できませんので、今までと同様にコンパイルされます。そのため、現段階ではアライメントの変更は性能に影響しません。
<変更前:calloc関数を利用した場合>
Max Peak MFlops per Core: 52800 MFlops
Base Peak MFlops per Core: 46400 MFlops
size , elapsed time[s], MFlops, base ratio[%], max ratio[%]
16, 3.8147E-05, 234.881, 0.506209, 0.44485
32, 2.90871E-05, 2358.71, 5.08343, 4.46726
64, 0.000140905, 3808.06, 8.20702, 7.21223
128, 0.000905037, 4688.71, 10.105, 8.88013
256, 0.00689793, 4892.93, 10.5451, 9.2669
512, 0.05779, 4658.62, 10.0401, 8.82315
1024, 0.42357, 5077.39, 10.9426, 9.61626
2048, 3.34036, 5146.89, 11.0924, 9.74789
<変更後:aligned_alloc関数を利用した場合>
Max Peak MFlops per Core: 52800 MFlops
Base Peak MFlops per Core: 46400 MFlops
size , elapsed time[s], MFlops, base ratio[%], max ratio[%]
16, 6.00815E-05, 144.87, 0.31222, 0.274375
32, 1.90735E-05, 3543.35, 7.63653, 6.71089
64, 0.000138998, 3830.85, 8.25613, 7.25539
128, 0.000906944, 4660.78, 10.0448, 8.82724
256, 0.00656891, 5128.02, 11.0518, 9.71216
512, 0.0586259, 4587.73, 9.88734, 8.68888
1024, 0.422727, 5085.03, 10.9591, 9.63074
2048, 3.32415, 5170.72, 11.1438, 9.79303
次の記事
元の記事はこちらです。
ソースコードはGitHubで公開しています。