HLS优化概述
使用HLS创建高质量 RTL 设计的一个关键部分是能够将优化应用于 C 代码。 HLS总是试图最小化循环和函数的延迟。为此,在循环和函数内,它尝试并行执行尽可能多的操作。 在函数级别,HLS总是尝试并行执行函数。
除了这些自动优化之外,指令还用于:
- 并行执行多个任务,例如,同一函数的多次执行或同一循环的多次迭代。 这就是流水线。
- 重构阵列(块 RAM)、函数、循环和端口的物理实现,以提高数据的可用性并帮助数据更快地通过设计。
- 提供有关数据相关性或缺乏数据相关性的信息,从而允许执行更多优化。
最后的优化技术是修改 C 源代码,以消除代码中可能限制硬件性能的非预期依赖性。
对比使用循环和函数流水线来创建每个时钟可以处理一个样本的设计。 本示例进行分析设计未能满足性能要求的两个最常见原因:循环依赖性和数据流限制或瓶颈。
练习目的
本练习使用矩阵乘法器设计来展示如何充分优化大量基于循环的设计。 设计目标是使用 FIFO 接口每个时钟周期读取一个样本,同时最小化面积。该分析包括将在循环级别进行优化的方法与在功能级别进行优化的方法进行比较。
分析报告
添加完成后,进行C综合,然后查看生成的报告。
性能估计表示,间隔为106个时钟周期。 由于每个输入阵列中有 9 个元素,因此设计每次读取输入大约需要 9 个周期。间隔和延迟一样长,因此此时硬件中没有并行性。延迟/间隔是嵌套循环造成的。
Product 的内循环:具有 3 个时钟周期的延迟。所有迭代总共有9个时钟周期。
Col 循环:进入循环 Product 需要 1 个时钟,退出循环需要 1 个时钟。所以每次迭代需要 11个时钟周期 (1+9+1)。有 33 个周期来完成所有迭代。
顶层循环每次迭代有 35 个时钟周期的延迟,循环的所有迭代总共有 105 个时钟周期。
改进启动间隔
您可以执行以下两种操作之一来改进启动间隔:流水操作循环或流水操作整个函数。 首先将循环流水线化,然后将这些结果与流水线化整个函数进行比较。
在流水线化循环时,循环的启动间隔是要监控的重要指标 。 如本练习所示,即使设计达到循环可以在每个时钟周期处理一个样本的阶段,函数的启动间隔仍报告为函数中包含的循环完成处理函数的所有数据所需的时间。
流水操作 Product 循环
新建解决方案然后选择Product循环标签添加PIPELINE指令,如下图所示:
添加完成后进行C综合。在综合过程中可以看到这样的警告:
提醒在流水操作中无法强制执行指定好的依赖约束。所以在综合报告显示,Product 循环以 2 的间隔进行流水线化,但顶层循环的间隔并未流水线化。
打开分析透视图。在性能视图中,展开循环 Row_Col 和 Product。选择状态 C1 下的写操作。右击选择Goto Source,
问题是携带依赖 。 这是循环的一次迭代中的操作与同一循环的不同迭代中的操作之间的依赖关系。例如,当 k=1 和 k=2 时的操作(其中 k 是循环索引)。
第一个操作是第 60 行对数组 res 的加载(内存读取操作)。第二个操作是第 60 行对数组 res 的存储(内存写操作)。
由于 += 运算符,第 60 行是从数组 res 读取和对数组 res 的写入。 默认情况下,阵列映射到块 RAM,性能视图中的详细信息可以显示发生此冲突的原因。这里的流水操作的问题稍后解决。
下一步是流水线化上面的循环,Col 循环。 这会自动展开 Product 循环并创建更多运算符,从而创建更多硬件资源,但它确保 Product 循环的不同迭代之间没有依赖性。
流水操作 Col 循环
新建解决方案然后选择Col循环标签添加PIPELINE指令,并移除Product中的流水操作,如下图所示:
在综合期间,控制台窗格中报告的信息同样会显示循环 Product 已展开,循环展平已在循环 Row 上执行,并且由于阵列 a 的内存资源限制,无法在循环 Row_Col 上实现默认启动内部目标 1。
查看综合报告显示,如上所述,循环 Row_Col 的间隔只有两个:目标是每个循环处理一个样本。 可以使用分析透视图突出显示未实现启动目标的原因。
上图显示了对数组 a 的操作,数组 a 有三个读操作。 两个操作从周期1开始,第三个读取操作从周期2 开始。
在资源的视图中可以看到数组a的使用情况,
在图中,数组a在三个时钟周期内都有调用,说明这部分资源存在复用。端口 b 和端口 a相似,也会出现同样的问题:它也必须执行 3 次读取。
重构数组
所以仅仅使用流水操作无法满足我们的目标要求,HLS允许对数组进行分区、映射和重构。这些技术允许在不更改源代码的情况下修改对数组的访问。
因为 Product 循环的循环索引是 k,所以两个数组都应该沿着它们各自的 k 维度进行分区:设计需要在每个时钟周期访问两个以上的 k 值。
对于数组 a,这是维度 2,因为它的访问模式是 a[i][k] ; (对于数组a来说,在最内层的循环中,k值改变读取的是数组下一行的值,也就是二维数组的第一列,所以维度是2)对于数组 b,这是维度 1,因为它的访问模式是 b[k][j],(对于数组b来说,在最内层的循环中,k值改变读取的是数组下一列的值,也可以理解为是一维数组的一行数据,所以维度是1)
所以根据上面的描述添加相关指令,对数组a添加重构指令,维度为2,对数组b添加重构指令,维度为1。如下图:
添加完成后进行C综合,并分析综合报告。
综合报告显示顶层循环 Row_Col 现在正在以每个时钟周期 1 个样本处理数据。顶层模块需要 13 个时钟周期才能完成。Row_Col 循环在 4个周期(迭代延迟)后输出一个样本。然后它在每个周期(启动间隔)读取 1 个样本。9 次迭代后,它完成所有样本计算。4 + 9 = 12 个时钟周期。