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核心的方法。
在英特尔术语中:
很多其他的计算机体系结构文献使用相反的术语,但这是您将在英特尔的优化手册中找到的术语,以及硬件性能计数器的名称,如或。
任意算术 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并行执行多个操作通常是错误的。