配列アクセスとパフォーマンス - 2023.2 日本語

Vitis 高位合成ユーザー ガイド (UG1399)

Document ID
UG1399
Release Date
2023-12-18
Version
2023.2 日本語

前のセクションでは、並列処理を探る手段として、ループ展開やパイプライン処理といった最適化の概念を紹介しました。ここでは、配列がレジスタではなくメモリにマップされている場合に配列のアクセス パターンによって最適化が妨げられる可能性があることを考慮していませんでした。メモリにマップされた配列は、デザインのパフォーマンスに影響を与えるボトルネックになる可能性があります。Vitis HLS では、配列の再形成や配列の分割など、こういったメモリのボトルネックを解消するための最適化を多数提供しています。こういった自動メモリ最適化機能を可能な限り使用し、コード変更の回数を最小限にする必要があります。一方で、メモリ アーキテクチャを明示的にコード化することが、パフォーマンスを満たすために必要であったり、設計者がより優れた QoR (結果の品質) を得ることができる可能性もあります。このような場合、配列へのアクセスは、パフォーマンスを制限しないようにコード化することが重要です。つまり、配列のアクセス パターンを解析し、目標とするスループットとエリアを達成できるようにデザインのメモリを編成する必要があります。次のコード例では、配列へのアクセスにより最終 RTL デザインでパフォーマンスが制限されます。この例では、配列 mem[N] に 3 回アクセスして合計を作成しています。このコード例のフル バージョンは、 Vitis-HLS-Introductory-Examples/Interface/Memory/memory_bottleneck を参照してください。

#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 には、配列のインプリメントおよびアクセス方法を変更できる最適化指示子が多くあります。最適化には次の 2 つのクラスがあります。

  • Array Partition では、元となる配列をより小さな配列に分割したり、個別のレジスタに分割したりできます。
  • Array Reshape では、配列を別のメモリ配置に編成して並列処理を向上させますが、元となる配列は分割されません。