前の例が合成されると、レイテンシはシーケンシャル ループの例の半分になります。これは、ループが関数として並 列に実行できるようになったからです。
シーケンシャル ループの例には、dataflow 最適化も使用できます。ここに示す並列処理のため、関数にループを取 り込む方法は、dataflow 最適化が使用できない場合に使用します。たとえば、より大型のデザイン例では、
dataflow 最適化を最上位のすべてのループ/関数、および各最上位ループおよび関数間に配置されたメモリに適用 できます。
ループ依存性
ループ依存性は、ループを最適化されないように (通常はパイプライン処理) するデータ依存性のことです。これら は、ループの 1 回の反復内またはループ内の異なる反復間にできます。
ループ依存性を理解するには、極端な例を見てみるのがわかりやすいです。次の例では、ループの結果がそのループ の継続/終了条件として使用されています。次のループを開始するには、前のループの各反復が終了する必要がありま す。
Minim_Loop: while (a != b) { if (a > b)
a -= b;
else b -= a;
}
このループはパイプライン処理できません。ループの前の反復が終了するまで次の反復を開始できないからです。す べてのループ依存性がこのように極端なわけではありませんが、ほかの演算が終了するまで開始できない演算がある ことに注意してください。ソリューションとしては、最初の演算ができるだけ早い段階で実行されるようにします。
ループ依存性はすべてのデータ型で発生する可能性はありますが、特に配列を使用する場合によく発生します。
if (k > 0)
shift[k] = shift[k-1];
else
shift[k] = data;
}
*dataOut = shift_output;
shift_output = shift[N-1];
}
*pcout = mac.exec1(shift[4*col], coeff, pcin);
};
UNROLL プラグマ指示子で指定したように Vitis™ HLS でループを展開できるようにするには、コードを記述し直し て k をクラス メンバーからはずす必要があります。
template <typename T0, typename T1, typename T2, typename T3, int N>
class foo_class { private:
pe_mac<T0, T1, T2> mac;
public:
T0 areg;
T0 breg;
T2 mreg;
T1 preg;
T0 shift[N];
T0 shift_output;
void exec(T1 *pcout, T0 *dataOut, T1 pcin, T3 coeff, T0 data, int col) {Function_label0:;
int k; // Local variable
#pragma HLS inline off
SRL:for (k = N-1; k >= 0; --k) {
#pragma HLS unroll // Loop will unroll if (k > 0)
shift[k] = shift[k-1];
else
shift[k] = data;
}
*dataOut = shift_output;
shift_output = shift[N-1];
}
*pcout = mac.exec1(shift[4*col], coeff, pcin);
};
配列
コーディング スタイルによって合成後の配列のインプリメンテーションがどのように変わるかについて説明する前 に、C シミュレーションなど合成が実行される前に発生する可能性のある問題について説明します。
次のようにかなり大きな配列が指定される場合は、C シミュレーションでメモリ不足によりエラーになる可能性があ ります。
#include "ap_cint.h"
int i, acc;
// Use an arbitrary precision type int32 la0[10000000], la1[10000000];
for (i=0 ; i < 10000000; i++) { acc = acc + la0[i] + la1[i];
}
シミュレーションはメモリ不足のためエラーになることがあります。これは、配列がメモリに存在するスタックに配 置され、OS で管理されローカル ディスクを使用可能なヒープには配置されないためです。
つまり、デザインを実行したときにメモリ不足になり、次のような状況によって問題が悪化することもあります。
• PC では使用可能なメモリが大型の Linux ボックスよりも少ないことがよくあり、使用可能なメモリが少ないこと があります。
• 任意精度型を使用すると、標準 C 型よりも多くのメモリが必要になるので、この問題が悪化する可能性がありま す。
• C++ のより複雑な固定小数点任意精度型を使用すると、さらに多くのメモリが必要となるので、メモリ不足にな る可能性があります。
C/C++ コード開発のメモリ リソースを向上するには、リンカー オプションを使用してスタックのサイズを増加する のが標準的な方法です。たとえば、-Wl,--stack,10485760 のようにスタック サイズを明示的に設定します。
Vitis™ HLS でこれを適用するには、[Project Settings] → [Simulation] → [Linker flags] をクリックするか、次のように Tcl コマンドのオプションとして指定します。
csim_design -ldflags {-Wl,--stack,10485760}
cosim_design -ldflags {-Wl,--stack,10485760}
マシンに十分なメモリがない場合は、スタック サイズを増加しても効果はありません。
次の例のように、シミュレーションにはダイナミック メモリ割り当てを、合成には固定サイズの配列を使用して、問 題を回避してください。この場合、こ必要なメモリはヒープに割り当てられ、OS で管理されるので、ローカル ディ スク空間が使用できるようになります。
このようなコードへの変更は、シミュレーションされるコードと合成されるコードが異なってしまうため、理想的で はありませんが、デザインを実行するにはこれしか方法がない場合があります。これを実行した場合は、C テストベ ンチでこの配列にアクセスするすべての 点が記述されるようにしてください。これにより、cosim_design で実行 される RTL シミュレーションでメモリ アクセスが正しいかどうかが検証されるようになります。
#include "ap_cint.h"
int i, acc;
#ifdef __SYNTHESIS__
// Use an arbitrary precision type & array for synthesis int32 la0[10000000], la1[10000000];
#else
// Use an arbitrary precision type & dynamic memory for simulation int32 *la0 = malloc(10000000 * sizeof(int32));
int32 *la1 = malloc(10000000 * sizeof(int32));
#endif
for (i=0 ; i < 10000000; i++) { acc = acc + la0[i] + la1[i];
}
注記: __SYNTHESIS__ マクロは、合成されるコードにのみ使用します。このマクロは C シミュレーションまたは C RTL 協調シミュレーションには従っていないので、テストベンチには使用しないでください。
配列は、通常合成後にメモリ (RAM、ROM、または FIFO) としてインプリメントされます。最上位関数インターフェ イスの配列はメモリ外部にアクセスする RTL ポートとして合成されます。デザインに対して内部にある 1024 未満 の配列は、SRL に最適化されます。1024 を超える配列は、最適化設定によって内部ブロック RAM、LUTRAM、
UltraRAM に合成されます。
ループと同様、配列も簡単なコード構文なので、C プログラムでよく使用されます。また、ループのように、Vitis HLS には多くの最適化が含まれ、コードを修正しなくても RTL のインプリメンテーションを最適化するための指示子が含 まれます。
配列により RTL で問題となるのは、次のような場合です。
• 配列アクセスがパフォーマンスの障害となってしまうことがよくあります。メモリとしてインプリメントされる と、メモリ ポートの数によりデータへのアクセスが制限されます。配列初期化は、注意して実行しないと、RTL でのリセットおよび初期化が不必要に長くなってしまうことがあります。
• 読み出しアクセスのみを必要とする配列は、RTL では ROM としてインプリメントされるようにする必要がありま す。
Vitis HLS では、ポインター配列がサポートされます。各ポインターは、スカラーまたはスカラーの配列のみを指定で きます。
注記: 配列はサイズ指定する必要があります。たとえば、Array[10]; のようなサイズ指定された配列はサポートさ れますが、Array[]; のようにサイズ指定のない配列はサポートされません。
配列アクセスとパフォーマンス
次のコード例では、配列へのアクセスにより最終 RTL デザインでパフォーマンスが制限されます。この例では、配列 mem[N] に 3 回アクセスして合計を作成しています。
#include "array_mem_bottleneck.h"
dout_t array_mem_bottleneck(din_t mem[N]) { dout_t sum=0;
int i;
SUM_LOOP:for(i=2;i<N;++i)
sum += mem[i] + mem[i-1] + mem[i-2];
return sum;
}
合成中、配列は RAM としてインプリメントされます。RAM をシングル ポート RAM として指定すると、クロック サ イクルごとに新しいループ反復を処理するように SUM_LOOP ループをパイプライン処理することは不可能です。
SUM_LOOP を開始間隔 1 でパイプライン処理しようとすると、次のようなメッセージが表示されます。スループット 1 を達成できなかったため、Vitis HLS により制約が緩和されます。
INFO: [SCHED 61] Pipelining loop 'SUM_LOOP'.
WARNING: [SCHED 69] Unable to schedule 'load' operation ('mem_load_2', bottleneck.c:62) on array 'mem' due to limited memory ports.
INFO: [SCHED 61] Pipelining result: Target II: 1, Final II: 2, Depth: 3.
ここでの問題は、シングル ポート RAM にはシングル データ ポートしかないので、各クロック サイクルで 1 つの読 み込み (および 1 つの書き出し) が実行できる点にあります。
• SUM_LOOP サイクル 1: mem[i] を読み出し
• SUM_LOOP サイクル 2: mem[i-1] を読み出して値を合計
• SUM_LOOP サイクル 3: mem[i-2] を読み出して値を合計
デュアル ポート RAM も使用できますが、クロック サイクルごとに 2 つのアクセスしか許容されません。合計値を計 算するのに 3 つの読み出しが必要なので、クロックサイクルごとに新しい反復でループをパイプライン処理するため には、クロック サイクルごとに 3 つのアクセスが必要になります。
注意: メモリまたはメモリ ポートとしてインプリメントされる配列は、パフォーマンスの障害となることがよくあり ます。
上記の例のコードをスループット 1 でパイプラインができるように変更すると、次のコードになります。次のコード 例では、先行読み出しを実行し、データ アクセスを手動でパイプライン処理することで、ループの各反復で指定され る配列読み出しが 1 回だけになっています。これにより、パフォーマンスを達成するためには、シングル ポート RAM だけが必要となります。
#include "array_mem_perform.h"
dout_t array_mem_perform(din_t mem[N]) { din_t tmp0, tmp1, tmp2;
dout_t sum=0;
int i;
tmp0 = mem[0];
tmp1 = mem[1];
SUM_LOOP:for (i = 2; i < N; i++) { tmp2 = mem[i];
sum += tmp2 + tmp1 + tmp0;
tmp0 = tmp1;
tmp1 = tmp2;
}
return sum;
}
Vitis HLS には、配列のインプリメントおよびアクセス方法を変更できる最適化指示子が多くあります。通常、このよ うな指示子が使用される場合、コードを変更する必要はありません。配列は、ブロックまたは個々の要素に分割でき ます。Vitis HLS で配列が個々の要素に分割されることもあります。これは、自動分割のコンフィギュレーション設定 を使用すると指定できます。