现代 CPU 是复杂的野兽,使用 pipelining、superscalar execution 和 out-of-order execution 以及其他使性能分析变得困难的技术...但并非不可能!
虽然您不能再简单地将指令流的延迟加在一起来获得总运行时间,但您仍然可以(通常)高度准确地分析某些代码(尤其是循环)的行为,如所述在下方和其他链接资源中。
指令时序
首先,您需要实际的时间安排。这些因 CPU 架构而异,但目前 x86 时序的最佳资源是 Agner Fog 的 instruction tables。这些表涵盖不少于 30 个 种不同的微架构,列出了指令延迟,这是一条指令从准备好输入到可用输出所花费的最小/典型时间。用阿格纳的话来说:
延迟:这是指令在一个
依赖链。数字是最小值。缓存未命中,
未对准,异常可能会增加时钟计数
相当。在启用超线程的情况下,使用相同的
另一个线程中的执行单元会导致性能下降。
非正规数、NAN 和无穷大不会增加延迟。这
使用的时间单位是核心时钟周期,而不是参考时钟周期
由时间戳计数器给出。
因此,例如,add 指令的延迟为一个周期,因此一系列相关添加指令,如图所示,每个 add 的延迟为 1 个周期:
add eax, eax
add eax, eax
add eax, eax
add eax, eax # total latency of 4 cycles for these 4 adds
请注意,这并不意味着add 指令每个只需要 1 个周期。例如,如果添加指令不依赖,那么在现代芯片上,所有 4 条添加指令都可以在同一周期内独立执行:
add eax, eax
add ebx, ebx
add ecx, ecx
add edx, edx # these 4 instructions might all execute, in parallel in a single cycle
Agner 提供了一个衡量这种潜在并行性的指标,称为互惠吞吐量:
倒数吞吐量:对于一系列独立的同类指令,每条指令的平均核心时钟周期数
在同一个线程中。
对于add,这被列为0.25,这意味着每个周期最多可以执行4条add指令(提供1 / 4 = 0.25的倒数吞吐量)。
吞吐量倒数也暗示了指令的流水线能力。例如,在最近的 x86 芯片上,imul 指令的常见形式有 3 个周期的延迟,并且在内部只有一个执行单元可以处理它们(不像 add 通常有四个可添加单元)。然而,观察到的一长串独立imul 指令的吞吐量是 1 个/周期,而不是您可能期望的每 3 个周期 1 个,因为延迟为 3。原因是 imul 单元是流水线的:它可以 开始一个新的imul每个循环,即使之前的乘法还没有完成。
这意味着一系列独立imul指令每个周期最多可以运行1个,但是一系列相关imul指令将只运行1个每 3 个周期(因为下一个 imul 在前一个的结果准备好之前无法启动)。
因此,有了这些信息,您就可以开始了解如何分析现代 CPU 上的指令时序了。
详细分析
不过,以上内容只是表面上的。您现在可以通过多种方式查看一系列指令(延迟或吞吐量),可能不清楚使用哪种方式。
此外,上述数字还没有涵盖其他限制,例如某些指令在 CPU 内竞争相同资源的事实,以及可能导致 CPU 流水线其他部分的限制(例如指令解码)总吞吐量比您仅通过查看延迟和吞吐量计算的要低。除此之外,您还有“ALU 之外”的因素,例如内存访问和分支预测:整个主题本身——您可以很好地对这些进行建模,但这需要工作。例如这里是recent post,其中的答案详细涵盖了大多数相关因素。
涵盖所有细节会使这个已经很长的答案的大小增加 10 倍或更多,所以我只会为您指出最佳资源。 Agner Fog 有一个 Optimizing Asembly guide,它详细介绍了包含十几个指令的循环的精确分析。请参阅“12.7矢量循环中的瓶颈分析示例”,该示例从 PDF 当前版本的第 95 页开始。
基本思想是创建一个表,每条指令有一行,并标记每条指令使用的执行资源。这让您可以看到任何吞吐量瓶颈。此外,您需要检查循环中携带的依赖项,看看是否有任何限制吞吐量(有关复杂情况,请参阅“12.16 分析依赖项”)。
如果您不想手动进行,英特尔发布了Intel Architecture Code Analyzer,这是一个自动执行此分析的工具。它目前尚未在 Skylake 之外进行更新,但对于 Kaby Lake 而言,结果在很大程度上仍然是合理的,因为微架构没有太大变化,因此时间保持可比性。 This answer 介绍了很多细节并提供了示例输出,user's guide 也不错(尽管相对于最新版本来说它已经过时了)。
其他来源
Agner 通常会在新架构发布后不久提供它们的时序,但您也可以查看instlatx64,在InstLatX86 和InstLatX64 结果中查看类似组织的时序。结果涵盖了很多有趣的旧筹码,而新筹码通常会很快出现。结果与 Agner 的结果基本一致,除了一些例外。您还可以在此页面上找到内存延迟和其他值。
您甚至可以在 附录 C:指令延迟和吞吐量中的 IA32 and Intel 64 optimization manual 中直接从 Intel 获得计时结果。我个人更喜欢 Agner 的版本,因为它们更完整,通常在英特尔手册更新之前到达,并且更易于使用,因为它们提供了电子表格和 PDF 版本。
最后,x86 tag wiki 拥有丰富的 x86 优化资源,包括指向如何对代码序列进行循环精确分析的其他示例的链接。
如果您想深入了解上述“数据流分析”的类型,我建议您使用A Whirlwind Introduction to Data Flow Graphs。