Intel® FPGA SDK for OpenCL™: ベスト・プラクティス・ガイド

ID 683521
日付 12/08/2017
Public
ドキュメント目次

5.3. Single Work-Itemカーネルの良いデザイン方法

OpenCL™カーネルにループ構造が含まれている場合は、 インテル® で推奨されるのガイドラインに従って、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーが効果的に解析できるようにカーネルを構築してください。 適切に構造化されたループは、ループ内でパイプライン並列処理を実行するようにオフライン・コンパイラーに指示する場合に特に重要です。

ポインター・エイリアシングの回避

可能であれば、ポインター引数にrestrictキーワードを挿入します。ポインター引数にrestrictキーワードを含めることにより、オフライン・コンパイラーは、競合しない読み出し動作と書き込み動作の間に不要なメモリー依存関係を作成することを防止します。各反復があるアレイからデータを読み出し、同じ物理メモリー内の別のアレイにデータを書き込むループを検討してください。これらのポインター引数にrestrictキーワードを含めることなく、オフライン・コンパイラーは2つの配列間の依存関係を想定し、その結果としてパイプラインの並列性を少なくします。

「整形式ループの構築

「正常に形成されたループは、整数境界と比較され、反復ごとに1の単純な誘導インクリメントを有する出口条件がありますカーネルに "整形式"ループを含めると、オフライン・コンパイラーがこれらのループを効率的に解析できるため、パフォーマンスが向上します。

次の例は、「整形式ループです。

for (i = 0; i < N; i++) {
   //statements
}
重要: 「整形式ネストループは、カーネルのパフォーマンスを最大化するのにも役立ちます。

次の例は、「整形式ネストループ構造です。

for (i = 0; i < N; i++) {
   //statements
   for(j = 0; j < M; j++) {
      //statements
   }
}

ループ運搬依存関係の最小化

以下のループ構造は、各ループ反復が以前の反復によって書き込まれたデータを読み込むため、ループに依存する依存関係を作成します。その結果、前の反復からの書き込み動作が完了するまで、各読み出し動作を続行できません。ループに依存する依存関係が存在すると、オフライン・コンパイラーが達成できるパイプラインの並列性が低下し、カーネルのパフォーマンスが低下します。

for (int i = 0; i < N; i++) {
    A[i] = A[i - 1] + i;
}

オフライン・コンパイラーは、ループで静的なメモリー依存分析を実行して、ループが達成できる並列度を判断します。場合によっては、オフライン・コンパイラーが2つの配列アクセス間の依存関係を想定し、その結果としてパイプラインの並列性が低下することがあります。オフライン・コンパイラーは、未知の変数のためにコンパイル時に依存関係を解決できない場合、または配列アクセスが複雑なアドレッシングを伴う場合、ループに依存する依存関係を前提としています。

可能な場合、ループに依存する依存関係を最小限に抑えるため、以下のガイドラインに従ってください。

  • ポインター演算を回避します。

    カーネルが算術演算から導出されたポインター値を逆参照することによって配列にアクセスするとき、コンパイラーの出力は最適ではない。たとえば、次の方法で配列にアクセスしないでください。

    for (int i = 0; i < N; i++) {
        int t = *(A++);
        *A = t;
    }
  • 単純な配列インデックスを導入します。

    オフライン・コンパイラーがそれらを効率的に解析できないため、次のタイプの複雑な配列インデックスは回避してください。コンパイラー出力が最適ではない場合があります。

    • 配列インデックスの非定数。

      たとえば、 A [K + i]iはループインデックス変数、 Kは未知変数)。

    • 同じ添字の場所に複数の索引変数があります。

      たとえば、 A [i + 2×j]ijはダブルネストループのループインデックス変数)です。

      注: オフライン・コンパイラーは、インデックス変数が異なる下付き文字であるため、効率的に配列インデックスA [i] [j]を解析できます。
    • 非線形インデックス付け。

      たとえば、 A [i&C]iがループインデックス変数、 Cが定数または非定数変数です。

  • 可能であれば、カーネル内の一定の境界を持つループを使用してください。

    一定の境界を持つループにより、オフライン・コンパイラーは範囲分析を効果的に実行できます。

複雑なループ終了条件の回避

オフライン・コンパイラーは、終了条件を評価して、後続のループ反復がループパイプラインに入るかどうかを判断します。オフライン・コンパイラーが終了条件を評価するためにメモリーアクセスまたは複雑な動作を必要とすることがあります。このような場合、後続の反復は、評価が完了するまで開始できず、全体的なループのパフォーマンスが低下します。

ネステッド・ループから単一のループへの変換

パフォーマンスを最大にするには、ネステッド・ループを可能な限り単一のフォームに結合します。ネステッド・ループを単一のループに再構成することは、ループ反復間のハードウェア・フットプリントおよび計算オーバーヘッドを低減します。

次のコード例は、ネストされたループの単一ループへの変換を示しています。

ネストループ 変換シングルループ
for (i = 0; i < N; i++) {
    //statements
    for (j = 0; j < M; j++) {
        //statements          
    }
    //statements
} 
for (i = 0; i < N*M; i++) {
    //statements
}

可能な限り最も深いスコープ内の変数の宣言

変数の実装に必要なハードウェア・リソースを減らすには、変数をループで使用する前に宣言します。変数を使用しないループ全体で変数データを保存する必要がないため、できるだけ深いスコープで変数を宣言すると、データの依存関係やハードウェアの使用が最小限に抑えられます。

次の式を検討してみましょう。

int a[N];
for (int i = 0; i < m; ++i) {
    int b[N];
    for (int j = 0; j < n; ++j) {
        // statements
    }
}

配列aは、配列bよりも多くのリソースを実装する必要があります。ハードウェアの使用を減らすには、外側ループの反復によってデータを維持する必要がない限り、配列aを内側ループの外側に宣言します。

ヒント: 可能な限り最も深いスコープで変数のすべての値を上書きすると、変数を表示するのに必要なリソースも少なくなります。