AI エンジン カーネルでの restrict キーワードの使用
AI エンジン カーネル C++ コードでは、__restrict キーワードを使用できます。この付録では、AI エンジン カーネル コードで restrict キーワードを使用する際のザイリンクスの推奨事項を示します。
ポインター エイリアシング
ポインター エイリアシングとは、同じメモリ ロケーションに異なるポインター名を使用してアクセスできる状況のことを指します。C/C++ での厳密なエイリアシング規則により、ポインターが根本的に異なるタイプをポイントする場合は、ポインターはエイリアシングしないと想定されます。エイリアシングにより、プログラムの実行順が厳密に制約されます。次の図に、p と q のエイリアシングを示します。
次の例は、ポインター p と q の両方が同じアドレスをポイントするポインター エイリアシングの例です。中央にコンパイラで生成されるアセンブリ言語コード、右側に演算とクロック サイクルを示しています。
restrict キーワードをこのコード例に追加すると、コンパイラで生成されるアセンブリ言語を最適化し、ハードウェアでの演算を並列化できます。次の例では、restrict キーワードを使用してエイリアシングしないようにすると、同じ演算を完了するのに使用するクロック サイクル数が少なくなることを示します。
メモリの依存性
コード内のメモリ依存により、コンパイラで実行される最適化の種類が制限されます。たとえば次のコードでは、xyz とポインター p および q は無関係である可能性がありますが、関数コード内では、ポインター p とポインター q が同じグローバル変数 xyz をポイントしています。コンパイラは、これら両方の状況で正しく実行されるように処理する必要があります。このようなメモリ依存があるので、コンパイラは最適化を制限する必要があります。
厳密なエイリアシング規則
厳密なエイリアシング規則により、ポインターが異なるタイプをポイントする場合は、任意のデータ型にエイリアシング可能な char* および void* を除き、ポインターはエイリアシングしないと想定されます。これを次の図に示します。この図では、オブジェクト ユニバースと関連のポインターを示します。
- ポインターはタイプ ユニバース U(T) で関連付けられている
- T はテンプレートです。上の図には、
intユニバース、floatユニバースなどのテンプレートが示されています。また、デザインごとにMyClassユニバースもあります。さらに、デフォルトですべてのユニバースを含むcharユニバースがあります。 - ユニバースはエイリアシングしない
- ポインター
pは、intユニバース内のアドレスのみをポイント可能で、ポインターqはfloatユニバース内のアドレスのみをポイント可能です。そのため、ポインターpとポインターqはエイリアシングできません。 - 派生ポインターは元のユニバースをポイント
- restrict ポインターから派生したポインターは restrict ポインターとなり、同じ制限されたメモリ領域をポイントします。詳細は、派生ポインター を参照してください。
char*ユニバースはすべてのユニバースを含むcharポインターは、すべてのユニバースのどの変数でもポイントできます。
次の図に示す 2 つのポインター p および q は同じタイプ (int) で、コンパイラによりエイリアシングが適用され、パフォーマンスが低下します。
次の図に示す例では、ポインター p は int、ポインター q は float であり、コンパイラにより厳密なエイリアシング規則が適用され、エイリアシングが存在する場合は動作が予期しないものになります。
restrict キーワード
restrict キーワードは、ポインターの宣言でポインターの型修飾子として主に使用されます。新しい機能は追加されません。コンパイラに可能な最適化について通知します。ポインターに __restrict を使用すると、ポイントされているオブジェクトにアクセスするにはポインターを使用する必要があることが指定され、コンパイラが追加のチェックを実行する必要はありません。
次は、ポインターの別の例であり、デフォルトではエイリアシングは実行されません。
パフォーマンスを向上させるため、restrict キーワードを適用します。次に、ほかのポインターとのメモリ依存がない例を示します。
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 ポインターとなり、同じ制限されたメモリ領域をポイントします。
未定義の動作
前のトピックで示したように、restrict キーワードを使用するとパフォーマンスが向上します。ただし、このキーワードを不適切に使用すると、問題が発生します。次の例のポインター p および q のように、__restrict 子ポインターは、親ポインターとは異なるブロック レベル スコープで使用する必要があります。
例 1
同じスコープで親ポインターを使用すると、__restrict 規則が機能せず、次の例の p および q のように未定義の動作になります。
例 2
これは、次の図の緑のテキスト (return *p;) に示すように、load 操作でも発生する可能性があります。
次の例のポインター p および q のように、restrict ポインターを同じスコープ内で使用すると、未定義の動作になります。
インライン展開関数での例
次のコードは、機能するインライン展開関数呼び出しの例で、ポインター p とポインター q が異なるスコープで定義されています。
次の例のポインター p および q のように、restrict ポインターを同じスコープ内で使用すると、未定義の動作になります。
インライン関数での restrict キーワードの適用範囲
スコープ内にほかのアクセスがない場合は、restrict ポインターを宣言してもパフォーマンスの利点はありません。
特殊な場合には、次の例に示すように、エイリアシングなしのアクセスが可能です。この例では、親ポインター p が使用されていますが、別の場所をポイントしているので、これは有効です。
読み出し/変更/書き込みループに restrict キーワードを使用する利点
次の例は、restrict キーワードなしでも機能しますが、パフォーマンスは悪くなります。
restrict キーワードを使用すると、反復間でエイリアシングは使用されず (__restrict)、反復内のエイリアシングはデータ依存により保持され、各反復で異なるロケーションにアクセスできるようになります。並列処理が増加するので、パフォーマンスが向上します。
派生ポインター
restrict ポインターから派生したポインターは、restrict ポインターと考慮され、次の図に示すように、同じ制限されtらメモリ領域をポイントします。ここで、rq2 は rq1 (restrict ポインターとして定義) から派生したポインターで、同じく restrict ポインターで、同じユニバースをポイントします。
まとめ
AI エンジン カーネルのプログラミングで restrict キーワード (__restrict) を正しく使用することにより、パフォーマンスが向上し、コードの未定義の動作を取り除くことができます。ただし、同じスコープに割り当てる場合、restrict ポインターによりデザインが未定義の動作になることがあるので注意してください。