フレーム ベースの C コード
フレーム ベースのコード形式の主な特徴は、各トランザクションで関数により複数のデータ サンプル (1 データ フレーム) が処理されることです。データ フレームは、ポインター演算を使用してアクセスされる、データを含む配列またはポインターとして供給されます。トランザクションは、C 関数の完全な実行 1 回と考えられます。このコード形式では、データは通常連続したループまたは入れ子のループで処理されます。
次に、フレーム ベースの C コード例を示します。
void foo(
data_t in1[HEIGHT][WIDTH],
data_t in2[HEIGHT][WIDTH],
data_t out[HEIGHT][WIDTH] {
Loop1: for(int i = 0; i < HEIGHT; i++) {
Loop2: for(int j = 0; j < WIDTH; j++) {
out[i][j] = in1[i][j] * in2[i][j];
Loop3: for(int k = 0; k < NUM_BITS; k++) {
. . . .
}
}
}
C/C++ コードをパイプライン処理してハードウェアでのパフォーマンスを向上する場合は、データのサンプルが処理されるレベルに PIPELINE 最適化指示子を配置します。
上記の例は、画像またはビデオ フレームを処理するのに使用されるコードで、ハードウェア関数を効率的にパイプライン処理する方法を示しています。2 つの入力セットがデータ フレームとして関数に供給され、出力もデータ フレームです。この関数をパイプライン処理できる箇所は、次のように複数あります。
- foo 関数のレベル
- Loop1 ループのレベル
- Loop2 ループのレベル
- Loop3 ループのレベル
これらの各箇所に PIPELINE 指示子を配置した場合の長所と短所を考慮することにより、コードのどこにパイプライン指示子を配置するのが最適なのかを判断するのに役立ちます。
関数レベル: 関数は、入力としてデータ フレーム (in1 および in2) を受信します。関数が II=1 (各クロック サイクルごとに新しい入力セットを読み込み) でパイプライン処理されると、in1 と in2 のすべての HEIGHT*WIDTH 値が 1 つのクロック サイクルで読み込まれるようコンパイルされます。これが意図するデザインであるという可能性は低いはずです。
PIPELINE 指示子を関数 foo に適用する場合、この階層の下位にあるループすべてを展開する必要があります。これは、パイプライン内に順序ロジックを存在させることはできないというパイプライン処理の要件です。このため、ロジックのコピーが HEIGHT*WIDTH*NUM_ELEMENT 個作成され、デザインが大きくなります。
データは順番にアクセスされるので、ハードウェア関数へのインターフェイスの配列は、次のような複数のハードウェア インターフェイスのタイプとしてインプリメントできます。
- ブロック RAM インターフェイス
- AXI4 インターフェイス
- AXI4-Lite インターフェイス
- AXI4 Stream インターフェイス
- FIFO インターフェイス
ブロック RAM インターフェイスは、クロックごとに 2 サンプルを供給可能なデュアル ポート インターフェイスとしてインプリメントできます。その他のインターフェイス タイプでは、クロックごとに 1 サンプルしか供給できないので、これがボトルネックになり、高度に並列化された大型ハードウェア デザインですべてのデータを並列処理できず、ハードウェア リソースが無駄に使用される結果となります。
Loop1 レベル: Loop1 内のロジックでは、2 次元行列の行全体が処理されます。ここに PIPELINE 指示子を配置すると、クロック サイクルごとに 1 行を処理するデザインが作成されます。これにより、これより下のループは展開されるので、追加のロジックが作成されます。ただし、この追加ロジックを利用するには、クロック サイクルごとにデータの行全体 (各ワードが WIDTH*<number of bits in data_t> ビット幅になる HEIGHT データ ワードの配列) を転送するのが唯一の方法です。
PS で実行されるホスト コードがこのような大型データ ワードを処理できることはまれなので、この場合も高度に並列化されたハードウェア リソースで、帯域幅の制限のため並列実行できないという結果になる可能性があります。
Loop2 レベル: Loop2 内のロジックでは、配列からの 1 サンプルが処理されます。画像アルゴリズムの場合は、これが 1 つのピクセルのレベルです。デザインでクロック サイクルごとに 1 サンプル処理される場合は、これがパイプライン処理を実行するレベルです。これは、インターフェイスが PS に入出力されるデータを消費して生成するレートでもあります。
これにより Loop3 が完全に展開されますが、クロックごとに 1 サンプル処理されるようになります。Loop3 の演算がすべて並列処理されることが要件です。典型的なデザインでは、Loop3 のロジックはシフト レジスタで、1 ワード内のビットを処理します。クロックごとに 1 サンプル処理されるようにするには、ループを展開してこれらの処理が並列実行されるようにしてください。Loop2 をパイプライン処理することにより作成されるハードウェア関数では、クロックごとに 1 データ サンプル処理され、必要なデータ スループットを達成するのに必要な場合にのみ並列ロジックが作成されます。
Loop3 レベル: 前述のように Loop2 が各データ サンプルまたはピクセルを演算をする場合、Loop3 のロジックでは通常ビット レベル タスクまたはデータ シフト タスクが実行されるので、このレベルではピクセルごとに複数の演算が実行されます。このレベルでパイプライン処理を実行すると、クロックごとにこのループの各演算が 1 回実行されるので、ピクセルごとに NUM_BITS クロックとなり、各ピクセルまたはデータ サンプルが複数クロックのレートで処理されます。
たとえば、Loop3 にウィンドウィングまたはたたみ込みアルゴリズム用に前のピクセルを保持するシフト レジスタが含まれているとします。このレベルに PIPELINE 指示子を追加すると、各クロック サイクルでデータ値が 1 つシフトされるようになります。デザインは Loop2 のロジックに戻って NUM_BITS 回の繰り返し後に次の入力を読み込むので、データ処理レートはかなり遅くなります。
この例の場合、パイプライン処理する理想的な箇所は Loop2 です。
フレーム ベースのコードの場合、ループ レベルでパイプライン処理し、通常はサンプルのレベルで演算を実行するループをパイプライン処理することをお勧めします。確信がない場合は、C コードに print コマンドを記述し、これが各クロック サイクルで実行するレベルであるかどうかを確認してください。
上記の例では入れ子のループのセットが 1 つしかありませんが、階層の同レベルに複数のループがある場合は、ループごとに PIPELINE 指示子を配置するのに最適な場所を決定してから、DATAFLOW 指示子を関数に適用して、各ループが同時に実行されるようにします。