概要
HLS(高位合成)では、C/C++風の記述が可能であるにもかかわらず、以下が原則として禁止、または制限されています。
-
malloc/newなどの動的メモリ確保 - 再帰呼び出し
一般的なソフト屋からすると、これは不自然に見えるでしょう。
ここでは、これに対して**「実装できないから」ではなく「回路として定義できないから」**という部分をなるべくわかりやすく説明します。
前提: HLSは回路生成
まず最も重要な前提を確認しましょう。
C/C++(ソフト)の世界
- 時間方向に逐次実行
- 実行時に状態が決まる
- メモリ・スタック・ヒープが存在する
HLS C/C++の世界
- 回路構造を静的に生成する
- 実行前にリソースが確定する
- クロックごとの動作が定義される
HLSは「コードを実行」ではなく「コードから回路生成」
この違いが、動的メモリと再帰呼び出しが禁止される理由の本質となっています。
なぜ動的メモリ確保がダメなのか
ソフトウェアにおける動的メモリ
int *p = malloc(sizeof(int) * n);
-
nは実行時に確定 - 必要なメモリ量も実行時に確定
- OSやランタイムがヒープ管理を行う
ハードウェアにおいて
- メモは配置時にサイズが確定
- アドレス幅・ポート数も合成時に固定
- ヒープ管理回路は自動生成されない
つまり、「何バイト必要かわからないメモリ」を回路として配置することができないといえます。
技術的に破綻する点
1. メモリサイズが決められない
malloc(n);
合成時に決定できない。
2. メモリ管理回路が必要
- 空き領域管理
- フラグメンテーション対策
-
freeリスト管理
HLSが暗黙に生成しない。
HLSは使うメモリ量が最初から分かっている設計が必要
なぜ再帰呼び出しがダメなのか
ソフトウェア
int fact(int n) {
if (n == 0) return 1;
return n * fact(n - 1);
}
- 関数呼び出しごとにスタックフレームを積む
- 深さは実行時に確定
- スタックは可変
ハードウェア
基本的にスタックがない。
- 関数呼び出し = 回路の接続
- 再帰 = 回路が自分自身を呼ぶ構造
これにより深さが分からない。
また、 unrollやパイプラインかを行えないため、不可能になります。
再帰は「動的構造」である
再帰は本質的に実行時に構造が成長するアルゴリズムです。
HLSが扱えるのは合成時に構造が固定される回路なので、不可能になります。
動的メモリと再帰の共通点
| 特徴 | 動的メモリ | 再帰 |
|---|---|---|
| 実行時にサイズが決まる | ○ | ○ |
| OS/ランタイム依存 | ○ | ○ |
| スタック/ヒープが必要 | ○ | ○ |
| 回路サイズが不定 | ○ | ○ |
実行時にサイズが変わってしまうのが欠点。
HLSが要求する設計哲学
- サイズが固定
- 最大値が明示
- 実行パスが静的
良い例
#define MAX 1024
for (int i = 0; i < MAX; i++) {
if (i < n) {
// 処理
}
}
- 最悪ケースが決まっている
- 回路として配置可能
実践的な対処法
- 動的メモリの場合は固定の配列
- 再帰の場合はループのサイズを固定化するなど
例外:制限付き機能
一部のHLSツールでは、制限付きの再帰(ループに自動変換)や動的メモリ(最大値を固定した)が実装されています。
静的設計
静的に固定された設計は、以下の最適化が可能です。
- 完全な並列化: ループ展開により複数演算を同時実行
- パイプライン化: データフローを最適にスケジューリング
- メモリアクセス最適化: 競合のない並列メモリアクセス
- リソース配分の最適化: 必要な演算器を過不足なく配置
動的な構造ではこれらの静的解析・最適化が不可能になります。
まとめ
動的メモリと再帰が禁止される理由は回路として定義できないからといえます。
配列サイズの固定やループの深さを固定にする必要があることでHLSの設計を理解し、より効率的なハードウェア設計が可能になります。