写在前面
本文参考赛灵思的官方手册UG1270,主要介绍了结构优化策略,以帮助我们更好的进行HLS的开发设计。
结构优化策略
C代码可以包含防止函数或循环以所需性能流水线化的描述。 这通常由 C 代码的结构或用于实现 PL 逻辑的默认逻辑结构暗示。 在某些情况下,这可能需要修改代码,但在大多数情况下,这些问题可以使用额外的优化指令来解决。
以下示例显示了使用优化指令来改进实现结构和流水线性能的情况。 在这个初始示例中,将 PIPELINE 指令添加到循环中以提高循环的性能。 此示例代码显示了在函数内部使用的循环。
#include "bottleneck.h" dout_t bottleneck(...) { ... SUM_LOOP: for(i=3;i<N;i=i+4) { #pragma HLS PIPELINE sum += mem[i] + mem[i-1] + mem[i-2] + mem[i-3]; } ... }
当上面的代码编译到硬件中时,会出现以下消息作为输出:
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.
I
此示例中的问题是使用 PL 结构中的高效的块 RAM 资源来实现阵列。这导致开销小、高效的快速设计。 但Block RAM 的缺点是,与 DDR 或 SRAM 等其他存储器一样,它们的数据端口数量有限,通常最多两个。
在上面的代码中,需要来自 mem 的四个数据值来计算 sum 的值。 由于 mem 是一个数组并在只有两个数据端口的块 RAM 中实现,因此每个时钟周期只能读取(或写入)两个值。 使用这种配置,不可能在一个时钟周期内计算 sum 的值,从而消耗或产生 II 为 1 的数据(每个时钟处理一个数据样本)。
内存端口限制问题可以通过在内存数组上使用 ARRAY_PARTITION 指令来解决。 该指令将数组划分为更小的数组,通过提供更多的数据端口和允许更高性能的管道来改进数据结构。
使用下面显示的附加指令,数组 mem 被划分为两个双端口存储器,以便所有四个读取都可以在一个时钟周期内发生。 对阵列进行分区有多种选择。 在这种情况下,因子为 2 的循环分区确保第一个分区包含来自原始数组的元素 0、2、4 等,而第二个分区包含元素 1、3、5 等。因为分区确保有 现在有两个双端口块 RAM(总共有四个数据端口),这允许在单个时钟周期内读取元素 0、1、2 和 3。
#include "bottleneck.h" dout_t bottleneck(...) { #pragma HLS ARRAY_PARTITION variable=mem cyclic factor=2 dim=1 ... SUM_LOOP: for(i=3;i<N;i=i+4) { #pragma HLS PIPELINE sum += mem[i] + mem[i-1] + mem[i-2] + mem[i-3]; } ... }
尝试流水线化循环和函数时可能会遇到其他此类问题。 下表列出了可能通过帮助减少数据结构中的瓶颈来解决这些问题的指令。
除了 ARRAY_PARTITION 指令外,阵列分区的配置还可用于自动对阵列进行分区。
流水线循环时,可能需要 DEPENDENCE 指令来删除隐含的依赖项。 这种依赖性由消息 SCHED-68 报告。
@W [SCHED-68] Target II not met due to carried dependence(s)
INLINE 指令删除函数边界。 这可用于将逻辑或循环向上一层层次结构。 通过将函数包含在其上方的函数中,将其逻辑流水线化可能更有效,将循环合并到它们上面的函数中,其中 DATAFLOW 优化可用于同时执行所有循环,而无需没有中间子函数调用的开销。 这可能会导致更高性能的设计。
对于无法使用所需的 II 流水线化循环的情况,可能需要 UNROLL 指令。 如果一个循环只能在 II = 4 的情况下进行流水线化,它会将系统中的其他循环和函数限制为 II = 4。在某些情况下,可能值得展开或部分展开循环以创建更多逻辑和消除潜在的瓶颈。 如果循环只能达到 II = 4,那么将循环展开 4 倍将创建可以并行处理循环的四次迭代并达到 II = 1 的逻辑。
减少延迟
当编译器完成最小化启动间隔 (II) 后,它会自动寻求最小化延迟。 下表中列出的优化指令可以帮助指定特定的延迟或通知编译器实现低于生成的延迟,即指示编译器满足延迟指令,即使它导致更高的 II。 这可能会导致性能较低的设计。
通常不需要延迟指令,因为大多数应用程序具有所需的吞吐量但没有所需的延迟。 当硬件功能与处理器集成时,处理器的延迟通常是系统中的限制因素。
如果循环和函数没有流水线化,吞吐量会受到延迟的限制,因为在当前任务完成之前,任务不会开始读取下一组输入。
循环优化指令可用于展平循环层次结构或将连续循环合并在一起。 延迟的好处是因为它通常在控制逻辑中花费一个时钟周期来进入和离开由循环创建的逻辑。 循环之间的转换次数越少,完成设计所需的时钟周期数就越少。
减少面积
在硬件中,实现逻辑功能所需的资源数量称为设计区域。 设计面积也指资源在固定尺寸的PL结构上使用的面积。 当硬件太大而无法在目标设备中实现时,以及当硬件功能占用可用区域的百分比非常高(> 90%)时,该区域很重要。 当尝试将硬件逻辑连接在一起时,这可能会导致困难,因为连接线本身需要资源。
在满足所需的性能目标(或 II)后,下一步可能是在保持相同性能的同时减少面积。 这一步可以是最佳的,因为如果硬件功能以所需的性能运行,并且在 PL 的剩余空间中不实施其他硬件功能,则通过减少面积没有任何好处。
最常见的区域优化是数据流内存通道的优化,以减少实现硬件功能所需的块 RAM 资源数量。 每个设备具有数量有限的 Block RAM 资源。
如果您使用了 DATAFLOW 优化并且编译器无法确定设计中的任务是否是流式数据,它会使用乒乓缓冲区实现数据流任务之间的内存通道。 这些需要两个块 RAM,每个块的大小为 N,其中 N 是要在任务之间传输的样本数(通常是在任务之间传递的数组的大小)。 如果设计是流水线式的,并且数据实际上是从一个任务流到下一个任务,并以顺序方式产生和消耗值,那么您可以通过使用 STREAM 指令来指定数组将在一个 使用简单 FIFO 的流式传输方式,您可以为其指定深度。 深度较小的 FIFO 使用寄存器实现,PL 结构有许多寄存器。
- 对于以相同速率产生和消耗数据的任务,指定它们之间的数组以深度为 1 进行流式传输。
- 对于将数据速率降低 X 到 1 倍的任务,在任务的输入处指定数组以 X 的深度进行流式传输。函数中在此之前的所有数组也应具有 X 的深度以确保 硬件功能不会因为 FIFO 已满而停止。
- 对于将数据速率提高 1 到 Y 倍的任务,在任务的输出处指定数组以 Y 的深度进行流式传输。函数中此之后的所有数组也应具有 Y 的深度以确保 硬件功能不会因为 FIFO 已满而停止。
下表列出了在尝试最小化用于实现设计的资源时要考虑的其他指令。
ALLOCATION 和 RESOURCE 指令用于限制操作数量并选择使用哪些内核(硬件资源)来实现操作。 例如,您可以将函数或循环限制为仅使用一个乘法器,并指定它使用流水线乘法器来实现。
如果 ARRAY_PARITION 指令用于改进启动间隔,您可能需要考虑使用 ARRAY_RESHAPE 指令。 ARRAY_RESHAPE 优化执行与数组分区类似的任务,但是,重塑优化将通过分区创建的元素重新组合到具有更宽数据端口的单个块 RAM 中。 这可能会阻止所需块 RAM 资源数量的增加。
如果 C 代码包含一系列具有相似索引的循环,将循环与 LOOP_MERGE 指令合并可能会允许进行一些优化。 最后,如果流水线区域中的一段代码只需要以低于该区域其余部分的启动间隔运行,则使用 OCCURENCE 指令指示可以优化此逻辑以以较低的速率执行。
设计优化工作流程
在执行任何优化之前,建议在项目中创建一个新的构建配置。 使用不同的构建配置允许将一组结果与另一组结果进行比较。 除了标准的 Debug 和 Release 配置之外,还可以使用 Manage Build Configurations for Project 工具栏按钮在 Project Settings 窗口中创建具有更有用名称的自定义配置(例如 Opt_ver1 和 UnOpt_ver)。
不同的构建配置不仅可以比较结果,还可以比较用于实现 FPGA 的日志文件甚至输出 RTL 文件(RTL 文件只推荐给非常熟悉硬件设计的用户)。
高性能设计的基本优化策略是:
- 创建初始或基线设计。
- 流水线化循环和函数。 应用 DATAFLOW 优化以同时执行循环和函数。
- 解决任何限制流水线的问题,例如数组瓶颈和循环依赖(使用 ARRAY_PARTITION 和 DEPENDENCE 指令)。
总之,目标是始终首先满足性能,然后再减少面积。 如果策略是用最少的资源创建设计,只需省略提高性能的步骤,尽管基线结果可能非常接近最小的设计。
在整个优化过程中,强烈建议在编译后查看控制台输出(或日志文件)。 当编译器无法达到优化的指定性能目标时,它会自动放宽目标(时钟频率除外)并创建具有可满足目标的设计。 查看编译日志文件和报告的输出以了解已执行的优化非常重要。
reference
- UG1270