4.4 ベクタデータのアラインメント
出典: PS3 Linux Information Site / Cell/B.E.のパワーを体験しよう
第2.2節ではベクタデータのアラインメントに関する注意事項について簡単に触れました。ここでは、ベクタデータのアラインメントについて、少しハードウェアに近い側面から解説します。
4.4.1 データのロードとストア
ベクタデータのアラインメントについて理解するために、まずはじめにデータの「ロード」と「ストア」について理解をする必要があります。PPEやSPEは、メモリ上にあるデータに対して直接演算をするわけではなく、実際には「レジスタ」と呼ばれる高速アクセス可能な記憶領域にメモリ上のデータをコピーし、レジスタ上にコピーされたデータに対してさまざまな演算をおこないます。
図 4.9に示すように、PPEやSPEが、メモリからレジスタへデータを読み込む操作を「ロード」、レジスタからメモリへデータを書き出す操作を「ストア」と呼びます。また、一般的にロードやストアには、データサイズやアラインメントに関する制約があります。この制約を守らなかった場合、ロードやストアができないことや、期待した結果が得られないことがあります。
4.4.2 ベクタデータのロードとストア
PPEとSPEには、それぞれベクタデータを扱うための16バイト長のレジスタが複数本存在します。これらのレジスタへのベクタデータのロードやストアには、以下のような制約があります。
(1) データサイズ
1度のロードやストアで処理されるデータサイズは、必ずレジスタ全体の長さである16バイトになります。メモリ-レジスタ間で16バイト未満のデータを部分的にロードもしくはストアすることはできません。
(2) アラインメント
ロードやストアで対象となるデータの格納先アドレスは、必ず16バイト境界に揃えられていなければなりません。
もし、ベクタデータの先頭アドレスが16バイト境界に揃えられていない場合、これらのレジスタとメモリとの間でロードやストアをすると、図 4.10に示すようにプログラマが意図しない値が得られることになります。また、ほとんどの環境では、PPEプログラムもSPEプログラムもエラーもなく継続して後続の処理を実行するため、プログラマが予期せぬ処理結果の原因に気付くことが非常に困難になります。
この例の場合、スカラ配列aの先頭アドレスが16バイト境界に揃えられていません。従って、スカラ配列aの全要素をレジスタにロードしたつもりでも、実際は、スカラ配列aの第1要素と第2要素と、プログラマが意図していないXとYという値がレジスタにロードされることになってしまいます。その結果、後続処理で変数vaに対して演算をおこなおうとしても、スカラ配列aの第3要素と第4要素は何もされず、さらにはプログラマが意図しないXとYという値に対して演算がされてしまうことになります。
4.4.3 バイト境界が揃えられたメモリ領域の確保
前項で述べたように、ベクタデータは適切にバイト境界が揃えられている必要があります。ここでは、バイト境界が揃えられたメモリ領域を確保する方法として、第2.2節で紹介した静的なメモリ領域の確保と、動的なメモリ領域の確保の仕方について解説します。
(1) aligned属性を利用した静的メモリ領域確保
変数を確保する時、型を宣言する時に、aligned属性を付与することで、バイト境界を揃えることができます。ただし、関数内で宣言されるローカル変数のようにスタック上に確保される変数は、aligned属性を指定しても、バイト境界を揃えることが保証されないため、静的確保をおこなう場合は、グローバル変数に対して利用してください。
リスト (4-10) aligned属性を利用した静的確保 (16バイト・アラインメント)
int a[4] __attribute__((aligned(16))) char b[16] __attribute__((aligned(16)))
(2) memalign()関数、posix_memalign()関数を利用した動的メモリ領域確保
プログラムの関数内のローカル変数や、確保するべきメモリ領域のサイズが不定の場合は、動的なメモリ領域を確保してください。バイト境界が揃えられたメモリ領域の動的確保には、memalign()関数もしくはposix_memalign()関数を利用します。それぞれの関数の引数および返値を、API定義 (4-1)、(4-2) に示します。
API定義 (4-1) memalign()関数のプロトタイプ宣言
void *mamalign(size_t boundary, size_t size);
| 第1引数 | boundary | 割り当てるメモリ領域のバイト境界サイズを指定します。 |
| 第2引数 | size | 割り当てるメモリ領域のサイズを指定します。 |
| 返値 | 成功時には、割り当てたメモリ領域へのポインタを返します。失敗時には、NULLを返し、errnoにエラーコードを設定します。 |
API定義 (4-2) posix_memalign()関数のプロトタイプ宣言
int posix_mamalign(void **memptr, size_t alignment, size_t size);
| 第1引数 | memptr | 割り当てられたメモリ領域のアドレスを格納するポインタ変数へのポインタを指定します。 |
| 第2引数 | alignment | 割り当てるメモリ領域のバイト境界サイズを指定します。 |
| 第3引数 | size | 割り当てるメモリ領域のサイズを指定します。 |
| 返値 | 成功時には、0を返します。失敗時には、0以外の値を返します。 |
なお、PPEプログラム側でのmemalign()関数の利用は推奨されていないため、posix_mamalign()関数を利用してください。また、SPEプログラム用には、posix_memalign()関数は提供されていないため、memalign()関数を利用してください。
memalign()関数およびposix_memalign()関数の利用方法を用例 (4-3)、(4-4) にそれぞれ示します。
用例 (4-3) memalign()関数を利用した動的確保 (16バイト・アラインメント) (SPEプログラム用)
#include <malloc.h> char *buffer; buffer = (char *) memalign(16, 1024);
用例 (4-4) posix_memalign()関数を利用した動的確保 (16バイト・アラインメント) (PPEプログラム用)
#define _XOPEN_SOURCE 600 #include <stdlib.h> char *buffer; ret = posix_memalign(&buffer, 16, 1024);
なお、SPEプログラムが利用できるメモリ領域は256KBとそれほど大きくないため、メモリの効率的な利用という点では、動的なメモリ領域の確保は基本的に避けるべきです。これは、メモリ領域の動的な確保と解放を繰り返すことによって、利用可能な空きメモリの領域が細分化 (フラグメント) されてしまい、一度に大きなメモリ領域を確保できなくなるためです。
4.4.4 バイト境界が揃えられていないデータのロードとストア
これまで述べてきたように、ベクタデータは適切にバイト境界が揃えられている必要があります。もし、バイト境界が揃えられていないメモリ領域に対してロードやストアをする場合は、単純なロードやストアの操作以外に多くの処理が必要になります。ここでは、参考までにSPEプログラム用のバイト境界が揃えられていないベクタデータのロードとストア方法について掲載します。
リスト (4-11) は、バイト境界が揃えられていないベクタデータをロードする関数の一例です。vector_load_unaligned()関数は、第1引数に指定された任意のアドレスに格納された16バイトデータをレジスタへロードし、その結果をベクタデータとして返します。
リスト (4-11) バイト境界が揃えられていないベクタデータのロード関数 (SPEプログラム用)
1 vector unsigned char vector_load_unaligned(vector unsigned char *ptr)
2 {
3 vector unsigned char qw0, qw1, qw;
4 int shift;
5
6 qw0 = *ptr;
7 qw1 = *(ptr+1);
8 shift = (unsigned int) ptr & 0xf;
9 qw = spu_or(spu_slqwbyte(qw0, shift), spu_rlmaskqwbyte(qw1, shift-16));
10
11 return (qw);
12 }
| 3行目 | ベクタデータのロードに必要な変数qw0、qw1、qwを宣言します。 |
| 4行目 | ベクタデータのバイトシフトを指定する変数shiftを宣言します。 |
| 6行目 | レジスタにロードしたいベクタデータの左部分を含む16バイト値をロードします。 |
| 7行目 | レジスタにロードしたいベクタデータの右部分を含む16バイト値をロードします。 |
| 8行目 | 16バイト境界からオフセットを変数shiftに代入します。 |
| 9行目 | spu_slqwbyte()関数を用いて、左部分のベクタデータをshiftバイト分左シフトします。spu_rlmaskqwbyte()関数を用いて、右部分のベクタデータをshift-16バイト分右シフトします。それぞれのシフトされたデータをspu_or()関数を用いて合成します。 |
| 11行目 | 適切にロードされた16バイト値を返します。 |
リスト (4-12) は、バイト境界が揃えられていないアドレスへベクタデータをストアする関数の一例です。vector_store_unaligned()関数は、第1引数に指定された16バイトのベクタデータを、第2引数に指定された任意のアドレスへ格納します。
リスト (4-12) バイト境界が揃えられていないベクタデータのストア関数 (SPEプログラム用)
1 void vector_store_unaligned(vector unsigned char qw, vector unsigned char *ptr)
2 {
3 vector unsigned char qw0, qw1;
4 vector unsigned char mask = spu_splats((unsigned char) 0xff);
5 int shift;
6
7 qw0 = *ptr;
8 qw1 = *(ptr+1);
9 shift = (unsigned int) ptr & 0xf;
10 mask = spu_rlmaskqwbyte(mask, -shift);
11 qw = spu_rlqwbyte(qw, -shift);
12 *ptr = spu_sel(qw0, qw, mask);
13 *(ptr+1) = spu_sel(qw1, qw, mask);
14
15 return;
16 }
| 3行目 | ベクタデータのストアに必要な変数qw0、qw1を宣言します。 |
| 4行目 | ベクタデータの合成用のマスク値を指定する変数shiftを宣言します。 |
| 5行目 | ベクタデータのバイトシフトを指定する変数maskを宣言します。 |
| 7行目 | ベクタデータの左部分のストア先のメモリ領域の16バイト値をロードします。 |
| 8行目 | ベクタデータの右部分のストア先のメモリ領域の16バイト値をロードします。 |
| 9行目 | 変数shiftにストア先のメモリ領域のバイトシフト値を代入します。 |
| 10行目 | spu_rlmaskqwbyte()関数を用いて、ベクタデータとストア先のメモリ領域の合成用のマスクを変数maskに代入します。 |
| 11行目 | spu_rlqwbyte()関数を用いて、ベクタデータをshiftバイト分右ローテートし、ストア用の合成データを作成します。 |
| 12行目 | spu_sel()関数を用いて、ベクタデータの左部分と、ストア先のメモリ領域を合成し、元のメモリ領域に書き戻します。 |
| 13行目 | spu_sel()関数を用いて、ベクタデータの右部分と、ストア先のメモリ領域を合成し、元のメモリ領域に書き戻します。 |
このように、バイト境界が揃えられていないメモリ領域に対するロードとストアでは、非常に多くの処理が必要となりますので、SIMDプログラミングをおこなう場合、処理対象のデータは適切なバイト境界に揃えるようにしてください。


