流水操作
pragma HLS pipeline
说明
PIPELINE pragma 通过允许并发执行操作来减少函数或循环的启动间隔。
流水线函数或循环可以每 N 个时钟周期处理新输入,其中 N 是循环或函数的启动间隔 (II)。 PIPELINE pragma 的默认启动间隔为 1,它在每个时钟周期处理一个新输入。 您还可以通过使用编译指示的 II 选项来指定启动间隔。
流水线化循环允许循环的操作以并发方式实现,如下图所示。 在该图中,(A) 显示了默认顺序操作,其中每次输入读取之间有 3 个时钟周期 (II=3),并且在执行最后一次输出写入之前需要 8 个时钟周期。
如果 Vivado HLS 无法使用指定的 II 创建设计,它会:
- 发出警告。
- 创建具有尽可能低的 II 的设计。
然后,您可以使用警告消息分析此设计,以确定必须采取哪些步骤来创建满足所需启动间隔的设计。
语法
将pragma放在函数或循环体的C源代码中。
#pragma HLS pipeline II=<int> enable_flush rewind
其中:
II=<int>
:指定流水操作的所需启动间隔。Vivado HLS试图满足此要求。基于数据依赖关系,实际结果可能具有更大的启动间隔。默认值II为1。enable_flush
:一个可选关键字,用于实现流水,如果流水输入的有效数据变为非活动状态,则该流水线将刷新并清空。此功能仅支持流水线函数:流水线循环不支持此功能。- rewind:一个可选关键字,用于启用倒带或连续循环流水,在一个循环迭代结束和下一个迭代开始之间不暂停。只有在顶级函数中有一个单循环(或完美的循环嵌套)时,rewind才有效。此功能仅支持流水线循环:流水线函数不支持此功能。
循环前的代码段:
- 被视为初始化。
- 在流水中只执行一次。
- 不能包含任何条件操作(if-else)
示例
此示例函数foo以1的启动间隔进行流水化:
void foo { a, b, c, d} { #pragma HLS pipeline II=1 ... }
II的默认值为1,因此本例中不需要特别设置II=1。
pragma HLS occurrence
说明
当流水操作函数或循环时,OCCURRENCE pragma指定区域中的代码执行频率低于封闭函数或循环中的代码。这允许执行频率较低的代码以较慢的速度进行流水线化,并可能在顶级流水操作中共享。要确定OCCURRENCE情况,请执行以下操作:
- 一个循环迭代N次。但是,循环体的一部分由条件语句启用,因此只执行M次,其中N是M的整数倍。
- 条件代码的出现速度比循环体的其余部分慢N/M倍。
例如,在执行10次的循环中,循环中的条件语句仅执行2次,occurrence次数为5(或10/2)。使用occurrence指令标识区域,可以使该区域中的函数和循环以比封闭函数或循环慢的更高启动间隔进行流水化操作。
语法
将pragma放在C源代码中的代码区域内。
#pragma HLS occurrence cycle=<int>
其中,cycle=<int>
:指定occurrence 次数 N/M。N是执行封闭函数或循环的次数。M是执行条件区域的次数。N必须是M的整数倍。
示例
在此示例中,区域Cond_Region的occurrence 次数为4(它的执行频率比包含它的周围代码的执行频率低四倍):
Cond_Region: { #pragma HLS occurrence cycle=4 ... }
循环展开
pragma HLS unroll
说明
展开循环以创建多个独立操作而不是单个操作集合。UNROLL 编译指示通过在 RTL 设计中创建循环体的多个副本来转换循环,这允许并行发生一些或所有循环迭代。
C/C++ 函数中的循环默认保持滚动。 当循环滚动时,综合为循环的一次迭代创建逻辑,RTL 设计按顺序为循环的每次迭代执行此逻辑。 循环执行循环归纳变量指定的迭代次数。 迭代次数也可能受循环体内部逻辑的影响(例如,中断条件或对循环出口变量的修改)。 使用 UNROLL 编译指示,您可以展开循环以增加数据访问和吞吐量。
UNROLL 编译指示允许完全或部分展开循环。 完全展开循环会为每次循环迭代在 RTL 中创建循环体的副本,因此可以同时运行整个循环。 部分展开循环可让您指定因子 N,以创建循环体的 N 个副本并相应地减少循环迭代。 要完全展开循环,必须在编译时知道循环边界。 这不是部分展开所必需的。
部分循环展开不需要 N 是最大循环迭代计数的整数因子。 Vivado HLS 添加了退出检查以确保部分展开的循环在功能上与原始循环相同。 例如,给定以下代码:
for(int i = 0; i < X; i++) { pragma HLS unroll factor=2 a[i] = b[i] + c[i]; }
循环展开 2 倍有效地将代码转换为如下代码,其中使用 break 构造确保功能保持不变,并且循环在适当的点退出:
for(int i = 0; i < X; i += 2) { a[i] = b[i] + c[i]; if (i+1 >= X) break; a[i+1] = b[i+1] + c[i+1]; }
由于最大迭代次数 X 是一个变量,Vivado HLS 可能无法确定其值,因此会向部分展开的循环添加退出检查和控制逻辑。 但是,如果知道指定的展开因子(在此示例中为 2)是最大迭代次数 X 的整数因子,则skip_exit_check 选项可让您删除退出检查和相关逻辑。 这有助于最小化面积并简化控制逻辑。
当使用 DATA_PACK、ARRAY_PARTITION 或 ARRAY_RESHAPE 等编译指示让更多数据在单个时钟周期内访问时,Vivado HLS 会自动展开任何消耗此数据的循环,如果这样做可以提高吞吐量。 该循环可以完全或部分展开,以创建足够的硬件以在单个时钟周期内消耗额外的数据。 此功能是使用 config_unroll 命令控制的。
语法
将编译指示放在循环体中的 C/C++ 源代码中以展开。
#pragma HLS unroll factor=<N> region skip_exit_check
其中:
factor=<N>
:指定一个非零整数,表示请求部分展开。 循环体重复指定的次数,并相应调整迭代信息。 如果未指定 factor=,则循环完全展开。region
:一个可选关键字,用于展开指定循环主体(区域)内的所有循环,而不展开封闭循环本身。skip_exit_check
:一个可选关键字,仅在使用factor= 指定部分展开 时才适用。 退出检查的消除取决于循环迭代计数是已知还是未知:- ○ 固定(已知)边界:如果迭代计数是因子的倍数,则不执行退出条件检查。 如果迭代计数不是因子的整数倍,则该工具:
- 防止展开。
- 发出警告,必须执行退出检查才能继续。
○ 变量(未知)边界:根据要求移除退出条件检查。 必须确保:
- 变量bounds是指定展开因子的整数倍。
- 实际上不需要退出检查。
示例
示例1
以下示例完全展开函数 foo 中的 loop_1。 将编译指示放在 loop_1 的主体中,如下所示:
loop_1: for(int i = 0; i < N; i++) { #pragma HLS unroll a[i] = b[i] + c[i]; }
示例2
此示例指定展开因子 4 以部分展开函数 foo 的 loop_2,并删除退出检查:
void foo (...) { int8 array1[M]; int12 array2[N]; ... loop_2: for(i=0;i<M;i++) { #pragma HLS unroll skip_exit_check factor=4 array1[i] = ...; array2[i] = ...; ... } ... }
示例3
下面的示例完全展开函数 foo 中 loop_1 内的所有循环,但由于 region 关键字的存在而不是 loop_1 本身:
void foo(int data_in[N], int scale, int data_out1[N], int data_out2[N]) { int temp1[N]; loop_1: for(int i = 0; i < N; i++) { #pragma HLS unroll region temp1[i] = data_in[i] * scale; loop_2: for(int j = 0; j < N; j++) { data_out1[j] = temp1[j] * 123; } loop_3: for(int k = 0; k < N; k++) { data_out2[k] = temp1[k] * 456; } } }
pragma HLS dependence
说明
DEPENDENCE 编译指示用于提供附加信息,这些信息可以克服循环携带依赖性并允许循环流水线化(或以较低的间隔流水线化)。
Vivado HLS 自动检测依赖性:
- 循环内(循环独立依赖性)。
- 循环的不同迭代之间(循环进位依赖性)。
这些依赖关系会影响何时可以调度操作,尤其是在函数和循环流水线期间。
- 循环独立依赖:在同一个循环迭代中访问相同的元素。
for (i=0;i<N;i++){ A[i]=x; y=A[i]; }
循环进位依赖:在不同的循环迭代中访问相同的元素。
for (i=0;i<N;i++) { A[i]=A[i-1]*2; }
在某些复杂的场景下,自动依赖分析可能过于保守,无法过滤掉错误的依赖。 在某些情况下,例如变量依赖数组索引,或者需要强制执行外部要求时(例如,两个输入永远不会是同一个索引),依赖分析可能过于保守。 DEPENDENCE pragma 允许您显式指定依赖项并解决错误的依赖项。
指定错误的依赖项,而实际上依赖项不是错误的,可能会导致硬件不正确。 在指定依赖项之前,请确保依赖项是正确的(true 或 false)。
语法
将 pragma 放在定义依赖关系的函数的边界内。
#pragma HLS dependence variable=<variable> <class> <type> <direction> distance=<int> <dependent>
其中:
variable=<variable>
:(可选)指定要考虑依赖关系的变量。<class>
:可选地指定依赖关系需要澄清的变量类。有效值包括数组或指针。<class>
和 variable= 不需要一起指定,因为您可以在函数内指定一个变量或一组变量。<type>
:有效值包括 intra或 inter。 指定依赖是否为:- ○ intra:同一循环迭代内的依赖。 当依赖 dependency <type> 被指定为intra,并且 <dependent> 为 false 时,Vivado HLS 可以在循环内自由移动操作,增加它们的移动性并潜在地提高性能或面积。 当 <dependent> 指定为 true 时,必须按照指定的顺序执行操作。
- ○ inter:不同循环迭代之间的依赖关系。 这是默认的 <type>。 如果将dependency <type> 指定为inter,并且<dependent> 为false,则在函数或循环已流水线化、循环展开或部分展开时,它允许Vivado HLS 并行执行操作,并在以下情况下阻止此类并发操作 <dependent> 被指定为 true。
<direction>
:有效值包括 RAW、WAR 或 WAW。 这仅与循环进位相关性相关,并指定相关性的方向:
○ RAW(写后读 - 真实相关性) 写指令使用读指令使用的值。
○ WAR(Write-After-Read - 反依赖) 读指令得到一个被写指令覆盖的值。
○ WAW(Write-After-Write - 输出依赖) 两条写入指令以一定的顺序写入同一个位置。
distance=<int>:指定数组访问的迭代间距离。 仅与循环进位依赖关系相关,其中依赖关系设置为 true。
<dependent>:指定是否需要强制执行 (true) 或删除 (false) 依赖项。 默认值为true。
示例
示例1
在以下示例中,Vivado HLS 不了解 cols 的值,并且保守地假设写入 buff_A[1] [col] 和读取 buff_A[1][col] 之间始终存在依赖关系 . 在这样的算法中,cols 不太可能为零,但 Vivado HLS 无法对数据依赖性做出假设。 为了克服这个缺陷,您可以使用 DEPENDENCE 编译指示声明循环迭代之间没有依赖关系(在这种情况下,对于 buff_A 和 buff_B)。
void foo(int rows, int cols, ...) for (row = 0; row < rows + 1; row++) { for (col = 0; col < cols + 1; col++) { #pragma HLS PIPELINE II=1 #pragma HLS dependence variable=buff_A inter false #pragma HLS dependence variable=buff_B inter false if (col < cols) { buff_A[2][col] = buff_A[1][col]; // read from buff_A[1][col] buff_A[1][col] = buff_A[0][col]; // write to buff_A[1][col] buff_B[1][col] = buff_B[0][col]; temp = buff_A[0][col]; }
示例2
删除函数 foo 中 loop_1 的相同迭代中 Var1 之间的依赖关系。
#pragma HLS dependence variable=Var1 intra false
示例3
在函数 foo 的 loop_2 中定义对所有数组的依赖,以通知 Vivado HLS 所有读取必须在同一循环迭代中的写入 (RAW) 之后发生。
#pragma HLS dependence array intra RAW true
循环优化
pragma HLS loop_flatten
说明
允许将嵌套循环展平为单个循环层次结构,从而提高延迟。在RTL实现中,需要一个时钟周期才能从外循环移动到内循环和从内循环移动到外循环。展平嵌套循环可以将其作为单个循环进行优化。这节省了时钟周期,可能允许对循环体逻辑进行更大的优化。
将LOOP_FLATTEN应用于循环层次结构中最内层循环的循环体。只有完美和半完美循环可以用这种方式展平:
- 完美的循环:
○ 只有最里面的循环具有循环体内容。
○ 循环语句之间没有指定逻辑。
○ 所有循环边界都是常量。 - 半完美环嵌套:
○ 只有最里面的循环具有循环体内容。
○ 循环语句之间没有指定逻辑。
○ 最外层的循环边界可以是变量。 - 不完美的循环嵌套:当内部循环具有可变边界(或循环体不完全位于内部循环内)时,尝试重新构造代码,或在循环体中展开循环以创建完美的循环嵌套。
语法
将pragma放在嵌套循环边界内的C源代码中。
#pragma HLS loop_flatten off
其中,off:是一个可选关键字,用于防止发生展平。可以防止在展平指定位置中的所有其他循环时展平某些循环。
示例
示例1
将函数foo中的loop_1和循环层次结构中它上面的所有(完美或半完美)循环展平为单个循环。将pragma放置在 loop_1的主体中。
void foo (num_samples, ...) { int i; ... loop_1: for(i=0;i< num_samples;i++) { #pragma HLS loop_flatten ... result = a + b; } }
示例2
防止loop_1中的循环展平:
loop_1: for(i=0;i< num_samples;i++) { #pragma HLS loop_flatten off ...
pragma HLS loop_merge
说明
将连续循环合并到单个循环中,以减少总体延迟、增加共享并改进逻辑优化。合并循环可以:
- 减少RTL中在循环体实现之间转换所需的时钟周期数。
- 允许并行实现循环。
LOOP_MERGE pragma将寻求合并其放置范围内的所有循环。例如,如果在循环体中应用 LOOP_MERGE pragma,Vivado HLS将指令应用于循环中的任何子循环,但不应用于循环本身。
合并循环的规则是:
- 如果循环边界是变量,则它们必须具有相同的值(迭代次数)。
- 如果循环边界为常量,则最大常量值用作合并循环的边界。
- 不能合并具有可变边界和常量边界的循环。
- 要合并的循环之间的代码不能有单侧作用。多次执行此代码应生成相同的结果(允许a=b,不允许a=a+1)。
- 循环包含FIFO读取时无法合并。合并会更改读取的顺序。从FIFO或FIFO接口的读取必须始终按顺序进行。
语法
将C源代码中的pragma放在所需的代码范围或区域内:
#pragma HLS loop_merge force
其中,force是一个可选关键字,用于强制合并循环,即使Vivado HLS发出警告。在这种情况下,您必须手动确保合并的循环将正常工作。
示例
将函数foo中的所有连续循环合并到单个循环中。
void foo (num_samples, ...) { #pragma HLS loop_merge int i; ... loop_1: for(i=0;i< num_samples;i++) { ...
loop_2 内的所有循环(但不是loop_2本身)通过使用force选项合并。将pragma放置在loop_2的主体中。
loop_2: for(i=0;i< num_samples;i++) { #pragma HLS loop_merge force ...
pragma HLS loop_tripcount
说明
TRIPCOUNT指令可应用于循环,以手动指定循环执行的总迭代次数。TRIPCOUNT指令仅用于分析,不影响合成结果。
Vivado HLS报告每个循环的总延迟,即执行循环所有迭代的时钟周期数。因此,循环延迟是循环迭代次数或tripcount的函数。tripcount可以是一个常量值。它可能取决于循环表达式中使用的变量值(例如,x<y),也可能取决于循环中使用的控制语句。在某些情况下,Vivado HLS无法确定tripcount,且延迟未知。这包括用于确定tripcount的变量为:
- 输入参数。
- 通过动态操作计算的变量。
在循环延迟未知或无法计算的情况下,TRIPCOUNT指令允许您指定循环的最小和最大迭代次数。这使该工具能够分析循环延迟如何影响报告中的总设计延迟,并帮助您确定设计的适当优化。
语法
将pragma放在循环体中的C源代码中:
#pragma HLS loop_tripcount min=<int>max=<int>avg=<int>
其中:
max=<int>
:指定循环迭代的最大次数。min=<int>
:指定循环迭代的最小次数。avg=<int>
:指定循环迭代的平均次数。
示例
函数foo中的loop_1指定最小tripcount为12,最大tripcount为16:
pragma HLS array_reshape
说明
将阵列分区与垂直阵列映射相结合。
ARRAY_RESHAPE 编译指示结合了 ARRAY_PARTITION 的效果,将数组分解为更小的数组,以及 ARRAY_MAP 的垂直类型的效果,通过增加位宽来连接数组元素。 这减少了消耗的块 RAM 数量,同时提供了分区的主要好处:对数据的并行访问。 此编译指示创建了一个元素更少但位宽更大的新数组,从而允许在单个时钟周期内访问更多数据。
参考以下代码:
void foo (...) { int array1[N]; int array2[N]; int array3[N]; #pragma HLS ARRAY_RESHAPE variable=array1 block factor=2 dim=1 #pragma HLS ARRAY_RESHAPE variable=array2 cycle factor=2 dim=1 #pragma HLS ARRAY_RESHAPE variable=array3 complete dim=1 ... }
ARRAY_RESHAPE 编译指示将数组转换为下图所示的形式:
语法
将编译指示放在 C 源代码中定义数组变量的函数区域内。
#pragma HLS array_reshape variable=<name> <type> factor=<int> dim=<int>
其中:
<name>
:一个必需的参数,指定要重塑的数组变量。<type>
:可选地指定分区类型。 默认类型是完整的。 支持以下类型:- ○ cyclic:循环重塑通过交错原始数组中的元素来创建更小的数组。 例如,如果使用factor=3,则将元素0分配给第一个新数组,将元素1分配给第二个新数组,将元素2分配给第三个新数组,然后再次将元素3分配给第一个新数组 . 最后一个数组是新数组的垂直串联(词串联,以创建更长的词)到单个数组中。○block:块重塑从原始数组的连续块创建更小的数组。 这有效地将数组拆分为 N 个相等的块,其中 N 是由 factor= 定义的整数,然后将 N 个块合并为一个字宽*N 的数组。○complete:完全重塑将数组分解为临时的单个元素,然后将它们重新组合成一个具有更宽单词的数组。 对于一维数组,这相当于创建一个非常宽的寄存器(如果原始数组是 M 位的 N 个元素,则结果是一个 N*M 位的寄存器)。 这是数组整形的默认类型。
factor=<int>
:指定当前数组除以的数量(或要创建的临时数组的数量)。 factor=2时,数组分成两半,同时将位宽加倍。
factor=3时将数组分成三份,位宽为三倍。对于complete的类型分区,不需要指定factor。 对于块和循环分区,需要 factor。dim=<int>
:指定要分区的多维数组的哪个维度。 对于具有 N 维的数组,指定为从 0 到 N 的整数:
○ 如果使用值 0,则多维数组的所有维度都使用指定的类型和因子选项进行分区。
○ 任何非零值仅对指定维度进行分区。 例如,如果使用值 1,则仅对第一个维度进行分区。object
:仅与容器数组相关的关键字。 当指定关键字时,ARRAY_RESHAPE 编译指示应用于容器中的对象,重塑容器内对象的所有维度,但保留容器本身的所有维度。当未指定关键字时,编译指示适用于容器数组而不是对象。
示例
示例1
使用块映射将具有 17 个元素的 8 位数组 AB[17] 重新整形(分区和映射)为具有五个元素的新 32 位数组。
#pragma HLS array_reshape variable=AB block factor=4
factor=4 表示数组应该被分成四份。 因此,17 个元素被重新整形为 5 个元素的数组,位宽是其四倍。 在这种情况下,最后一个元素 AB[17] 被映射到第五个元素的低八位,而第五个元素的其余部分为空。
示例2
将二维数组 AB[6][4]
重塑为维度 [6][2]
的新数组,其中维度 2 具有两倍的位宽:
#pragma HLS array_reshape variable=AB block factor=2 dim=2
示例3
将函数 foo 中的三维 8 位数组 AB[4][2][2]
重塑为一个新的单元素数组(一个寄存器),128 位宽(4*2*2*8)
:
#pragma HLS array_reshape variable=AB complete dim=0
dim=0 表示重塑数组的所有维度。
结构体打包
pragma HLS data_pack
说明
将结构的数据字段打包成一个具有更宽字宽的标量。
DATA_PACK 编译指示用于将结构体的所有元素打包到单个宽向量中以减少变量所需的内存,同时允许同时读取和写入结构体的所有成员。 生成的新宽字的位对齐可以从结构域的声明顺序推断出来。 第一个字段采用向量的 LSB,结构体的最后一个元素与向量的 MSB 对齐。
如果结构包含数组,则 DATA_PACK 编译指示执行与 ARRAY_RESHAPE 编译指示类似的操作,并将重构后的数组与结构中的其他元素组合。 在结构中声明的任何数组都被完全分区并重新整形为一个宽标量,并与其他标量字段打包在一起。 但是,不能使用 DATA_PACK 和 ARRAY_PARTITION 或 ARRAY_RESHAPE 优化结构,因为这些编译指示是互斥的。
在对具有大型数组的结构对象使用 DATA_PACK 优化时,应该谨慎行事。 如果一个数组有 4096 个 int 类型的元素,这将导致一个宽度为 4096*32=131072 位的向量(和端口)。 Vivado HLS 可以创建此 RTL 设计,但是逻辑综合不太可能在 FPGA 实现期间对其进行路由。
通常,赛灵思建议使用任意精度(或位精度)数据类型。标准 C 类型基于 8 位边界(8 位、16 位、32 位、64 位),但是,在设计中使用任意精度数据类型可让您在 C 代码中指定确切的位大小。 位精确宽度导致硬件运算符更小、更快。 这允许在 FPGA 中放置更多逻辑,并使逻辑以更高的时钟频率执行。 但是,如果需要,DATA_PACK 编译指示还允许您沿 8 位边界对齐打包结构中的数据。
如果要使用 AXI4 接口实现结构端口,应该考虑使用 DATA_PACK <byte_pad> 选项来自动将结构的成员元素对齐到 8 位边界。 AXI4-Stream 协议要求 IP 的 TDATA 端口的宽度为 8 的倍数。定义 TDATA 端口宽度不是 8 的倍数的 AXI4-Stream IP 是违反规范的,因此,它是一个 要求将 TDATA 宽度四舍五入为字节倍数。
语法
将编译指示放在要打包的 struct 变量的定义附近:
#pragma HLS data_pack variable=<variable> instance=<name> <byte_pad>
其中,
variable=<variable>
:是要打包的变量。instance=<name>
:指定打包后结果变量的名称。 如果未指定<name>
,则使用输入<variable>
。- <byte_pad>:可选地指定是否在8 位边界(8 位、16 位、24 位…)上打包数据。 此选项支持的两个值是:
○ struct_level:首先打包整个结构,然后向上填充到下一个 8 位边界。
○field_level:首先在 8 位边界上填充结构的每个单独元素(字段),然后打包结构。
示例
示例
将具有三个 8 位字段字段(R、G、B)的结构数组 AB[17] 打包成一个新的 24 位 17 元素数组。
typedef struct{ unsigned char R, G, B; } pixel; pixel AB[17]; #pragma HLS data_pack variable=AB
示例2
将具有函数 foo 中的三个 8 位字段(typedef struct {unsigned char R, G, B;} pixel)的结构指针 AB 打包成一个新的 24 位指针。
typedef struct{ unsigned char R, G, B; } pixel; pixel AB; #pragma HLS data_pack variable=AB
示例3
在此示例中,为 rgb_to_hsv 函数的输入和输出参数指定了 DATA_PACK 编译指示,以指示编译器在 8 位边界上打包结构以改进内存访问:
void rgb_to_hsv( RGBcolor* in, // Access global memory as RGBcolor structwise HSVcolor* out, // Access Global Memory as HSVcolor structwise int size) { #pragma HLS data_pack variable=in struct_level #pragma HLS data_pack variable=out struct_level ... }