简短的回答是,对于控制指令和许多孤立的 mov 指令,延迟在实践中并不是一个真正有意义的指标。
在您提到的评论中:
我指的是英特尔的控制说明手册。我所说的控制指令的平均延迟是指我们得到一些关于在一段时间内退役的指令数量的数据,然后花费时间/(指令数量)。
当我们谈论指令的延迟时,我们通常指的是从其输入中产生结果所花费的时间,而不是在给定的时间段内可以产生多少结果。这是一个需要 9 个月才能生下一个婴儿(潜伏期)与一个城市一个月内出生 100 个婴儿(吞吐量)之间的差异。
测量延迟的常用方法是将一系列指令链接在一起,其中一条指令的输出用作下一条指令的输入。由于它们是相互依赖的,因此您会得到延迟测量,因为它们是串行执行的。例如,如果您想测量 的延迟add
,您可以使用如下序列:
add eax, eax
add eax, eax
add eax, eax
...
注意输出寄存器eax
是如何在输入中反馈到下一个的add
。
现在,控制流指令没有明显的明确“输出”可以反馈到它们的输入中。它们的输出是指令流的变化,但不清楚如何将其反馈到下一条指令中。此外,控制流的整个机制通常被解耦到一个分支预测引擎中,该引擎试图在控制流指令执行之前很久就正确地引导前端,从而在延迟方面进一步搅浑水。
充其量您可以谈论这些构造的吞吐量:现代英特尔通常每个周期可以执行两个分支,最多可以采用其中一个。
mov
您在使用内存指令或从内存指令时遇到了同样的问题。在这里,输出和输入很清楚,但它们存在于不同的域中(寄存器与内存)。因此,您不一定将存储指令的输出提供给后续存储指令,因为存储具有“内存”输出但“寄存器”输入。您可以做的是将同一位置上的加载和存储指令对链接在一起,并获得该对的组合延迟:这通常在现代英特尔上运行 3 到 7 个周期,具体取决于寻址模式和其他因素。
特别是对于加载,您可以在地址计算中使用加载的结果(寄存器域)进行下一次加载,从而为您提供加载到加载地址的延迟(有些人称之为加载使用,但我考虑到这令人困惑),在现代 Intel 上通常最多 4 个周期,对于复杂的寻址模式或向量加载,每个周期都需要 1 个额外的周期。
对于寄存器到寄存器的移动,延迟通常是零周期(由于 mov 消除),或者 mov 无法消除时的 1 个周期。
这些问题可能就是为什么您在 Intel 的指南中,甚至在 Agner 等其他指南中都看不到这些结构的延迟数据。