多核处理器理论上可以并行运行多个代码线程,但目前某些类别的操作阻碍了通过并行计算来提高整体性能的尝试。

是时候使用加速器来运行高度并行的代码了吗?标准处理器有许多 CPU,因此缓存一致性和同步可能涉及数千个低级系统代码周期,以保持所有内核的协调。根据 CPU 宽度和数据依赖性,CPU 利用指令级并行性的能力也有限。

这些 CPU 性能瓶颈是真实存在的、普遍存在的,而且很难解决。尽管软件开发人员在创建可并行算法方面发挥着巨大作用,但专门适合执行并行代码的硬件仍有发展空间。

三大瓶颈

CPU 架构师花费了无数个周期来寻找改进处理器的方法,以提高性能或降低功耗。这是不断改进 CPU 的主要动机。“CPU 是为运行非结构化应用程序代码而设计的,”Quadric 首席营销官 Steve Roddy 解释说。“为了加快执行速度,不给程序员增加考虑代码或目标机器的负担,现代 CPU 积累了一长串越来越奇特的功能,旨在尽可能快地运行随机未知代码。”

多年来不断发展的技术包括:

1、超标量架构,其特点是解码器可以同时向一系列并行的功能单元发出多条指令。

2、推测执行,即 CPU 在分支决策之前推测执行它认为最有可能的分支结果。如果猜测正确,则向前迈出一步。如果猜测错误,则必须刷新管道并运行另一个分支。

3、无序执行,只要序列中的多条指令不相互依赖,它们就会无序运行。

4、实时内存访问跟踪,其中可能同时向内存发出多个内存请求。根据拥塞和流量情况,一些较晚发出的请求可能会较早返回。

尽管做出了这些努力,但仍存在一些瓶颈:

首先是从内存中获取数据所需的时间,这通常需要数百个周期。多个 CPU 和线程可以发出多个请求,重叠它们的访问时间以最大限度地减少明显的延迟。缓存有助于最大限度地减少未来访问的延迟,但如果一个线程更改了其他线程正在使用的值,则一致性需要时间。

第二个瓶颈是线程间数据依赖性导致的同步。如果多个线程想要访问相同的数据,则可能需要在数据更改时锁定该数据以供独占使用,之后再释放该锁定。或者,如果多个线程参与整体计算,则可能存在所有线程必须先完成工作才能继续进行的情况。管理同步的开销可能很大。

第三个瓶颈涉及指令级并行性,即多条指令同时执行。超标量 CPU 明确支持该行为,但它们有限制。

Flow Computing 首席技术官兼首席架构师 Martti Forsell 表示:“CPU 无法正确处理延迟隐藏。它们无法正确处理同步。它们无法正确处理低级并行性。”

一致性延迟

此外,缓存可缓冲数据,以应对较长的 DRAM 访问时间。但缓存系统通常是分层的,混合了私有和共享缓存。Cadence 硅片解决方案事业部高级产品营销部总监 Arif Khan 表示:“影响性能的基本选择是通过增加多级缓存来优化引用局部性。”一级(L1) 缓存最靠近 CPU,优先考虑快速访问。它们通常是私有的,这意味着只有一个 CPU 可以访问它们。

一些 CPU 还提供私有二级 (L2) 缓存。这样做的好处是能够将更多数据保存在缓存中,同时将部分数据移至更大且速度稍慢(但更便宜)的 L2 缓存。有了这些私有缓存,如果多个 CPU 需要访问相同的数据,则每个单独的缓存中都会保留一份副本。“在 CPU 中,由于私有缓存的存在,一致性问题是不可避免的,”Forsell 指出。

一些 CPU 具有共享的 L2 缓存,大多数 3 级(或最后一级)缓存也是共享的。共享缓存的好处是多个 CPU 可以访问单个数据,而无需单独的副本。但 L2 共享通常发生在两个或四个 CPU 之间。因此,具有该结构的八 CPU 处理器将具有两个 L2 缓存,并且共享不会在它们之间交叉。这意味着,如果在具有不同 L2 缓存的两个 CPU 上执行的两个线程需要相同的数据,则每个 L2 缓存都必须有自己的副本才能保持一致。只有最后一级缓存 (LLC) 始终在所有 CPU 之间共享,并且不需要一致性。

图 1:缓存层次结构示例。L1 缓存通常专用于一个 CPU。L2 缓存也可能如此,或者它们可能由所有或部分 CPU 共享。最后一级缓存通常由所有人共享。

但共享也变得复杂。对于具有多个 CPU 的处理器,这些 CPU 通常分布在整个芯片上,因此很难放置一个统一的 LLC,以便所有 CPU 都能以相同的延迟访问它。而且更大的缓存是有代价的。“缓存越大,延迟越高,” Rambus的杰出发明家 Steven Woo 指出。实际上,虽然 LLC 在逻辑上被视为一个单元,但它在物理上被分成多个块,每个块都靠近一个 CPU。

一致性协议已经建立多年。Arm 的 CPU 技术副总裁兼研究员 Fred Piry 表示:“MESI 是最常见的缓存一致性协议,它将缓存行描述为被修改 (M)、独占 (E)、共享 (S) 或无效 (I )。

Woo 表示:“目前,MESI 是一种相当稳定的协议。事实上,它并没有发生太大的变化,这说明我们过去已经做了很多工作 [来对其进行微调]。”

CPU 向其他 CPU 发送消息,指示状态变化。“如果您正在写入已共享的行,则需要告诉其他 CPU,‘我正在修改该缓存行,请忘掉它。我现在有了一个新版本。’在手机或笔记本电脑中,没有那么多 CPU,所以速度非常快,”Piry 说。“如果大型服务器中有数百个 CPU,延迟可能会很明显。”

跟踪这些消息并非易事。“这可能很有挑战性,因为你可能会遇到消息在系统中互相追逐的情况,”Woo 说。“在最坏的情况下,如果你有一个大芯片,芯片一角的核心必须使另一角的核心无效。这需要遍历整个芯片的时间,还需要消息在层次结构中传递的时间。”

这通常发生在承载消息的专用一致性网格上,对于较小的处理器,延迟可能限制在 10 个左右的周期。“Arm 的一致性网格网络 (CMN) 支持 AMBA CHI,就是这样一种结构,”Khan 指出。“IP 提供商也在这个领域有解决方案。”

但是对于具有多个核心的处理器来说,该网格在消息通过时可能会遇到拥塞和延迟,最坏的情况可能会花费大约 1000 个周期的时间。

图 2:缓存更新延迟。对于多核系统来说,更新缓存可能需要相当多的周期

这种一致性活动发生在硬件和系统软件层面的幕后,因此应用软件开发人员几乎没有控制权。然而,他们最有能力根据程序的结构以及线程之间共享变量的方式影响性能。对于硬件设计师来说,最好的选择是调整缓存大小、缓存隐私和一致性网格。改进它们可以提高性能,但会增加芯片尺寸和成本,并增加功耗。

Synopsys ARC-V RPX 处理器产品经理 Mohit Wani 表示:“高效一致的共享缓存架构对于最大限度地减少参与共享计算的所有线程的共享数据访问延迟至关重要。”

同步延迟

两种主要的同步机制是锁(也称为互斥,即“相互排斥”)和屏障。锁会获取多个 CPU 可访问和使用的数据,并将访问限制在单个 CPU 上。这有助于确保每个 CPU 都使用相同的值处理相同的数据,并且不会有 CPU 使用旧版本或中间版本,而其他 CPU 使用新版本。

屏障限制代码在特定点之外的执行,允许所有对结果有贡献的线程赶上并完成,然后任何线程才能继续执行结果。归约算法就是一个例子,它可能涉及多个线程计算单个结果。

至少有两种方法可以实现这两种工具,而且这两种方法都有成本。“有时,当遇到障碍时,人们所做的相当于进入睡眠状态,然后像中断这样的操作会再次唤醒他们,”Woo 解释道。“中断的成本很高,而且发送所有释放消息和重新启动内核都需要花费大量时间。”这样做的好处是可以节省电量,因为内核在等待时处于睡眠状态。

另一种方法是使用标志或信号量,内核执行一个繁忙循环来轮询标志,以查看标志何时发生变化。“当内存位置的值发生变化时,它们就知道可以继续前进了,”Woo 说。这种技术响应速度更快,因为它不涉及中断,但内核在等待时旋转会消耗更多能量。

此外,空闲核心上的线程可能会在此期间被其他线程替换,而这些上下文交换也很昂贵。“当暂停某项任务以腾出处理器核心以等待其他任务赶上进度时,上下文切换开销会产生很大的损失,”Wani 指出。

最后,就像一致性一样,一旦释放屏障或锁,消息就会发送到其他核心,对于多核单元来说,这些周期可能多达数千个。“如果您的系统中有 100 个 CPU,命令将传播到系统中的所有 CPU,”Piry 解释道。“并且所有 CPU 都将收到同步请求并确认已收到并完成。此外,它们需要根据命令进行操作,这可能需要数千个周期。”

图 3:同步延迟。顶部显示 CPU n 设置了屏障;然后它计算其部分的时间比其他 CPU 更长。因此,其他 CPU 必须在完成后等待,直到 CPU n 释放屏障。类似地,当 CPU 设置锁时,在释放锁之前,其他 CPU 都无法访问该数据。

“CPU 硬件可以通过提供优化线程同步和数据共享的指令集和内存架构来提供帮助,”Wani 说道。一些用于嵌入式的小型处理器带有硬件信号量或邮箱来加速同步。这种方法虽然适合小型 CPU,但扩展性不佳。

更复杂的单元会执行特定指令,例如内存原子指令。它们以原子方式执行读取/修改/写入序列,这意味着在完成之前,没有其他实体可以看到或影响结果。此类指令有助于避免需要显式同步的数据危害。

程序架构也是一个重要的工具。“解决同步开销的最佳方法是采用一种可以降低线程需要同步的频率的软件架构,”Wani 说。

指令级并行  

并行执行可以在多个级别进行。指令级并行是指同时执行多条指令。这是现代超标量 CPU 所允许的,但有限制。“您可以在超标量进程中的多个功能单元中执行多条指令,但要求是这些指令必须是独立的,”Forsell 说。

图 4 显示了 10 宽 CPU 微架构的一部分,反映了当今商用的最宽 CPU。根据定义,这里可能的最大并行度是 10,但有两个问题。第一个问题与功能单元的分布有关,而第二个问题则来自数据依赖性。

10 宽 CPU 有 10 组可用功能单元,而哪组获得指令取决于指令是什么。在图 4 中的虚构示例中,每组有两个功能单元,其中一个是整数 ALU。因此,对于整数运算,最多可以同时运行 10 个。但只有五个有浮点单元,两个有位操作单元,乘法器、除法器和融合乘加 (FMA) 各有一个。因此,例如,最多两个位操作可以一起运行,但乘法、除法和 FMA 不可能并行。

图4:超标量 CPU 功能单元。并非所有功能单元通道都具备相同的功能,某些功能可能仅在少数甚至一个通道中可用。

这些并行机会反映了最大可能,并且只有当并行计算中涉及的变量不相互依赖时才会发生这种情况。例如,如果一个操作依赖于前一个操作的结果,那么这两个操作必须连续执行。

再次强调,软件架构是实现并行性最大化的最强大杠杆。“算法始终是优化给定硬件平台性能的最佳方式,”Khan 说道。“分布式数据并行和完全分片数据并行技术可用于模型复制和数据分区,从而实现最佳系统利用率和模型性能。”

识别数据依赖关系需要软件分析。可以静态分析声明的变量,编译器可以以最大化目标 CPU 性能的方式对目标代码进行排序。然而,许多程序大量使用指针,而这些指针无法静态分析,因为它们的值是实时确定的。动态分析,监控加载和存储的访问模式,可能是必要的。在这种情况下,分析结果可以帮助开发人员优化程序的并行性,同时确保依赖关系得到尊重。

控制和数据平面

线程运行一系列指令而不进行任何分支(所谓的基本块),可以实现最大程度的并行性。分支会带来不确定性,尝试推测性地执行可能会以错误猜测和系统调整时出现故障而告终。

工程师有时会描述两种编码风格:一种用于控制,一种用于数据操作。这种区别是网络处理芯片架构的基础,它提供专用硬件来对数据进行长串计算,并辅以 CPU 来执行控制代码,这通常涉及许多分支。数据平面中的代码比控制代码更适合并行执行。

鉴于传统 CPU 架构中性能提升的机会有限,有人建议,为类似数据平面的代码配备一个单独的硬件单元,可以大大提高并行性,而这比单独使用 CPU 所能达到的水平要好得多。“现在,所有性能级别的平台都接受了将结构化任务从通用 CPU 中卸载并将这些任务转移到特定于任务的专用处理引擎上的想法,”Roddy 说。

初创公司 Flow Computing 正在提供这样的单元。Flow Computing 首席执行官 Timo Valtonen 解释说:“这是一个与 CPU 紧密集成在同一块硅片上的 IP 块。它会像 GPU 或神经加速器一样挂在总线上。它是一个可配置的小型处理器阵列,可以执行并行代码,而不需要太多屏障或锁。”

图 5:建议的具有专用并行处理单元的架构。CPU 控制流程并将工作负载交给并行单元。CPU 有效地执行控制流,并行单元执行相当于数据位置代码的代码

提议的单元有一个由其所有核心共享的大型缓存,从而消除了一致性延迟。如果主 CPU 和并行单元都在处理一些相同的数据,那么 CPU 缓存和并行单元缓存之间的一致性将是必要的,但 Flow 认为这不太可能。

使用光纤提供了一种轻量级的方式来处理原本是线程的线程。这些线程由 CPU 创建,可以传递给加速器,并避免处理线程所需的操作系统服务调用。这可以减轻一些同步延迟。

将指令链接在一起的能力可以最大化指令级并行性。“链接就是将一条指令的输出直接输入到下一条指令中,”Woo 解释道。“自 20 世纪 80 年代和 90 年代初以来,Cray 等公司的超级计算机一直在做同样的事情。”

使用并行单元需要重新编译代码。“我们确实应用了一种特殊的编译算法来处理具有依赖关系的代码,以实现链式执行,”Forsell 说。新代码可以让 CPU 交出高度并行的部分,依赖性分析允许将流经并行单元的指令链式化。

但问题是,整个代码库将使用 CPU 的原生语言。编译器不会为并行部分生成单独的目标代码。这意味着并行单元必须配备解码器和其他与随附 CPU 镜像对应的逻辑。这项工作需要与 CPU 供应商合作完成,特别是因为解码其他人的指令集可能被视为侵犯知识产权。

然而,编译器并不是万能的工具。“你必须记住,编译器重视正确性而不是性能,”Woo 警告说。“他们有时必须做出保守的假设,而不是激进的假设。为了充分利用系统,程序员必须输入指令或绕过编译器的自动代码生成。”

超越渐进式增长

尽管 CPU 设计师们一直在努力寻找提高性能的机会,但唾手可得的成果早已不复存在。屋顶线模型(Roofline models)描述了计算在何处受到内存带宽的限制以及计算受限于何处,它可以帮助开发人员确定改进的机会及其相关成本。但如果没有一些改变游戏规则的开发,改进将随着每一代而逐渐增加。

Synopsys 指出了其用于提高性能的四种架构理念。

1、使更高级别的缓存可配置为更大的缓存或更小的缓存加上一些内存,并能够动态重新配置以适应当前工作负载;

2、在每个缓存级别,使用预取器来隐藏更多的获取延迟;

3、即使使用有序 CPU,也允许无序内存请求,这样即使先前的请求仍处于挂起状态,也可以使用先返回的任何内容,并且

4、使用软件实现服务质量 (QoS),使某些核心优先于关键工作负载。

并行卸载单元是否会成为更大的游戏规则改变者?这是有可能的,只要系统开发人员认为它能够顺利地融入他们现有的工作方式。如果变化太多,设计师(他们大多天性保守)会抵制如此多的变化可能给项目带来的风险。此外,增量硅片空间必须增加足够的价值,以保持芯片的适当盈利。这可能会使其对专用 SoC 更具吸引力,因为增加的性能的价值是显而易见的。

与此类加速器相关的工具和开发流程也需要顺利融入现有流程,这又要尊重程序员不愿做出过多改变的心理。即使硬件理念及其相关工具在概念上无可挑剔,新用户仍然需要说服自己硬件和软件实现是正确的,没有错误。

Flow 是否能取得成功,可能有助于决定该技术的发展方向。如果失败了,是因为概念上的根本缺陷,还是因为执行上的问题?如果是后者,其他人可能会接手并继续发展。如果是前者,那么整个想法都会受到质疑。

另一方面,如果它成功了,你可以打赌其他人会试图模仿并改进它的成功。考虑到这个提议还处于早期阶段,我们可能至少有一年甚至更长的时间来确定我们是否有一个赢家。

参考链接

https://semiengineering.com/cpu-performance-bottlenecks-limit-parallel-processing-speedups/