【问题标题】:Slow jmp-instruction慢 jmp 指令
【发布时间】:2016-12-13 04:58:23
【问题描述】:

在回答我的问题The advantages of using 32bit registers/instructions in x86-64 时,我开始衡量指令的成本。我知道这已经多次完成(例如Agner Fog),但我这样做是为了娱乐和自我教育。

我的测试代码非常简单(为了简单起见,这里是伪代码,实际上是在汇编程序中):

for(outer_loop=0; outer_loop<NO;outer_loop++){
    operation  #first
    operation  #second
    ...
    operation #NI-th
} 

但还是需要考虑一些事情。

  1. 如果循环的内部部分很大(大NI&gt;10^7),则循环的整个内容都无法放入指令缓存中,因此必须一遍又一遍地加载,从而使RAM的速度决定了时间执行所需。例如,对于大型内部零件,xorl %eax, %eax(2 字节)比 xorq %rax, %rax(3 字节)快 33%。
  2. 如果NI 很小并且整个循环很容易放入指令缓存中,那么xorl %eax, %eaxxorq %rax, %rax 速度相同,每个时钟周期可以执行4 次。

但是,这个简单的模型不适用于jmp-指令。对于jmp-指令,我的测试代码如下所示:

for(outer_loop=0; outer_loop<NO;outer_loop++){
    jmp .L0
    .L0: jmp .L1
    L1: jmp L2
    ....
}

结果是:

  1. 对于“大”循环大小(已用于 NI&gt;10^4),我测量 4.2 ns/jmp-instruction(相当于从 RAM 加载 42 个字节或在我的机器上大约 12 个时钟周期)。
  2. 对于小循环大小 (NI&lt;10^3),我测量为 1 ns/jmp-instruction(大约 3 个时钟周期,这听起来很合理 - Agner Fog 的表格显示成本为 2 个时钟周期)。

指令jmp LX使用2字节eb 00编码。

因此,我的问题是:“大”循环中jmp-instruction 的高成本可能是什么原因?

PS:如果你想在你的机器上试用它,你可以从here下载脚本,在src文件夹中运行sh jmp_test.sh


编辑:实验结果证实了彼得的 BTB 大小理论。

下表显示了不同ǸI 值(相对于NI=1000)的每条指令周期:

|oprations/ NI        | 1000 |  2000|  3000|  4000|  5000| 10000|
|---------------------|------|------|------|------|------|------|
|jmp                  |  1.0 |  1.0 |  1.0 |  1.2 |  1.9 |   3.8|
|jmp+xor              |  1.0 |  1.2 |  1.3 |  1.6 |  2.8 |   5.3|
|jmp+cmp+je (jump)    |  1.0 |  1.5 |  4.0 |  4.4 |  5.5 |   5.5|
|jmp+cmp+je (no jump) |  1.0 |  1.2 |  1.3 |  1.5 |  3.8 |   7.6|

可见:

  1. 对于 jmp 指令,(未知的)资源变得稀缺,这导致大于 4000 的 ǸI 的性能下降。
  2. 此资源不与诸如xor 之类的指令共享 - 如果jmpxor 相继执行,则NI 的性能下降仍然会持续大约4000。
  3. 但是如果进行跳转,则此资源与je 共享 - 对于jmp+je,资源在NI 之间变得稀缺大约2000 年。
  4. 但是,如果je 根本不跳转,则资源再次变得稀缺,NI 大约为 4000(第 4 行)。

Matt Godbolt's branch-prediction reverse engineering articles 确定分支目标缓冲区容量为 4096 个条目。这是一个非常有力的证据,表明 BTB 未命中是观察到的小型和大型 jmp 循环之间的吞吐量差异的原因。

【问题讨论】:

  • 名称在调试信息中。发布的可执行文件在任何地方都没有标签名称。
  • 请注意,xorq %rax,%raxxorl %eax,%eax 的作用完全相同,因此几乎没有理由使用前者(除非可能避免在某处插入 nop 进行对齐)。
  • 您的“大型”10,000 条指令循环很容易适应现代处理器 (256K) 的二级缓存,因此您无需测量 RAM 的速度。
  • @RossRidge 你是对的,对于movxor,我需要在循环中执行 10^7 指令才能看到“RAM 速度”。然而jmp 从 10^3 到 10^4 变慢了 4 倍。我并不是说这是因为 RAM - 它是不同的东西,但我不太清楚它是什么。
  • 您可能已经理解了它(因为您首先编写了该测试用例),但它可能需要明确 - 您的 jmp+cmp+je (no jump) 案例直到大约 4,000 次跳转才会达到资源稀缺的原因是因为未进行的跳转不会消耗 BTB 条目(实际上,BTB 中不会有任何内容!)。

标签: assembly x86 intel cpu-architecture branch-prediction


【解决方案1】:

TL:DR:我目前的猜测是 BTB(分支目标缓冲区)条目已用完。流水线代码提取需要在无条件分支被解码之前预测它的存在。见下文。

2021 年更新:https://blog.cloudflare.com/branch-predictor/ 详细探讨了这一点,使用 jmp next_insn 块作为实验。例如,分支密度和别名(相对于 64 字节行的相同偏移量)可能很重要。


即使您的jmps 没有操作,CPU 也没有额外的晶体管来检测这种特殊情况。它们的处理方式与任何其他 jmp 一样,这意味着必须从新位置重新开始指令获取,从而在管道中创建气泡。

要了解有关跳转及其对流水线 CPU 的影响的更多信息,Control Hazards in a classic RISC pipeline 应该很好地介绍了为什么分支对于流水线 CPU 来说是困难的。 Agner Fog 的指南解释了实际含义,但我认为假设了一些此类背景知识。


您的 Intel Broadwell CPU has a uop-cache,用于缓存解码的指令(与 32kiB L1 I-cache 分开)。

uop缓存大小为32组8路,每行6个uop,一共1536个uop(如果每行打包6个uop,效率完美)。 1536 uops 介于 1000 和 10000 测试大小之间。在您编辑之前,我预测从慢到快的截止值将在您的循环中大约 1536 条指令。直到远远超过 1536 条指令它才会减速,所以我认为我们可以排除 uop-cache 影响。这不是我想的那么简单的问题。 :)

从 uop-cache(小代码大小)而不是 x86 指令解码器(大循环)运行意味着在识别jmp 指令的阶段之前有更少的流水线阶段。因此,即使预测正确,我们也可能会预期来自不断跳跃的气泡会更小。

从解码器运行应该会产生更大的分支错误预测惩罚(比如可能是 20 个周期而不是 15 个),但这些不是错误预测的分支。


即使 CPU 不需要预测分支是否被采用,它仍然使用分支预测资源来预测代码块在解码之前是否包含已采用的分支。

缓存某个代码块中存在分支的事实及其目标地址,允许前端在实际解码jmp rel32 编码之前开始从分支目标获取代码。请记住,解码可变长度 x86 指令很困难:在前一条指令被解码之前,您不知道一条指令从哪里开始。因此,您不能只对指令流进行模式匹配,以便在获取后立即寻找无条件跳转/调用。

我目前的理论是,当您用完分支目标缓冲区条目时,您会放慢速度。

另请参阅What branch misprediction does the Branch Target Buffer detect? 有一个很好的答案,并在此Realworldtech thread 中进行讨论。

非常重要的一点:BTB 预测下一个要提取的块,而不是提取块中特定分支的确切目的地。因此,不必预测 fetch 块中所有分支的目标,the CPU just needs to predict the address of the next fetch.


是的,当运行异或归零等高吞吐量的东西时,内存带宽可能是一个瓶颈,但你遇到了jmp 的另一个瓶颈。 CPU 将有时间从内存中获取 42B,但这不是它正在做的事情。预取可以轻松地跟上每 3 个时钟 2 个字节的速度,因此 L1 I-cache 未命中率应该接近于零。

在您的 xor 有/无 REX 测试中,如果您使用足够大的循环进行测试以不适合 L3 缓存,则主内存带宽实际上可能是那里的瓶颈。我在 ~3GHz CPU 上每个周期消耗 4 * 2B,这几乎可以超过 DDR3-1600MHz 的 25GB/s。不过,即使是 L3 缓存也足够快,可以跟上每个周期 4 * 3B 的速度。

有趣的是,主内存 BW 是瓶颈;我最初猜想解码(以 16 字节为单位)将成为 3 字节 XOR 的瓶颈,但我猜它们已经足够小了。


另请注意,以核心时钟周期测量时间更为正常。但是,我猜,当您查看内存时,以 ns 为单位的测量值很有用,因为用于节能的低时钟速度会改变核心时钟速度与内存速度的比率。 (即在最低 CPU 时钟速度下,内存瓶颈问题不大。)

对于时钟周期的基准测试,请使用perf stat ./a.out。还有其他有用的性能计数器对于尝试了解性能特征必不可少

请参阅 x86-64 Relative jmp performance 了解 Core2 的性能计数器结果(每个 jmp 8 个周期),以及一些未知的微架构,每个 jmp 大约 10c。


即使在或多或少的白盒条件下(阅读英特尔的优化手册,以及他们发布的有关 CPU 内部结构的内容),现代 CPU 性能特征的细节也很难理解。如果您坚持进行黑盒测试,而您不会阅读诸如有关新 CPU 设计的 arstechnica 文章之类的内容,或者可能是诸如 David Kanter 的 Haswell microarch overview 之类的更详细的内容,或者类似的我之前链接的 Sandybridge 文章。

如果早点卡住并且经常卡住是可以的,而且你玩得很开心,那么一定要继续做你正在做的事情。但是如果你不知道这些细节,人们就很难回答你的问题,就像在这种情况下一样。 :/例如我这个答案的第一个版本假设您已经阅读了足够的内容以了解 uop 缓存是什么。

【讨论】:

  • 感谢您的回答。我不太清楚你所说的 uop-cache 是什么意思:操作缓存(在我的机器 i-7 上应该是 32kB)或预取队列(我猜我的机器有一个,不知道有多大)?
  • 在我的例子中 jmp 只是一个 2 字节的 nop。无需将新操作提取到预取队列中,因此我不确定气泡是否是缓慢的原因。对于较小的代码大小,这些气泡也会成为问题 - 但事实并非如此。
  • @ead:在我的情况下,jmp 只是一个 2 字节的 nop:是的,但是 CPU 没有针对这种无用的特殊情况进行任何优化。它仍然像正常的 jmp 一样运行它,需要从新位置重新启动指令获取 + 解码。
  • 是的,您在现代 CPU 上基本上有两个独立的分支预测资源 - 众所周知的“分支方向”预测器,需要在条件分支上采取与未采取的决定,以及 BTB。这些“分支”资源中的第二个对于所有类型的跳转都是必需的——包括所有无条件跳转,例如jmpcall,以及条件跳转和间接跳转。即使分支目标是一个常数,解码管道中也没有魔法可以让前端重新引导到跳转到的位置——它依赖于 BTB。
  • 是的,这是有道理的。我请here 上的专家权衡。在一些点会检测到分支并重新引导获取,但我认为您的问题是,多早?甚至可能在解码之前(您最初的想法)?如果不是,它是在/周围解码吗?还是必须一直等到执行(即,与分支错误预测一样糟糕)?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-04-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多