阵列访问和性能 - 2021.2 Chinese

Vitis 高层次综合用户指南 (UG1399)

Document ID
UG1399
Release Date
2021-12-15
Version
2021.2 Chinese

以下代码示例显示了访问阵列导致最终 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 循环进行流水打拍来实现在每个时钟周期内处理新的循环迭代的目标。

尝试以值为 1 的启动时间间隔对 SUM_LOOP 进行流水打拍将生成如下消息(无法达成吞吐量 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 次读取(或 1 次写入)。

  • SUM_LOOP Cycle1:读取 mem[i]
  • SUM_LOOP Cycle2:读取 mem[i-1],对值求和;
  • SUM_LOOP Cycle3:读取 mem[i-2],对值求和;

可使用双端口 RAM,但这样仅支持每个时钟周期 2 次访问。要计算总和值,需读取 3 次,因此要将循环流水打拍,每个时钟周期需访问 3 次,即每个时钟周期内新增 1 次迭代。

警告:
作为存储器或存储器端口实现的阵列通常会成为性能瓶颈。

以上示例中的代码可按以下代码示例中所示方式重写,以便按吞吐量为 1 来对代码进行流水打拍。在以下代码示例中,通过执行预读取并手动对数据访问进行流水打拍,即可在循环每次迭代中仅指定 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 将阵列分区到各独立元素中。这可使用自动分区配置设置来加以控制。

将阵列分区到多个块中时,单一阵列将作为多个 RTL RAM 块来实现。分区到元素中时,每个元素都作为 RTL 中的 1 个寄存器来实现。在这 2 种情况下,分区允许并行访问更多元素,并且有助于提升性能;设计将在性能与实现性能所需的 RAM 或寄存器数量之间加以权衡取舍。