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

restrict キーワードは、AI エンジン カーネルの C++ コードで使用できます。ここでは、AI エンジン カーネルのコード コンテンツで restrict キーワードを使用する際に役立つザイリンクスの推奨事項を紹介します。

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

ポインター エイリアシングとは、異なるポインター名を使用して同じメモリ ロケーションにアクセスできる状況のことです。C/C++ 言語の厳密なエイリアシング規則によって、ポインターがまったく異なるタイプを指している場合には、エイリアシングを使用しません。エイリアシングは、プログラムの実行順序に大きな制約を与えます。次に、pq のエイリアシングを示します。

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

次にポインター エイリアシングの例を示します。2 つのポインター (pq) が同じアドレスを指しています。中央の列には、コンパイラで生成されたアセンブリ言語コードを示し、右側には動作とクロック サイクルを示します。

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

このコード例に restrict キーワードを追加することにより、コンパイラは結果のアセンブリ言語を最適化して、ハードウェアでの並列処理を最大化できます。次の例は、restrict キーワードを使用してエイリアシングを回避した場合、同じ動作に使用するクロック サイクルが少なくなることを示しています。

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

メモリ依存関係

コード内のメモリの依存関係により、コンパイラで実行される最適化の種類が制限される可能性があります。たとえば、次のコードでは xyz とポインター pq は無関係となっていますが、関数コード内では、ポインター p とポインター q は両方とも同じグローバル変数 xyz を指している場合があります。このとき、コンパイラは、これらの 2 つの条件で正しい実行を確証する必要があります。このようなメモリ依存関係のため、コンパイラでは最適化が制限される必要があります。

4: 無関係なポインター

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

厳密なエイリアシング規則によって、ポインターが別のタイプを指している場合、エイリアシングを使用しないように指示されます。ただし、ほかのデータ型にエイリアシングを使用できる char*void* を除きます。オブジェクト ユニバースとその関連するポインターを示している次の図を参照してください。

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

次のような同じタイプの 2 つのポインター (pq の両方が int) の場合は、エイリアシングが適用され、性能が低下します。

6: 性能低下

次の例のように、異なるタイプの 2 つのポインター (p が int (整数) で q が float (浮動小数点)) の場合、コンパイラは厳密なエイリアシング規則を適用し、エイリアシングがあると未定義動作が生じます。

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

restrict キーワード

restrict キーワードは、主にポインターの型修飾子としてポインター宣言で使用されます。新しい機能は追加しません。これは、コンパイラが実行できる最適化についてプログラマがコンパイラに伝える唯一の方法です。ポインター (ptr) に restrict を使用すると、指定されたオブジェクトにアクセスする唯一の方法が ptr であることをコンパイラに伝えられるため、コンパイラはチェックを追加する必要はありません。

プログラマが restrict キーワードを使用し、上記の条件に違反した場合は、未定義動作が発生します。次に、デフォルトでエイリアシングのないポインターを使用する例を示します。

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

restrict キーワードを適用して、性能向上を図ります。次の例は、ほかのポインターとのメモリ依存関係がないことを示しています。

9: その他のポインターとのメモリ依存関係がない

restrict 修飾子

C 標準では、ポインター修飾子 restrict を利用できます。これは、ポインターが参照するものと、その他すべての変数との間のデータ独立性を明示することで、より積極的なコンパイラ最適化を可能にします。次に例を示します。

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

foo の分析は、*p*qa と同じオブジェクトを示さないという条件で進めることができます。したがって、a*q は、ループの前に 1 回ロード可能です。

現在、コンパイラのフロントエンドは、同じ配列への異なるアクセスを明確に認識できません。したがって、配列の 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);

これは、オプティマイザー内でこれらのポインターを区別するために必要です (たとえば、共通部分式除去なし)。この動作は、オプション -mllvm -chess-implicit-chess_copy=false を使用して、AI エンジン コンパイラ フロントエンドで無効にできます。したがって、chess_copy が 2 つのポインターを作成すると同時に、restrict はこれらのポインターを介すストア/ロード間の相互依存関係を考慮しないようにコンパイラに伝えます。ローカル スコープを持つ restrict ポインターの場合、相互独立性は restrict ポインターの存続期間中にのみ成立します。

restrict ポインターから派生したポインター (rA+1 や through pointer intrinsics) は、そのまま制限を保持します。つまり、同じ制限メモリ領域を指していると考えられます。

注記: chess_copy の詳細は、Chess コンパイラのユーザー マニュアルを参照してください。この資料は AI エンジン ラウンジにあります。

未定義動作

前のトピックで説明したとおり、restrict キーワードを使用すると性能が向上します。ただし、キーワードが適切に使用されない場合は問題が生じます。次の例に示すように、restrict が適用される子ポインターは、ポインター pq などの親ポインターとは異なるブロック レベルのスコープ内で使用する必要があります。

例 1

10: restrict キーワードを使用

同じスコープ内で親ポインターを使用すると、restrict の条件が無効になり、次の例のようにポインター pq で未定義の動作が生じる可能性があります。

11: 未定義動作

例 2

これは、次の図の緑色テキストに示されているように、load 動作中にも起こる可能性があります。

12: 読み込み (load) 動作

未定義の動作は、次の例に示すポインター pq などと同じスコープ内で restrict ポインターが使用されている場合に発生します。

13: 同じスコープ内で制限が適用されているポインター

インライン関数を使用する例

次のコード例は、ポインター p やポインター q とは異なるスコープ内で使用されるインライン関数呼び出しの動作を示しています。

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

次の例のようにポインター pq などと同じスコープ内で restrict ポインターが使用されている場合には未定義の動作が発生します。

15: インライン関数呼び出し - 同じスコープ

インライン関数における resetrict のスコープ

スコープ内にほかのアクセスがない場合には、restrict ポインターを宣言しても性能上のメリットはありません。

16: 性能上のメリットがない例

特殊なケースとして、次の例のような非エイリアシング アクセスが可能です。ここでは親ポインター p が使用されていますが、異なるロケーションを指しているため許容されます。

17: 特殊なケース ‐ 非エイリアシング アクセス

Read-Modify-Write ループに restrict キーワードを使用した場合の利点

次の例に示すように、restrict キーワードを使用しなくても機能しますが性能が低下します。

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

restrict キーワードを追加すると、すべての反復が異なるロケーション (反復間のエイリアシングはない (restrict) が、データの依存性によって保持される反復内のエイリアシングはある) にアクセス可能になります。並列処理が増えるほど、性能は向上します。

19: restrict キーワードを使用する場合

派生ポインター

次の例に示すように、restrict ポインターから派生したポインターは restrict ポインターとして見なされ、同じ制限付きメモリ領域を指します。この例では、rq1 (restrict ポインターとして定義)から派生した rq2restrict ポインターであり、同じ領域を指します。

20: 同じ制限付きメモリ領域を指すポインター

まとめ

AI エンジン カーネル プログラミングで restrict キーワードを適切に使用することで、性能が向上し、コード内での未定義動作を回避できます。ただし、同じスコープに割り当てられている場合は、restrict ポインターによってデザイン内で未定義動作が生じる可能性があります。

21: restrict キーワード使用方法