ループのパイプライン処理により、ループの前の反復が終了する前に次の反復を開始でき、ループの一部が重複して実行されるようになります。デフォルトでは、ループの反復は前の反復が終了してから開始されます。たとえば次に示すループの例では、ループの 1 回の反復で 2 つの変数が加算され、結果が 3 つ目の変数に格納されます。ハードウェアでこのループの 1 回の反復を終了するのに 3 サイクルかかるとします。また、ループの変数 len
は 20 だとします (カーネルで vadd
ループが 20 回反復実行される)。このループのすべての演算を終了するには、合計 60 クロックサイクル (20 反復 * 3 サイクル) かかります。
vadd: for(int i = 0; i < len; i++) {
c[i] = a[i] + b[i];
}
vadd:…
)。このようにしておくと、Vitis HLS 環境でデザインをデバッグしやすくなります。未使用のラベルが原因でコンパイル中に警告メッセージが表示されますが、無視しても問題ありません。ループをパイプライン処理すると、ループの後続の反復が重複して同時実行されます。ループのパイプライン処理は、次のようにループ本体内に PIPELINE プラグマまたは指示子を追加することで可能になります。
vadd: for(int i = 0; i < len; i++) {
#pragma HLS PIPELINE
c[i] = a[i] + b[i];
}
syn.compile.pipeline_loops
コンフィギュレーション コマンドを使用します。ループの次の反復を開始するまでにかかるサイクル数は、パイプライン ループの開始間隔 (II) と呼ばれます。つまり、II=2 は、ループの次の反復が、現在の反復の 2 サイクル後に開始されることを意味します。II=1 (理想的) はループの各反復が各サイクルで開始されることを意味します。pragma HLS pipeline を使用すれば、コンパイラで達成する II を指定できます。目標とする II が指定されていない場合、コンパイラではデフォルトで II=1 を達成することが試みられます。
次の図は、パイプライン処理ありとなしのループの違いを示しています。この図の (A) はデフォルトの順次演算を示しています。各入力は 3 クロック サイクルごとに処理され (II=3)、最後の出力が書き出されるまでに 8 クロック サイクルかかっています。
(B) に示すパイプライン処理されたループでは、入力サンプルが各クロック サイクルで読み出され、最終的な出力は 4 クロック サイクル後に書き込まれるようになり、同じハードウェア リソースを使用して、開始間隔 (II) とレイテンシの両方を向上できます。
ループ内にデータ依存性がある場合、II=1 を達成できず、開始間隔が 1 より大きな値になることがあります。ループ依存性は、ループを最適化されないように (通常はパイプライン処理) するデータ依存性のことです。これらは、ループの 1 回の反復内またはループ内の異なる反復間にできます。ループ依存性を理解するには、極端な例を見てみるのがわかりやすいです。次の例では、ループの結果がそのループの継続/終了条件として使用されています。次のループを開始するには、前のループの各反復が終了する必要があります。
Minim_Loop: while (a != b) {
if (a > b)a -= b;
else b -= a;
}
上記コード例の Minim_Loop
ループは、ループの前の反復が終了するまで次の反復を開始できないため、パイプライン処理できません。すべてのループ依存性がこのように極端なわけではありませんが、ほかの演算が終了するまで開始できない演算があることに注意してください。ソリューションとしては、最初の演算ができるだけ早い段階で実行されるようにします。
ループ依存性はすべてのデータ型で発生する可能性はありますが、特に配列を使用する場合によく発生します。