【问题标题】:Estimating Cycles Per Instruction估计每条指令的周期
【发布时间】:2023-12-17 01:45:02
【问题描述】:

我已经反汇编了一个使用 MSVC v140 编译的小型 C++ 程序,并试图估计每条指令的周期,以便更好地了解代码设计如何影响性能。我一直在关注 Mike Acton 在 "Data-Oriented Design and C++" 上的 CppCon 2014 演讲,特别是我链接到的部分。

在其中,他指出了以下几行:

movss   8(%rbx), %xmm1
movss   12(%rbx), %xmm0

然后他声称这些 2 x 32 位读取可能在同一高速缓存行上,因此大约需要大约 200 个周期

Intel 64 and IA-32 Architectures Optimization Reference Manual 是一个很好的资源,特别是“附录 C - 指令延迟和吞吐量”。然而,在 “表 C-16. 流式 SIMD 扩展单精度浮点指令”中的 C-15 页上,它声明 movss 只有 1 个周期(除非我我理解这里的延迟意味着什么是错误的......如果是这样,我该如何阅读这个东西?)

我知道theoretical prediction of execution time 永远不会是正确的,但学习这一点很重要。 这两个命令的 200 个周期如何,我如何学会推理超出这个 sn-p 的执行时间?

我已经开始阅读有关 CPU 流水线的一些内容......也许大部分周期都在那里被拾取?

PS:我对在这里实际测量硬件性能计数器不感兴趣。我只是想学习如何合理地阅读 ASM 和周期。

【问题讨论】:

  • 你看过 Agner Fog 的作品吗? agner.org/optimize/instruction_tables.pdf
  • 你不能再计算周期了。现在很久没有了。流水线、缓存、分支预测等……流水线就像工厂里的流水线一样。你可能有 117 个步骤或工位来建造一件东西,每个可能需要 30 秒,但这意味着理论上你可以每 30 秒生产一件产品,而不是因为生产线而每小时生产一件。这就是他们从后端出来的速度。
  • 大多数(如果不是所有)指令都需要一个时钟来执行,它们可能需要 15 到 1000 个时钟来通过缓存未命中获取相对较慢的 DRAM,并且所有其他步骤都在管道中,但是一旦所有输入存在于执行单元中,则流水线中的这一步骤需要一个时钟。然后是接下来的所有内容,将结果保存在寄存器或慢速内存等中。
  • 不要误会我的意思,你所做的事情有很多价值,了解编译器正在产生什么,如何操纵编译器来产生不同的东西。明白指令的数量不等于速度。内存指令比寄存器指令花费更长的时间,同样,如果你有缓存未命中,你可以有几十个寄存器指令运行一个基于内存的指令,所有其他的东西都保持不变。使用多个执行单元、管道,您可以重新排列不必按特定顺序排列的指令并改变整体性能
  • 和其他有趣的事情,例如添加一个或多个 nop 或删除它们以更改整个程序或部分程序的缓存对齐方式,并查看性能变化。

标签: performance assembly architecture x86 sse


【解决方案1】:

正如您已经指出的,MOVSS 指令的理论吞吐量和延迟为 1 个周期。您正在查看正确的文档 (Intel Optimization Manual)。 Agner Fog(在 cmets 中提到)在他的 Intruction Tables 中为 Intel CPU 测量了相同的数字(AMD 具有更高的延迟)。

这将我们引向第一个问题:您正在研究什么特定的微架构?即使对于同一个供应商,这也会产生很大的不同。 Agner Fog 报告称,MOVSS 在 AMD Bulldozer 上的延迟为 2-6cy,具体取决于源和目标(寄存器与内存)。在研究计算机架构的性能时,请牢记这一点。

200cy 最有可能是缓存未命中,正如已经指出的那样,它存在于 cmets 中。您从优化手册中获得的任何内存访问指令的数字都是假设数据驻留在一级缓存 (L1) 中的。现在,如果您从未通过之前的指令接触过数据,则需要将缓存行(Intel 和 AMD x86 为 64 字节)从内存加载到最后一级缓存,从那里形成二级缓存,然后进入 L1,最后进入 XMM 寄存器(在 1 个周期内)。在当前英特尔微架构上,L3-L2 和 L2-L1 之间的传输具有每个高速缓存行两个周期的吞吐量(不是延迟!)。并且内存带宽可用于估计 L3 和内存之间的吞吐量(例如,可实现 40 GB/s 内存带宽的 2 GHz CPU 将具有每个高速缓存行 3.2 个周期的吞吐量)。缓存线或内存块通常是最小的单位缓存和内存可以操作,它们在微架构之间有所不同,甚至可能在架构内有所不同,具体取决于缓存级别(L1、L2 等)。

现在这是所有吞吐量而不是延迟,这无法帮助您估计上面描述的内容。为了验证这一点,您需要一遍又一遍地执行指令(至少 1/10 秒)以获得周期精确测量。通过更改指令,您可以决定是否要测量延迟(通过包括指令之间的依赖关系)或吞吐量(通过使指令输入独立于先前指令的结果)。要测量缓存和内存访问,您需要预测访问是否会进入缓存,这可以使用layer conditions 来完成。

估计英特尔 CPU 指令执行(延迟和吞吐量)的工具是 Intel Architecture Code Analyzer,它支持多个微架构,最高支持 Haswell。延迟预测要小心谨慎,因为预测延迟比估计吞吐量要困难得多。

【讨论】:

  • IACA 通常必须与一粒盐一起服用。它假设没有缓存未命中,并且对其建模方式有一些限制。不过,IDK 为什么你说估计延迟比估计吞吐量更难。几乎所有指令都具有固定的延迟,因为这使得乱序调度程序的工作变得更加容易(就避免执行端口试图在同一周期内产生两个结果的回写冲突而言)。无论如何,由于 IACA 不模拟缓存未命中,因此延迟唯一棘手的问题是资源冲突,一旦输入准备好,就会阻止 insn 执行。
  • 您的权利。如果假设所有数据都在 L1 中可用,那么内核中的延迟并不比吞吐量更难估计。但是一旦它依赖于来自其他缓存级别或内存的负载,就很难预测它比吞吐量要远得多。这是由于延迟的概念,一个单一的延迟会增加整体延迟,而一个单一的延迟会降低吞吐量,仅轻微影响指令总数。所以,是的,IACA 预测必须始终持保留态度,但吞吐量的性质使其更容易出错。
  • 所以您是说 IACA 延迟数字比其吞吐量数字更不可能与现实相匹配。这是有道理的,只要你没有很多 L3 失误。主内存延迟足以使管道停止并影响吞吐量,但是如果内存延迟不是关键路径的一部分,即使 L2 未命中在 L3 中也可能不会对吞吐量产生太大影响。
  • 当然,但是从内存(以及从 L3 和 L2)到 L1 的吞吐量可以合理估计并且通常与测量值匹配。请参阅以下论文中的图 4:Automatic Loop Kernel Analysis and Performance Modeling With Kerncraft。 L1 到寄存器的吞吐量是使用 IACA 估计的,所有其他传输时间都是根据 Intel 的文档和内存带宽测量计算的。这不适合延迟。但是延迟通常会引起实时系统人员的兴趣,他们只担心最坏情况下的执行时间。