配列は、C++ のソフトウェア プログラムにおいて基本的なデータ構造です。ソフトウェア プログラマは、配列を単なるコンテナーとして捉え、必要に応じて (多くの場合、動的に) 配列の割り当て/割り当て解除を実行します。このような動的な配列の割り当ては、同じプログラムをハードウェア用に合成する必要がある場合はサポートされません。配列をハードウェアに合成する場合、アルゴリズムに (静的に) 必要なメモリの容量を正確に把握することが必要です。さらに、FPGA 上のメモリ アーキテクチャ (ローカル メモリとも呼ばれる) は、DDR メモリや HBM メモリ バンクに代表されるグローバル メモリと比較すると、トレードオフが大きく異なります。グローバル メモリへのアクセスはレイテンシ コストが高くなり、何サイクルもかかることがありますが、ローカル メモリへのアクセスは多くの場合、高速で 1 サイクルまたは数サイクルで済みます。
HLS デザインが適切にパイプライン処理および/または展開されると、メモリ アクセス パターンが確立されます。HLS コンパイラでは、配列をさまざまなタイプのリソースにマップできます。この際、配列要素は、ハンドシェイク信号の有無にかかわらず、並行して利用可能です。内部配列および最上位関数のインターフェイスの配列は、いずれもレジスタまたはメモリにマップできます。配列が最上位インターフェイスにある場合、ツールで外部メモリとの接続に必要なアドレス信号、データ信号、および制御信号が作成されます。配列がデザイン内部にある場合、ツールではメモリにアクセスするために必要なアドレス信号、データ信号、および制御信号が作成されるのに加え、メモリ モデルがインスタンシエートされます (その後、ダウンストリームの RTL 合成ツールでメモリとして推論される)。
配列は、通常合成後にメモリ (RAM、ROM、またはシフト レジスタ) としてインプリメントされます。また、プラットフォームに十分なレジスタがある場合、配列を個々のレジスタに完全に分割して、完全かつ並列にインプリメントすることも可能です。GitHub で公開されている initialization_and_reset の例では、メモリのさまざまなインプリメンテーションが示されています。
最上位関数インターフェイスの配列は外部メモリにアクセスする RTL ポートとして合成されます。デザインに対して内部にあるサイズが 1024 未満の配列は、シフト レジスタとして合成されます。サイズが 1024 を超える配列は、最適化設定によってブロック RAM (BRAM)、UltraRAM (URAM) に合成されます (BIND_STORAGE 指示子/プラグマを参照)。
次のコードが使用されている場合、HLS コンパイラでシフト レジスタが推論される例を考慮する必要があります。
int A[N]; // This will be replaced by a shift register
for(...) {
// The loop below is the shift operation
for (int i = 0; i < N-1; ++i)
A[i] = A[i+1];
A[N] = ...;
// This is an access to the shift register
... A[x] ...
}
シフト レジスタはサイクルごとに 1 つのシフト操作が可能で、シフト レジスタのどこででもサイクルごとにランダムな読み出しアクセスを実行できるので、FIFO よりも柔軟性があります。
配列により RTL で問題となるのは、次のような場合です。
- メモリ (BRAM/LUTRAM/URAM) としてインプリメントされると、メモリ ポートの数によりデータへのアクセスが制限され、パイプライン ループで II 違反が発生する可能性があります。
- HLS コンパイラでは、相互排他的アクセスが正しく推論されない場合があります。
- 読み出しアクセスのみを必要とする配列は、RTL では ROM としてインプリメントされるようにする必要があります。
HLS コンパイラでは、ポインターの配列がサポートされます。各ポインターは、スカラーまたはスカラーの配列のみを指定できます。
Array[10];
)。サイズは C++ コンパイラでは無視されますが、ツールでは使用されます。Array[];
のようにサイズ指定のない配列はサポートされません。