目录
[1. 简介](#1. 简介)
[2. 示例](#2. 示例)
[2.1 不完美循环](#2.1 不完美循环)
[2.2 完美循环](#2.2 完美循环)
[2.3 HLS 眼中的循环](#2.3 HLS 眼中的循环)
[3. 总结](#3. 总结)
1. 简介
在处理嵌套循环时(HDL或HLS工具中),优化循环结构对于实现最佳性能至关重要。嵌套循环的性能优化直接影响着计算的时延和资源利用率。创建完美嵌套的循环意味着循环结构被设计得紧凑、高效,以最大程度地利用硬件资源和并行计算能力。
在硬件描述或高级综合中,循环的每个细节都被转换为硬件电路或流水线阶段。因此,创建完美嵌套循环的目标是确保循环体内的计算被最有效地映射到硬件逻辑中,并且循环的边界和计算步骤都能够被静态分析和优化。这种优化可以带来多方面的好处,包括更低的时延、更高的时钟频率、更少的资源占用和更高的吞吐量。
通过创建完美嵌套的循环,可以确保最内层循环包含所有计算逻辑,并且循环之间没有逻辑依赖或数据依赖,从而允许并行执行和流水线处理。此外,循环边界应该是常量,以便编译器在优化时能够静态确定迭代次数,从而更好地利用硬件资源。最终,这些优化可以使得硬件实现具有更高的性能、更低的时延,并且能够更好地适应特定的应用需求。
以下介绍三种典型的循环及其特点。
完美循环嵌套:
- 仅限最内层循环才包含循环主体内容。
- 在循环语句之间不指定任何逻辑。
- 所有循环边界均为常量。
半完美循环嵌套:
- 仅限最内层循环才包含循环主体内容。
- 在循环语句之间不指定任何逻辑。
- 最外层的循环边界可采用变量。
非完美循环嵌套:
- 内层循环具有变量边界。
- 循环主体未完全包含在内层循环内。
2. 示例
2.1 不完美循环
cpp
void loop_imperfect(int A[20], int B[20]) {
int i, j; // 定义循环变量i和j
int acc; // 定义累加器变量acc
// 外层循环遍历数组B的每个元素
for (i = 0; i < 20; i++) {
acc = 0; // 在计算每个B[i]之前,将累加器归零
// 内层循环遍历数组A,计算加权和
for (j = 0; j < 20; j++) {
acc += A[j] * j; // 将A[j]与其索引j相乘,并累加到acc
}
// 根据索引i的奇偶性决定B[i]的值
if (i % 2 == 0)
B[i] = acc / 20; // 如果i是偶数,B[i]为acc除以20的结果
else
B[i] = 0; // 如果i是奇数,B[i]设为0
}
}
如果按照纯软件代码的标准, 这段代码有很大的问题:
- 内层循环对于每个B[i]都重新计算了加权和,即使B[i]被设为0。这意味着有一半的计算是不必要的。
- 由于j的范围是固定的,所以acc的计算可以在循环外部进行一次,然后根据需要分配给B[i]。
但是在Vitis HLS的角度,以上两点都不是问题,因为代码最终对应的是硬件的连接。事实上,以上代码的问题是:循环主体未完全包含在内层循环内。
2.2完美循环
cpp
void loop_perfect(int A[20], int B[20]) {
int i, j; // 定义循环变量i和j
int acc; // 定义累加器变量acc
// 外层循环遍历数组B的每个元素
for (i = 0; i < 20; i++) {
// 内层循环遍历数组A,计算加权和
for (j = 0; j < 20; j++) {
if (j == 0) acc = 0; // 当j为0时,累加变量归零
acc += A[j] * j; // 将A[j]与其索引j相乘,并累加到acc
// 当内层循环到达最后一个元素时,根据i的奇偶输出B
if (j == 19) {
if (i % 2 == 0)
B[i] = acc / 20; // 如果i是偶数,B[i]为acc除以20的结果
else
B[i] = 0; B[i] = 0; // 如果i是奇数,B[i]设为0
}
}
}
}
相对于2.1的"改进点":
- 累加器归零(if (j == 0) acc = 0;)的操作被移动到了内层循环的开始。
- 条件判断(if (j == 19))被放置在内层循环的末尾。
如果按照纯软件代码的标准, 以上代码的问题更大了:
以上两个判断在每次内层循环时都会执行,这是不必要的,因为它只需要在内层循环开始或结束时执行一次!
2.3 HLS 眼中的循环
在 HLS 工具的眼中,完美循环嵌套是指循环结构被优化以便于硬件实现,从而实现最佳的性能和最低的资源使用。
原因如下:
- **最内层循环包含所有计算:**所有的计算逻辑都应该包含在最内层循环中,这样可以最大化并行性和流水线效率。
- **循环边界是常量:**这允许HLS工具在编译时确定循环的迭代次数,从而更好地优化生成的硬件。
- **没有循环间的逻辑依赖:**循环之间不应该有数据依赖或逻辑依赖,这样可以避免不必要的延迟,并允许循环独立地并行执行。
- **循环展开和流水线:**HLS工具可以自动展开循环并应用流水线技术,以提高并行性和吞吐量。
- **数据访问模式是规则的:**数据访问应该是可预测的,以便于HLS工具进行有效的内存访问优化。
因为 HLS 会用到如下两个优化指令:
cpp
#pragma HLS UNROLL // 指示HLS工具完全展开这个循环
#pragma HLS PIPELINE // 指示HLS工具流水线化这个循环
第一个指令,会让内层循环完全展开,即内层循环在一个周期内完成。
第二个指令,内层循环可流水线拆分,20个周期内完成。
所以,第二段代码实现了"最内层循环包含所有计算",是HLS眼中的完美循环。
3. 总结
本文分享了处理嵌套循环时的重要性以及创建完美嵌套循环的关键原则。优化循环结构对于实现最佳性能至关重要,完美循环嵌套的设计能够最大程度地利用硬件资源和并行计算能力,从而提高性能并降低时延。