AI エンジン カーネルでの restrict キーワードの使用

AI エンジン カーネル C++ コードでは、__restrict キーワードを使用できます。この付録では、AI エンジン カーネル コードで restrict キーワードを使用する際のザイリンクスの推奨事項を示します。

ポインター エイリアシング

ポインター エイリアシングとは、同じメモリ ロケーションに異なるポインター名を使用してアクセスできる状況のことを指します。C/C++ での厳密なエイリアシング規則により、ポインターが根本的に異なるタイプをポイントする場合は、ポインターはエイリアシングしないと想定されます。エイリアシングにより、プログラムの実行順が厳密に制約されます。次の図に、pq のエイリアシングを示します。

1: ポインター エイリアシング

次の例は、ポインター pq の両方が同じアドレスをポイントするポインター エイリアシングの例です。中央にコンパイラで生成されるアセンブリ言語コード、右側に演算とクロック サイクルを示しています。

2: エイリアシングのコード例

restrict キーワードをこのコード例に追加すると、コンパイラで生成されるアセンブリ言語を最適化し、ハードウェアでの演算を並列化できます。次の例では、restrict キーワードを使用してエイリアシングしないようにすると、同じ演算を完了するのに使用するクロック サイクル数が少なくなることを示します。

3: restrict キーワードを使用してエイリアシングを回避

メモリの依存性

コード内のメモリ依存により、コンパイラで実行される最適化の種類が制限されます。たとえば次のコードでは、xyz とポインター p および q は無関係である可能性がありますが、関数コード内では、ポインター p とポインター q が同じグローバル変数 xyz をポイントしています。コンパイラは、これら両方の状況で正しく実行されるように処理する必要があります。このようなメモリ依存があるので、コンパイラは最適化を制限する必要があります。

4: 無関係のポインター

厳密なエイリアシング規則

厳密なエイリアシング規則により、ポインターが異なるタイプをポイントする場合は、任意のデータ型にエイリアシング可能な char* および void* を除き、ポインターはエイリアシングしないと想定されます。これを次の図に示します。この図では、オブジェクト ユニバースと関連のポインターを示します。

5: オブジェクト ユニバース
ポインターはタイプ ユニバース U(T) で関連付けられている
T はテンプレートです。上の図には、int ユニバース、float ユニバースなどのテンプレートが示されています。また、デザインごとに MyClass ユニバースもあります。さらに、デフォルトですべてのユニバースを含む char ユニバースがあります。
ユニバースはエイリアシングしない
ポインター p は、int ユニバース内のアドレスのみをポイント可能で、ポインター qfloat ユニバース内のアドレスのみをポイント可能です。そのため、ポインター p とポインター q はエイリアシングできません。
派生ポインターは元のユニバースをポイント
restrict ポインターから派生したポインターは restrict ポインターとなり、同じ制限されたメモリ領域をポイントします。詳細は、派生ポインター を参照してください。
char* ユニバースはすべてのユニバースを含む
char ポインターは、すべてのユニバースのどの変数でもポイントできます。

次の図に示す 2 つのポインター p および q は同じタイプ (int) で、コンパイラによりエイリアシングが適用され、パフォーマンスが低下します。

6: パフォーマンスの低下

次の図に示す例では、ポインター pint、ポインター qfloat であり、コンパイラにより厳密なエイリアシング規則が適用され、エイリアシングが存在する場合は動作が予期しないものになります。

7: タイプの異なる 2 つのポインター

restrict キーワード

restrict キーワードは、ポインターの宣言でポインターの型修飾子として主に使用されます。新しい機能は追加されません。コンパイラに可能な最適化について通知します。ポインターに __restrict を使用すると、ポイントされているオブジェクトにアクセスするにはポインターを使用する必要があることが指定され、コンパイラが追加のチェックを実行する必要はありません。

注記: restrict キーワードを使用しているときに上記の条件に違反すると、動作が不定義のものになります。

次は、ポインターの別の例であり、デフォルトではエイリアシングは実行されません。

8: エイリアシングなしの例

パフォーマンスを向上させるため、restrict キーワードを適用します。次に、ほかのポインターとのメモリ依存がない例を示します。

9: ほかのポインターとのメモリ依存がない例

restrict 修飾子

C 標準には __restrict ポインター修飾子があり、ポインター参照とその他すべての変数とのデータ独立性を明示的に記述することにより、コンパイラにより最適化をより積極的に実行することができます。次に例を示します。

int a; // global variable
void foo(int* __restrict p, int* q)
{
  for (...) { ... *p += a + *q; ...}
}

foo の解析は、*p*q および a と同じオブジェクトを指していないという事実に基づいて進めることができます。そのため、a*q はループの前に一度に読み込むことができます。

現在のところ、コンパイラのフロント エンドでは、同じ配列への異なるアクセスは明確に区別されません。そのため、配列の 1 つの要素をアップデートすると、配列全体の値が変更されたと想定されます。__restrict 修飾子を使用すると、この想定を無効にできます。これは、同じ配列への複数の独立したポインターを取得する場合に便利です。

void foo(int A[])
{
  int* __restrict rA = A; // force independent access
  for (int i = ...)
  rA[i] = ... A[i];
}

この例では、__restrict 修飾子により、ループのソフトウェア パイプライン処理が可能となり、前の要素を保存しながら次の要素を読み込むことができます。__restrict 修飾子の影響を最大限にするため、コンパイラのフロント エンドにより、デフォルトで初期化に chess_copy 演算が挿入されます。

int* __restrict rA = chess_copy(A);

これは、最適化で両方のポインターを異なるものに保持するために必要です (たとえば、共通部分式除去なし)。AI エンジン コンパイラのフロント エンドでは、この動作は -mllvm -chess-implicit-chess_copy=false オプションによりディスエーブルになっています。そのため、chess_copy により 2 つのポインターが作成され、__restrict によりこれらのポインターを使用したストア/ロード間の相互依存はコンパイラにより考慮されません。ローカル スコープを持つ __restrict ポインターでは、__restrict ポインターが有効である間のみ相互に独立していると想定されます。

__restrict ポインターから派生したポインター (rA+1 やポインター組み込み関数など) も restrict ポインターとなり、同じ制限されたメモリ領域をポイントします。

注記: chess_copy の詳細は、AI エンジン ラウンジから『Chess コンパイラ ユーザー マニュアル』を参照してください。

未定義の動作

前のトピックで示したように、restrict キーワードを使用するとパフォーマンスが向上します。ただし、このキーワードを不適切に使用すると、問題が発生します。次の例のポインター p および q のように、__restrict 子ポインターは、親ポインターとは異なるブロック レベル スコープで使用する必要があります

例 1

10: restrict キーワードの使用

同じスコープで親ポインターを使用すると、__restrict 規則が機能せず、次の例の p および q のように未定義の動作になります。

11: 未定義の動作

例 2

これは、次の図の緑のテキスト (return *p;) に示すように、load 操作でも発生する可能性があります。

12: 読み込み操作

次の例のポインター p および q のように、restrict ポインターを同じスコープ内で使用すると、未定義の動作になります。

13: 同じスコープでの restrict ポインターの指定

インライン展開関数での例

次のコードは、機能するインライン展開関数呼び出しの例で、ポインター p とポインター q が異なるスコープで定義されています。

14: インライン展開関数呼び出し

次の例のポインター p および q のように、restrict ポインターを同じスコープ内で使用すると、未定義の動作になります。

15: 同じスコープでのインライン展開関数呼び出し

インライン関数での restrict キーワードの適用範囲

スコープ内にほかのアクセスがない場合は、restrict ポインターを宣言してもパフォーマンスの利点はありません。

16: パフォーマンスの利点がない例

特殊な場合には、次の例に示すように、エイリアシングなしのアクセスが可能です。この例では、親ポインター p が使用されていますが、別の場所をポイントしているので、これは有効です。

17: 特殊な場合: エイリアシングなしのアクセス

読み出し/変更/書き込みループに restrict キーワードを使用する利点

次の例は、restrict キーワードなしでも機能しますが、パフォーマンスは悪くなります。

18: restrict キーワードを使用しない例

restrict キーワードを使用すると、反復間でエイリアシングは使用されず (__restrict)、反復内のエイリアシングはデータ依存により保持され、各反復で異なるロケーションにアクセスできるようになります。並列処理が増加するので、パフォーマンスが向上します。

19: restrict キーワードを追加

派生ポインター

restrict ポインターから派生したポインターは、restrict ポインターと考慮され、次の図に示すように、同じ制限されtらメモリ領域をポイントします。ここで、rq2rq1 (restrict ポインターとして定義) から派生したポインターで、同じく restrict ポインターで、同じユニバースをポイントします。

20: 同じ制限されたメモリ領域へのポインター

まとめ

AI エンジン カーネルのプログラミングで restrict キーワード (__restrict) を正しく使用することにより、パフォーマンスが向上し、コードの未定義の動作を取り除くことができます。ただし、同じスコープに割り当てる場合、restrict ポインターによりデザインが未定義の動作になることがあるので注意してください。

21: restrict キーワードの使用サマリ