インテル® C++ コンパイラー XE 13.1 ユーザー・リファレンス・ガイド
インテル® コンパイラーのベクトライザー・コンポーネントは、自動的に SIMD (single-instruction multiple data) 処理を活用することを目的としています。コンパイラーに (自動ベクトライザー・ヒントやプラグマなどを使用して) 追加情報を提供することで、コンパイラーのベクトル化を助けることができます。
-vec (Linux*) または /Qvec (Windows*) オプションを使用すると、インテル製マイクロプロセッサーおよび互換マイクロプロセッサーにおいて、デフォルトの最適化レベルでベクトル化が有効になります。ベクトル化により呼び出されるライブラリー・ルーチンは、互換マイクロプロセッサーよりもインテル製マイクロプロセッサーにおいてより優れたパフォーマンスが得られる可能性があります。また、ベクトル化は、/arch や /Qx (Windows*) または -m や -x (Linux* および OS X*) などの特定のオプションによる影響を受けます。
最内ループ本体をベクトル化するガイドラインを次に示します。
好ましいもの
分かりやすいコード (単一の基本ブロック)
ベクトルデータのみ。すなわち、代入文の右辺には、配列と不変式を使用します。代入式の左辺には配列参照を使用してもかまいません。
代入文のみ
避けるべきもの
関数呼び出し (算術ライブラリー関数の呼び出し以外)
ベクトル化できない演算 (ループがベクトル化できないか、演算がいくつかの命令によってエミュレートされている)
ベクトル化できる複数の型を同じループの中に混在させること (リソース活用の低下を招く)
データ依存性を持つループ出口条件 (ベクトル化の妨げとなる)
ベクトル化可能なコードに変えるには、ループを変更しなければならない場合がよくあります。ただし、変更する部分は、ベクトル化に最小限必要な箇所だけです。特に、次の項目に注意してください。
ループアンロールはしないこと。これはコンパイラーが自動的に行います。
本体の中で複数の文で構成している 1 つのループを複数の単文ループに解体すること
次の制約事項に注意してください。ベクトル化は、ハードウェアとソースコードのスタイルという 2 つの主な要因により制約されます。
要因 |
説明 |
---|---|
ハードウェア |
コンパイラーは、それを動かすハードウェアからいくつか制約を受けます。ストリーミング SIMD 拡張命令を実行する場合、ベクトルメモリー演算は、16 バイトでアライメントしたメモリー参照が優先するため、アクセスストライドが 1 に制限されます。つまり、コンパイラーは、たとえループをベクトル化可能と抽象的に認識しても、別のターゲット・アーキテクチャーに対してはベクトル化をしないかもしれません。 |
ソースコードのスタイル |
ソースコードの書き方によってはベクトル化の妨げになるときもあります。例えば、グローバルポインターを使用する場合に、2 つのメモリー参照が別々の場所で行われるかどうかをコンパイラーが判別できないという問題がよく起こります。このような場合、一部の変換処理は順序の並べ替えができなくなります。 |
コンパイラーによる自動ベクトル化を阻害する多くの要因はループ構造の書き方にあります。ループ本体にはキーワード、演算子、ポインター計算、データ参照、およびメモリー演算が含まれており、それらが互いに作用し合うためにループの動作が分かりづらくなります。
しかし、これらの制約を理解し、診断メッセージの読み方を知れば、そうした既知の制約条件を克服して効率良くベクトル化できるようプログラムを修正できます。
ベクトル化可能なコードを記述するガイドラインを次に示します。
このヒントは注意して使用してください。アライメントされたデータ移動の誤った使用は、SSE の例外をもたらします。
データ構造のアライメントとは、他のオブジェクトと関連したデータ・オブジェクトの調整です。インテル® コンパイラーはメモリーアクセスを高速化するために、各変数が特定のアドレスで開始するようにアライメントします。アライメントされていないメモリーへのアクセスは、ハードウェアでこのようなアクセスをサポートしていない対象プロセッサーでは、パフォーマンスの大幅な低下を招きます。
アライメントとはメモリーアドレスのプロパティーの 1 つで、数値アドレスモジュロ 2 の累乗 (数値アドレスを 2 のべき乗で除算した剰余) で表します。各データにはアドレスに加えて、サイズも設定されています。アドレスがデータのサイズに合わせてアライメントされている場合、そのデータは '自然にアライメントされてる' といいます。そうでない場合は 'アライメントされていない' といいます。例えば、8 バイトの浮動小数点データのアドレスが 8 にアライメントされている場合、このデータは自然にアライメントされています。
データ構造は、データを効率良く使用できるようにコンピューターに格納するための方法です。通常、慎重に選択されたデータ構造では、より効率的なアルゴリズムを使用できます。適切に設計されたデータ構造により、最小のリソース (実行時間とメモリー空間の両方) を使用して、さまざまな操作が実行可能になります。
struct MyData{
short Data1;
short Data2;
short Data3;
};
上記のデータ構造の例では、short 型が 2 バイトのメモリーに格納されると、データ構造の各メンバーは 2 バイト境界にアライメントされます。Data1 はオフセット 0、Data2 はオフセット 2、Data3 はオフセット4 です。この構造のサイズは 6 バイトです。通常、構造の各メンバーの型にはアライメント要件があります。つまり、開発者が要求しない限り、あらかじめ決定された境界にアライメントされます。コンパイラーが次善のアライメントを採用した場合、開発者は declspec(align(base,offset)) 宣言を使用して、特定の base からの offset にデータ構造を割り当てられます。(0 <= offset < base で、base は 2 の累乗)。
プログラムの実行時間のほとんどが次のようなループで費やされている例について考えてみてだくさい。
double a[N], b[N];
...
for (i = 0; i < N; i++){
a[i+1] = b[i] * 3;
}
両方の配列の最初の要素が 16 バイト境界でアライメントされる場合、ベクトル化後に b からのアライメントの合っていない要素のロードか、a へのアライメントの合っていない要素のストアのいずれかが行われます。
この場合、反復を小さくしても意味がありません。
__declspec(align(16, 8)) double a[N];
__declspec(align(16, 0)) double b[N];
/* or simply "align(16)" */
ポインター変数が使用されると、コンパイラーは通常、コンパイル時にアクセスパターンのアライメントを判断できません。次の単純な fill() 関数について考えます。
void fill(char *x) {
int i;
for (i = 0; i < 1024; i++){
x[i] = 1;
}
}
追加の情報がない限り、コンパイラーは上記のループでアクセスされるメモリー領域のアライメントについて仮定できません。この時点で、コンパイラーは unaligned データ移動命令を使用してこのループをベクトル化することを決定するか、またはここで示すランタイムのアライメントの最適化を生成します。
peel = x & 0x0f;
if (peel != 0) {
peel = 16 - peel;
/* runtime peeling loop */
for (i = 0; i < peel; i++){
x[i] = 1;
}
}
/* aligned access */
for (i = peel; i < 1024; i++){
x[i] = 1;
}
ランタイムの最適化は、コードサイズとテストがやや増えますが、一般にアライメントの合ったアクセスパターンを取得する効率的な方法を提供します。ただし、アクセスパターンが 16 バイト境界にアライメントされていることが保証されていれば、関数でヒント __assume_aligned(x, 16); を使用してその情報をコンパイラーに伝え、オーバーヘッドを回避することができます。
このヒントは注意して使用してください。アライメントされたデータ移動の誤った使用は、SSE の例外をもたらします。
最も一般的でよく知られているデータ構造は、通常のインデックスでアクセス可能な、連続したデータ項目の集合を含む配列で、構造体配列 (AoS)、または配列構造体 (SoA) として整理することができます。AoS はカプセル化に最適ですが、ベクトル処理には適していません。
適切なデータ構造を選択することで、ベクトル化でより効率的な生成コードを得ることができます。この点を示す例として、3 次元ポイントのセットの r、g、b コンポーネントを格納する従来の構造体配列 (AoS) と、配列構造体 (SoA) を比較してみます。
Point 構造体:
// AoS を使用した Point 構造体
struct Point{
float r;
float g;
float b;
}
Points 構造体:
// SoA を使用した Points 構造体
struct Points{
float* x;
float* y;
float* z;
}
AoS 配置では、次の RGB ポイントに移動する前に 1 つの RGB ポイントのすべてのコンポーネントにアクセスするループは、フェッチされたキャッシュラインのすべての要素を使用するため、高い参照の局所性をもたらします。AoS 配置の短所は、そのようなループの各メモリー参照は非ユニットストライドをもたらすことです。これは一般に、ベクトル・パフォーマンスに不利に働きます。さらに、すべてのポイントの 1 つのコンポーネントにのみアクセスするループは、フェッチされたキャッシュラインの多くの要素が再利用されないため、より低い参照の局所性をもたらします。
逆に SoA 配置では、ユニットストライドのメモリー参照は効率的なベクトル化の恩恵がより受けられ、また 3 つのデータストリームのそれぞれで、高い参照の局所性をもたらします。したがって、ベクトル化コンパイラーでコンパイルする場合、SoA 配置を使用するアプリケーションは、AoS 配置ベースのアプリケーションよりパフォーマンスが優れています (ただし、このパフォーマンスの差は実装の早い段階では明白でないことがあります)。
例えば、3 次元座標を処理する場合、3 コンポーネント構造体の 1 つの配列 (AoS) を使用する代わりに、各コンポーネントに対する 3 つの異なる配列 (SoA) を使用します。最終的にベクトル化を阻むループ間の依存性を避けるために、3 コンポーネント構造体の 1 つの配列 (AoS) の代わりに、各コンポーネントに対する 3 つの異なる配列 (SoA) を使用します。AoS 配置を使用する場合、各反復は XYZ を計算することによって 1 つの結果を導きますが、4 つ目のコンポーネントが使用されていないため、最大でも SSE の 75%しか使用できません。場合によっては、1 つのコンポーネント (25%) しか使用しないこともあります。SoA 配置を使用すると、各反復は SSE ユニットを 100% 使用し、XXXX、YYYY、および ZZZZ を計算することによって 4 つの結果を導きます。SoA 配置の短所は、コード量がおよそ 3 倍に増えることです。一方、コンパイラーは AoS 配置の SSE コードを全くベクトル化できない可能性があります。
オリジナルのデータレイアウトが AoS 形式であれば、性能に影響を及ぼすループの前に SoA への動的な変換を検討してください。ベクトル化されれば、それだけの価値はあります。