原文链接:
https://blog.csdn.net/weixin_45825101/article/details/123909016m=1001.2014.3001.5502
虽然 FPGA 可使用 Verilog 或 VHDL 等低层次硬件描述语言 (HDL) 来编程,但现在已有多种高层次综合 (HLS) 工具可以采用以 C/C++ 之类的更高层次的语言编写的算法描述,并将其转换为 Verilog 或 VHDL 等低层次的硬件描述语言。随后,下游工具即可对转换后的语言进行处理,以便对 FPGA 器件进行编程。此类流程的主要优势在于,您可使用诸如 C/C++ 等编程语言来编写高效代码,而后将代码转换为硬件,但这类编程语言的优势仍能得以完整保留。此外,写好代码乃是软件设计师的专长,比学习新的硬件描述语言更简单。
以 C/C++ 编写的程序本质上是专为冯诺依曼样式的架构编写的,此类架构中用户程序内的每条指令都是按顺序执行的。为了实现高性能,HLS 工具必须推断顺序代码中的并行性,并利用它来实现更高的性能。要解决这个问题可并不简单。此外,优秀的软件程序员按明确定义的规则和实践来编写程序,例如,RTTI、递归和动态存储器分配。其中诸多技巧在硬件中都无法找到直接等效的对象,故而给 HLS 工具带来了诸多挑战。这也意味着任意现成软件都无法高效转换为硬件。最低限度,需检验此类软件中是否存在不可综合的构造,并需要重构代码,使其可综合。
现如今,即使软件程序可自动转换(或综合)为硬件,但要实现可接受的结果质量 (QoR),仍需要额外工作(例如,重写软件)以帮助 HLS 工具实现期望的性能目标。为此,您需要了解正确编写软件的最佳实践,以确保在 FPGA 器件上正常执行软件。在接下来的几个章节内,将着重探讨如何首先识别部分宏观级别架构最优化以明确程序结构,然后聚焦更细化的微观级别架构最优化来实现性能目标。
生产者使用者范例
请考虑软件设计师编写多线程程序的方式,通常有一个主线程用于执行某些初始化步骤,随后分叉为多个子线程用于执行某些并行计算,当所有并行计算都完成后,主线程会整理结果并写入输出。程序员必须理清哪些部分可以分叉以供并行计算,哪些部分需要按顺序执行。这种分叉/连接类型的并行化操作不仅适用于 CPU,也适用于 FPGA,但 FPGA 上的吞吐量的关键模式之一是生产者使用者范例。您需要将生产者使用者范例应用于顺序程序,并将其转换为可并行执行的抽取功能以便提升性能。
您可借助一条简单的问题语句的帮助来更好地理解这个分解进程。假定您有一份数据手册,可供我们将其中的项导入列表。随后,您将对列表中的每个项进行处理。处理完每个项需耗时约 2 秒。处理完后,您将把结果写入另一份数据手册,此操作将耗时约每项各 1 秒。因此,如果输入 Excel 工作表中有总计 100 个项,那么将耗时总计 300 秒来生成输出。这样做的目的是对此问题进行分解,以便您识别能够并行执行的任务,从而提升系统吞吐量。
图 1. 程序工作流程
连接每项“任务”的串流通常是作为 FIFO 队列来实现的。FIFO 能够抽离程序员的并行行为,使其专注于推理任务活动(调度)的“快照”时间。FIFO 能够使并行化更易于实现。这主要得益于它减少了程序员实现并行化框架或容错解决方案时,必须应付的可变空间。独立内核之间的 FIFO 展现出经典的排队行为。对于纯串流系统,可使用排队或 络流模型来对此行为进行建模。这种数据流类型 络和串流最优化的另一个主要优势在于它可按不同粒度级别来应用。程序员可以在每项任务内部设计此类 络,也可以为任务或内核系统设计此类 络。实际上,您可以通过串流 络来以分层方式例化并连接多个串流 络或任务。支持更细粒度的并行化的另一项最优化措施是流水打拍。
流水线范例
流水线是您日常生活中常用的概念。造车厂生产线就是一个典型的例子,其中每一项具体任务通常都是由一个独立且唯一的工作站来完成的,如安装引擎、安装车门和安装车轮。各工作站各自对一辆不同的车并行执行自己的任务。当某一辆车执行完某一项任务后,它就会移至下一个工作站。完成各项任务的时间差可通过“缓冲”(将一辆或多辆车暂存在各工作站之间的空间内)和/或“停滞”(暂时中止上游工作站的操作)来加以调整,直至下一个工作站变为可用为止。
假设组装一辆车需要执行 3 项任务 A、B 和 C,这 3 项任务分别需要 20、10 和 30 分钟。那么,如果全部 3 项任务均由单个工作站来执行,工厂每 60 分钟才能输出一辆车。通过使用 3 个工作站组成的流水线,该工厂 60 分钟即可输出第一辆车,随后每 30 分钟再输出一辆新车。正如此示例所示,流水线并不会降低时延,即单个项穿越整个系统的总时间。但它会增加系统吞吐量,即,第一个项完成后处理新的项的速率。
由于流水线的吞吐量不可能优于其最慢的元素,因此程序员应尝试在各阶段间拆分工作和资源,以使各阶段耗用相同时间来完成自己的任务。在上述车辆组装线示例中,如果 3 项任务 A、B 和 C 各自耗时 20 分钟,而不是分别耗时 20、10 和 30 分钟,那么时延将仍为 60 分钟,但每隔 20 分钟(而不是 30 分钟)即可完成一辆新车。下图显示了承担制造 3 辆车任务的假想生产线示例。假定任务 A、B 和 C 各耗时 20 分钟,那么顺序生产线将需要 180 分钟才能生产 3 辆车。而流水线式生产线只需 100 分钟即可生产 3 辆车。
生产第一辆车所耗费的时间为 60 分钟,称为流水线的迭代时延。生产完第一辆车后,后两辆车各自只需 20 分钟,这称为流水线的启动时间间隔 (II)。生产三辆车所耗费的总时间为 100 分钟,称为流水线的总时延,即,总时延 = 迭代时延 + II * (项数 – 1)。因此,改善 II 即可改善总时延,但不影响迭代时延。从程序员视角来看,流水线范例可适用于设计中的函数和循环。确定初始建立时间成本后,要实现理想吞吐量目标,II 应为 1,即初始建立时间延迟过后,在流水线的每个周期都将有输出可用。在以上示例中,初始建立时间延迟 60 分钟过后,每隔 20 分钟就有一辆车可用。
图 3. 流水打拍
在菱形示例中,B 与 C 完全彼此独立。两者既不相互通信也不访问任何共享存储器资源,因此如果无需共享计算资源,那么两者可并行执行。由此可得结果如下图所示,一轮运行内形成了某种形式的分叉式连接并行运行。当 A 任务结束后,B 和 C 并行执行,而 D 则等待 B 和 C,但下一轮仍按顺序连续执行。
图 5. 一轮运行中的任务并行
但现在,当 B 首次运行从存储器中执行读取时,A 则已得到其首轮运行的结果,A 的第二轮操作可能已在相同存储器内执行写入。为避免在使用数据之前写入数据,您可以依靠某种形式的存储器扩展(即所谓的双重缓冲或 PIPO)来达成此交织操作。这种交织操作以任务间的黑色圆圈来表示。
有一种有效的方法可用于提升吞吐量和复用计算资源,即对运算符、循环和/或函数进行流水打拍。如果每个任务现在都能与自身重叠,您即可在一轮运行内实现任务并行操作,同时跨多轮运行实现任务流水打拍,这两者都属于宏观级别并行度的例证。多任务内流水打拍则是微观级别并行度的例证。现在,由于每一轮运行依赖于任务间的最小吞吐量而不是任务的最大吞吐量,因此每一轮的总体吞吐量得以进一步提升。最后,根据通信数据的同步方式,仅当生成全部数据 (PIPO) 或者以逐元素方式 (FIFO) 生成全部数据后,才有可能在一轮运行内出现某种程度的额外重叠。例如,在下图中,B 和 C 都比 A 更早启动并以流水打拍方式执行,而 D 则假定仍必须等待 B 和 C 完成。这是最后一种类型的单轮运行内重叠,当 A 通过 FIFO 串流访问(表现为不含圆圈的直线)来与 B 和 C 通信时,才能实现这种方式的重叠。同样,D 也能与 B 和 C 重叠,前提是采用的通道为 FIFO 而不是 PIPO。但不同于前几种执行模式,使用 FIFO 可能导致死锁,因此需要正确设置这些串流 FIFO 的大小。
图 7. 单轮运行内的任务并行和流水打拍、多轮运行的流水打拍以及单一任务内的流水打拍
点个在看你最好看
文章知识点与官方知识档案匹配,可进一步学习相关知识Java技能树 络编程 络编程基础92463 人正在系统学习中
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!