見出し画像

アライメントを揃える方法|行列積高速化#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で公開しています。


この記事が気に入ったらサポートをしてみませんか?