现代超标量处理器操作的延迟预测需要考虑哪些因素?我如何手动计算它们?

19

我希望能够手动预测任意算术(即无分支或存储器,尽管这也很好)x86-64汇编代码在特定架构下需要多长时间,考虑指令重排序、超标量、延迟、CPI等因素。

为实现此目的,必须遵循哪些规则?


我想我已经制定了一些初步的规则,但是我没有找到任何有关将示例代码分解到此详细程度的参考资料,因此我不得不猜测一些内容。(例如,英特尔优化手册几乎没有提到指令重排序。)

至少,我要查找:(1) 确认每个规则是否正确,或者正确说明每个规则;(2) 我可能忘记的任何规则列表。

  • 尽可能多的指令在每个周期中发出,从当前周期按顺序开始,可能一直到重新排序缓冲区大小为止。
  • 如果满足以下条件,则可以在给定周期内发出指令:
    • 影响其操作数的所有指令都没有被执行。而且:
    • 如果它是浮点指令,则在它之前的每个浮点指令都已发出(浮点指令具有静态指令重排序)。而且:
    • 对于该指令,在该周期上有一个可用的功能单元。每个(?)功能单元都是流水线化的,这意味着每个循环可以接受1条新指令,并且总功能单元数为给定函数类别的CPI的倒数(不确定这里:可能例如addpssubps使用相同的功能单元?如何确定此信息?)。而且:
    • 已经发出的指令数少于超标量宽度(通常为4)。
  • 如果没有指令可发布,则处理器不会发布任何指令,这种情况被称为“停顿”。

以以下示例代码(计算叉积)为例:

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`)

无法准确预测,因为很难预测管道在执行开始时处于什么状态。我认为英特尔发布了一种可以为您执行此类分析的工具,但上述警告适用。 - fuz
1
也许@fuz指的是iaca?它可以帮助您在Intel微架构上静态分析代码片段的数据依赖性、吞吐量和延迟。 - David Wohlferd
在我看来,这是一个非常有趣的问题!特别是如果目的是拥有一组流程图来指导分析师。不幸的是,我认为制作这样的答案是一项非常耗时的任务。总的原则是关于“资源可用性”的。最好的方法是草拟一个模型,并通过PM测量进行改进。 - Margaret Bloom
1
iaca 的第二个插头:这里有一个 SO 问题链接,我认为它比英特尔的页面更好地描述了它的功能。特别是,它展示了 iaca 生成的输出。通过这个,你可以决定它是否符合你的需求,而不必下载它并弄清楚如何使用。仅供参考。 - David Wohlferd
@DavidWohlferd 那就是那个工具! - fuz
值得一提的是英特尔软件优化手册:https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html#inpage-nav-5 - Andrew Tomazos
1个回答

21
TL:DR: 寻找依赖链,特别是循环依赖。对于长时间运行的循环,请查看哪个延迟、前端吞吐量或后端端口争用/吞吐量最糟糕。如果没有缓存未命中或分支预测错误,则每次迭代循环可能需要多少个周期。 延迟边界和处理器吞吐量边界是在具有两个依赖链的特定循环中分析循环依赖链的一个很好的例子,其中一个依赖链从另一个依赖链中提取值。
相关:每个汇编指令需要多少CPU周期?是介绍基于每个指令的吞吐量与延迟的良好入门,以及对多个指令序列的含义。还可参见汇编 - 如何按延迟和吞吐量评分CPU指令以了解如何测量单个指令。
这被称为静态(性能)分析。维基百科说(https://en.wikipedia.org/wiki/List_of_performance_analysis_tools),AMD的AMD CodeXL有一个“静态内核分析器”(即针对计算内核,也称为循环)。我从未尝试过它。
英特尔还有一个免费工具,用于分析循环如何通过Sandybridge系列CPU的流水线:什么是IACA,我该如何使用它? IACA并不差,但存在错误(例如,在Sandybridge上使用了错误的shld数据,并且据我所知,它不知道Haswell/Skylake可以在某些指令中保持索引寻址模式微融合。但是现在英特尔已将此添加到其优化手册中,也许这种情况会改变)。 IACA对于计算前端uop以查看您距瓶颈有多近并不有帮助(它只喜欢给您未融合域uop计数)。
静态分析通常很好,但一定要通过性能计数器进行剖析来检查。请参见x86的MOV是否真的“免费”?为什么我完全无法复制这个结果?,以了解探究微架构特性的简单循环的剖析示例。

必读:

Agner Fog的微架构指南(第2章:乱序执行)解释了一些基本的依赖关系链和乱序执行。他的“优化汇编”指南还有更多初级和高级性能知识。

他的微架构指南的后几章涵盖了像Nehalem、Sandybridge、Haswell、K8/K10、Bulldozer和Ryzen等CPU中管道的详细信息。 (以及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/

请参阅the x86 tag wiki中的其他性能链接。

我也尝试解释了CPU核心如何寻找和利用此答案中的指令级并行性,但我认为您已经掌握了这些基础知识,只要与调整软件有关。我提到了SMT(超线程)是一种将更多ILP暴露给单个CPU核心的方法。

在英特尔术语中:

  • "issue" 表示将uop发送到核心的乱序部分;与寄存器重命名一起,这是前端的最后一步。发射/重命名阶段通常是管道中最狭窄的点,例如英特尔Core2上的4宽度。(随着后来的uarches(如Haswell和特别是Skylake)在某些真实代码中实际上非常接近于此,这要归功于SKL改进的解码器和uop-cache带宽以及后端和缓存带宽的提高。)这是融合域uops:微融合允许您通过前端发送2个uops并仅占用一个ROB条目。(我能够在Skylake上构建一个循环,每个时钟周期支持7个未融合域uops)。另请参见http://blog.stuffedcow.net/2013/05/measuring-rob-capacity/有关乱序窗口大小的信息。

  • "dispatch" 表示调度程序将uop发送到执行端口。只要所有输入都准备好,并且相关的执行端口可用,就会发生这种情况。x86 uops如何被调度?调度发生在“未融合”域中;微融合uops在OoO调度程序(也称为预约站,RS)中单独跟踪。

很多其他的计算机体系结构文献使用相反的术语,但这是您将在英特尔的优化手册中找到的术语,以及硬件性能计数器的名称,如或。
任意算术 x86-64 汇编代码需要多长时间取决于周围的代码,因为 OoO 执行。您的最终 subps 结果在 CPU 开始运行后的其他指令之前不一定要准备好。延迟只对稍后需要该值作为输入的指令有影响,而不对整数循环等产生影响。有时吞吐量很重要,而乱序执行可以隐藏多个独立短依赖链的延迟。(例如,如果您正在对多个向量的大型数组执行相同的操作,则多个叉积可以同时进行。)即使按程序顺序,在做下一个迭代之前完成所有一个迭代的工作,也会有多个迭代同时在进行中。(如果 OoO exec 在硬件中难以进行所有重新排序,则软件流水线可以帮助高延迟循环体。)
对于短块,有三个主要维度需要分析。通常只有其中一个是特定用例的瓶颈。通常您正在查看将用作循环的块的一部分,而不是整个循环体,但 OoO exec 通常足够好,您可以将这些数字相加,以获取几个不同块的结果,如果它们不太长,以至于 OoO 窗口大小无法找到所有 ILP。
  • 每个输入到输出之间的延迟。查看每个输入到每个输出的依赖链上的指令。例如,其中一个选择可能需要更快地准备好一个输入。
  • 总uop计数(用于前端吞吐量瓶颈),Intel CPU中的融合域。例如,Core2及更高版本理论上可以每个时钟周期发出/重命名4个融合域uop到乱序调度器/ROB。Sandybridge系列通常可以通过uop缓存和循环缓冲区实现这一点,特别是Skylake具有改进的解码器和uop缓存吞吐量。
  • 每个后端执行端口的uop计数(未融合域)。例如,以洗牌为主的代码通常会在Intel CPU上的端口5上成为瓶颈。Intel通常只发布吞吐量数字,而不是端口分解,这就是为什么您必须查看Agner Fog的表格(或IACA输出),以便做出有意义的事情,如果您不只是重复相同的指令一亿次。

通常情况下,您可以假设最佳调度/分配,使用可以在其他端口运行的uop不会经常窃取繁忙端口,但确实会发生一些情况。(x86 uop的调度方式是什么?

仅查看CPI是不够的;两个CPI=1的指令可能会竞争相同的执行端口。如果它们没有竞争,则可以并行执行。例如,Haswell只能在端口0上运行psadbw(5c延迟,1c吞吐量,即CPI=1),但它是单个uop,因此1个psadbw + 3个add指令的混合可以维持每个时钟4个指令。Intel CPU上有3个不同端口的向量ALU,其中一些操作在所有3个端口上复制(例如布尔运算),而有些操作仅在一个端口上(例如Skylake之前的移位)。
有时您可以提出几种不同的策略,一种可能具有更低的延迟,但代价更高的uops。一个经典的例子是像imul eax,ecx,10这样的常数乘法(Intel上的1个uop,3c延迟)与lea eax,[rcx + rcx * 4] / add eax,eax(2个uops,2c延迟)。现代编译器倾向于选择2个LEA而不是1个IMUL,尽管clang直到3.7都更喜欢IMUL,除非它只能用单个其他指令完成工作。
请参见什么是计算位于某个位置或更低位置的位的有效方法?,其中介绍了几种不同实现函数的静态分析示例。
此外,请参见为什么Haswell上mulss只需要3个周期,而与Agner的指令表不同?(使用多个累加器展开FP循环)(这篇文章比问题标题所示的更详细),其中还有一些关于使用多个累加器进行归约的有趣内容。

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

在最近的CPU中,除法器是流水线化的,但不是完全流水线化。(FP除法是单uop的,因此如果您在数十个mulps / addps中混合使用一个divps,则其吞吐量可以忽略不计,如果延迟不重要:浮点除法与浮点乘法rcpps +牛顿迭代的吞吐量更差,但延迟大致相同。)
在主流的Intel CPU上,其他所有内容都是完全流水线化的;单个uop的多周期(倒数)吞吐量。 (变量计数整数移位,如shl eax,cl,其3个uop的吞吐量低于预期,因为它们通过标志合并uop创建了依赖性。但是,如果您通过add或其他方法打破FLAGS的依赖性,可以获得更好的吞吐量和延迟。)
在Ryzen之前的AMD上,整数乘法器也仅部分流水线化。例如,Bulldozer的imul ecx,edx只有1个uop,但具有4c延迟,2c吞吐量。
Xeon Phi(KNL)还具有一些未完全流水线化的洗牌指令,但它往往会在前端(指令解码)上出现瓶颈,而不是后端,并且具有小缓冲区+ OoO执行能力以隐藏后端气泡。

如果它是浮点指令,则在其之前发出了每个浮点指令(浮点指令具有静态指令重新排序)

不是这样的。
也许您读到了Silvermont的情况,它不对FP / SIMD执行OoO exec,只对整数执行(具有小约20个uop窗口)。也许一些ARM芯片也是如此,具有更简单的NEON调度程序?我不太了解ARM uarch细节。
主流的大核微架构,如P6 / SnB系列和所有AMD OoO芯片,对SIMD和FP指令的OoO执行与整数指令相同。 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或Broadwell的3/3/5上下)。
这是为什么在SIMD向量中保留整个XYZ方向向量通常很糟糕的一个很好的例子。保持X数组,Y数组和Z数组将使您可以同时执行4个叉积,而无需任何洗牌。 SSE标签维基中有一个链接指向这些幻灯片:Insomniac Games的SIMD(GDC 2015),其中涵盖了针对3D向量的结构数组与数组结构体之间的问题,并解释了为什么总是尝试对单个操作进行SIMD而不是使用SIMD并行执行多个操作通常是错误的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接