首页 > 解决方案 > 在预测现代超标量处理器上的操作延迟时需要考虑哪些因素,以及如何手动计算它们?

问题描述

我希望能够手动预测任意算术(即没有分支或内存,尽管这也很好)x86-64 汇编代码在特定架构下将花费多长时间,同时考虑到指令重新排序、超标量、延迟、CPI 等。

必须遵循什么/描述的规则来实现这一点?


我想我已经弄清楚了一些初步的规则,但是我还没有找到任何关于将任何示例代码分解到这个细节级别的参考资料,所以我不得不做出一些猜测。(例如,英特尔优化手册甚至几乎没有提到指令重新排序。)

至少,我正在寻找(1)确认每条规则是正确的,或者是每条规则的正确陈述,以及(2)我可能已经忘记的任何规则的列表。


例如,考虑以下示例代码(计算叉积):

shufps   xmm3, xmm2, 210
shufps   xmm0, xmm1, 201
shufps   xmm2, xmm2, 201
mulps    xmm0, xmm3
shufps   xmm1, xmm1, 210
mulps    xmm1, xmm2
subps    xmm0, xmm1

我预测 Haswell 延迟的尝试如下所示:

; `mulps`  Haswell latency=5, CPI=0.5
; `shufps` Haswell latency=1, CPI=1
; `subps`  Haswell latency=3, CPI=1

shufps   xmm3, xmm2, 210   ; cycle  1
shufps   xmm0, xmm1, 201   ; cycle  2
shufps   xmm2, xmm2, 201   ; cycle  3
mulps    xmm0, xmm3        ;   (superscalar execution)
shufps   xmm1, xmm1, 210   ; cycle  4
mulps    xmm1, xmm2        ; cycle  5
                           ; cycle  6 (stall `xmm0` and `xmm1`)
                           ; cycle  7 (stall `xmm1`)
                           ; cycle  8 (stall `xmm1`)
subps    xmm0, xmm1        ; cycle  9
                           ; cycle 10 (stall `xmm0`)

标签: assemblyx86-64pipelinelatencysuperscalar

解决方案


TL:DR:寻找依赖链,尤其是循环携带的。对于长时间运行的循环,查看哪个延迟、前端吞吐量或后端端口争用/吞吐量是最严重的瓶颈。如果没有缓存未命中或分支错误预测,这就是您的循环平均每次迭代可能需要多少个周期。

相关:每条汇编指令需要多少个 CPU 周期?很好地介绍了基于每条指令的吞吐量与延迟,以及这对多条指令序列的意义。


这称为静态(性能)分析。维基百科说 ( https://en.wikipedia.org/wiki/List_of_performance_analysis_tools ) AMD 的 AMD CodeXL 有一个“静态内核分析器”(即用于计算内核,又名循环)。我从来没有尝试过。

英特尔还有一个免费工具,用于分析循环将如何通过 Sandybridge 系列 CPU 的管道:什么是 IACA 以及如何使用它?

IACA 还不错,但有错误(例如shld,Sandybridge 上的数据错误,最后我检查了一下,它不知道Haswell/Skylake 可以保持索引寻址模式对某些指令进行微融合。但现在英特尔的可能会改变在他们的优化手册中添加了详细信息。)IACA 也无助于计算前端 uop 以查看您离瓶颈有多近(它喜欢只给您未融合域的 uop 计数)。


静态分析通常非常好,但绝对可以通过性能计数器进行分析来检查。看看x86 的 MOV 真的可以“免费”吗?为什么我根本无法重现这个?有关分析简单循环以研究微架构功能的示例。


必读:

Agner Fog 的 微架构指南(第 2 章:乱序执行)解释了依赖链和乱序执行的一些基础知识。他的“优化装配”指南有更多好的介绍性和高级性能的东西。

他的微架构指南的后面章节涵盖了 CPU 中的管道细节,如 Nehalem、Sandybridge、Haswell、K8/K10、Bulldozer 和 Ryzen。(和 Atom / Silvermont / Jaguar)。

Agner Fog 的指令表(电子表格或 PDF)通常也是指令延迟/吞吐量/执行端口故障的最佳来源。

David Kanter 的微架构分析文档非常好,有图表。例如https://www.realworldtech.com/sandy-bridge/https://www.realworldtech.com/haswell-cpu/https://www.realworldtech.com/bulldozer/

另请参阅x86 标签 wiki 中的其他性能链接。

我还在此答案中尝试解释 CPU 内核如何发现和利用指令级并行性,但我认为您已经掌握了与调优软件相关的这些基础知识。不过,我确实提到了 SMT(超线程)如何将更多 ILP 暴露给单个 CPU 内核。


在英特尔术语中

  • “发出”是指将一个uop发送到核心的乱序部分;连同寄存器重命名,这是前端的最后一步。问题/重命名阶段通常是管道中最窄的点,例如自 Core2 以来英特尔上的 4-wide。(由于 SKL 改进的解码器和 uop-cache 带宽,以及后端和缓存带宽的改进,后来的 uarches 像 Haswell 尤其是 Skylake 通常实际上在一些实际代码中非常接近。)这是融合域 uop :微融合让你通过前端发送2个微指令,只占用一个ROB条目。(我能够在 Skylake 上构建一个循环,每个时钟维持 7 个非融合域微指令)。另请参阅http://blog.stuffedcow.net/2013/05/measuring-rob-capacity/ re:无序窗口大小。

  • “dispatch”表示调度程序向执行端口发送一个微指令。一旦所有输入都准备好并且相关的执行端口可用,就会发生这种情况。 究竟是如何安排 x86 微指令的?. 调度发生在“未融合”域中;微融合的微指令在 OoO 调度程序(又名预订站,RS)中单独跟踪。

许多其他计算机体系结构文献以相反的方式使用这些术语,但这是您可以在 Intel 的优化手册中找到的术语,以及硬件性能计数器的名称,如uops_issued.anyuops_dispatched_port.port_5


任意算术 x86-64 汇编代码究竟需要多长时间

它也取决于周围的代码,因为 OoO exec

在 CPU 开始运行后面的指令之前,您的最终subps结果不必准备好。延迟仅对需要该值作为输入的后续指令很重要,而不是整数循环等。

有时吞吐量很重要,乱序执行可以隐藏多个独立的短依赖链的延迟。(例如,如果您对多个向量的大数组中的每个元素执行相同的操作,则多个叉积可以同时进行。)您最终会一次进行多次迭代,即使按照程序顺序在执行任何下一个迭代之前完成所有一个迭代。(如果 OoO exec 很难在硬件中进行所有重新排序,则软件流水线可以帮助处理高延迟循环体。)

短块主要分析三个维度

您可以根据这三个因素大致表征一小段非分支代码。通常只有其中一个是给定用例的瓶颈。通常,您正在查看将用作循环的一部分而不是整个循环体的块,但是OoO exec 通常工作得很好,您可以将这些数字相加为几个不同的块,如果它们是不会太长以至于 OoO 窗口大小无法找到所有 ILP。

  • 从每个输入到输出的延迟。查看从每个输入到每个输出的依赖链上有哪些指令。例如,一个选择可能需要一个输入才能更快地准备好。
  • uop 总数(针对前端吞吐量瓶颈),英特尔 CPU 上的融合域。例如,Core2 和更高版本理论上可以将每个时钟的 4 个融合域 uops 发布/重命名为乱序调度器/ROB。Sandybridge 系列通常可以通过 uop 缓存和循环缓冲区在实践中实现这一点,尤其是 Skylake 具有改进的解码器和 uop-cache 吞吐量。
  • 每个后端执行端口(未融合域)的 uop 计数。例如,重洗牌的代码通常会在英特尔 CPU 的端口 5 上成为瓶颈。英特尔通常只发布吞吐量数字,而不是端口故障,这就是为什么你必须查看 Agner Fog 的表(或 IACA 输出)来做任何有意义的事情,如果你不只是重复相同的指令无数次。

通常,您可以假设最佳情况下的调度/分发,可以在其他端口上运行的微指令不会经常窃取繁忙的端口,但确实会发生一些。(确切地说,x86 微指令是如何安排的?

只看 CPI 是不够的;两条 CPI=1 指令可能会或可能不会竞争相同的执行端口。如果他们不这样做,他们可以并行执行。例如,Haswell 只能psadbw在端口 0 上运行(5c 延迟,1c 吞吐量,即 CPI=1),但它是单个 uop,因此 1 psadbw+ 3add条指令的混合可以维持每个时钟 4 条指令。在英特尔 CPU 的 3 个不同端口上有向量 ALU,一些操作在所有 3 个端口(例如布尔值)上复制,而一些仅在一个端口上复制(例如在 Skylake 之前的移位)。

有时你可以想出几种不同的策略,一种可能会降低延迟,但会花费更多的微指令。一个经典的例子是乘以常数,例如imul eax, ecx, 10(1 uop,3c 在 Intel 上的延迟)与lea eax, [rcx + rcx*4]/ add eax,eax(2 uop,2c 延迟)。现代编译器倾向于选择 2 LEA 与 1 IMUL,尽管最多 3.7 的首选 IMUL 除非它可以仅用一条其他指令完成工作。

请参阅在某个位置或更低位置计算设置位的有效方法是什么?有关实现功能的几种不同方法的静态分析示例。

另请参阅为什么 mulss 在 Haswell 上只需要 3 个周期,与 Agner 的指令表不同?(使用多个累加器展开 FP 循环)(最终比您从问题标题中猜到的更详细)以获得静态分析的另一个摘要,以及一些关于使用多个累加器展开以减少的简洁内容。

每个(?)功能单元都是流水线的

分频器在最近的 CPU 中是流水线的,但不是完全流水线的。(不过,FP 除法是单微指令,所以如果你将一个divps与几十个mulps/混合在一起addps,如果延迟无关紧要,它对吞吐量的影响可以忽略不计:浮点除法与浮点乘法rcpps+牛顿迭代的吞吐量更差和大约相同的延迟。

其他一切都在主流英特尔 CPU 上完全流水线化;单个 uop 的多周期(倒数)吞吐量。(变量计数整数移位,例如shl eax, cl其 3 微指令的吞吐量低于预期,因为它们通过标志合并微指令创建依赖关系。但是,如果您通过带有add或某物的 FLAGS 打破这种依赖关系,您可以获得更好的吞吐量和延迟。)

在 Ryzen 之前的 AMD 上,整数乘法器也只是部分流水线化的。例如 Bulldozer 的imul ecx, edx只有 1 uop,但延迟为 4c,吞吐量为 2c。

Xeon Phi (KNL) 也有一些不完全流水线的 shuffle 指令,但它往往会成为前端(指令解码)而不是后端的瓶颈,并且确实有一个小缓冲区 + OoO exec 能力来隐藏-结束气泡。

如果是浮点指令,则之前的每条浮点指令都已发出(浮点指令有静态指令重排序)

不。

也许您为 Silvermont 读过,它不对 FP/SIMD 执行 OoO exec,只有整数(有一个小的 ~20 uop 窗口)。也许一些 ARM 芯片也是这样,带有更简单的 NEON 调度程序?我不太了解 ARM uarch 的详细信息。

主流的大核微架构,如 P6 / SnB 系列,以及所有 AMD OoO 芯片,对 SIMD 和 FP 指令的 OoO exec 与整数相同。AMD CPU 使用单独的调度程序,但 Intel 使用统一的调度程序,因此它的完整大小可以应用于查找整数或 FP 代码中的 ILP,无论哪个当前正在运行。

即使是位于 silvermont 的 Knight's Landing(在 Xeon Phi 中)也为 SIMD 执行 OoO。

x86 通常对指令顺序不是很敏感,但是 uop 调度不做关键路径分析。因此,有时将指令先放在关键路径上可能会有所帮助,这样当其他指令在该端口上运行时,它们就不会等待输入准备就绪,从而导致稍后当我们到达需要结果的指令时出现更大的停顿关键路径。(即这就是为什么它是关键路径。)

我预测 Haswell 延迟的尝试如下所示:

是的,看起来没错。 shufps在端口 5 上addps运行,在 p1 上mulps运行,在 p0 或 p1 上运行。Skylake 丢弃了专用的 FP-add 单元,并在 p0/p1 上的 FMA 单元上运行 SIMD FP add/mul/FMA,所有延迟均具有 4c 延迟(从 Haswell 的 3/5/5 或 3/3/5 in布罗德威尔)。

这是一个很好的例子,说明为什么将整个 XYZ 方向向量保存在 SIMD 向量中通常很糟糕。 保留一个 X 数组、一个 Y 数组和一个 Z 数组,可以让您并行执行 4 个交叉乘积,而无需任何洗牌。

SSE 标签 wiki有一个指向这些幻灯片的链接:Insomniac Games (GDC 2015) 上的 SIMD,其中涵盖了 3D 向量的结构数组与数组结构问题,以及为什么总是尝试 SIMD 往往是错误的单个操作而不是使用 SIMD 并行执行多个操作。


推荐阅读