缓慢的jmp-instruction

时间:2016-08-07 07:23:03

标签: performance assembly x86 intel cpu-architecture

作为我的问题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>10^7),则循环的整个内容不适合指令缓存,因此必须一遍又一遍地加载,使得RAM的速度定义执行所需的时间。例如,对于较大的内部部分,xorl %eax, %eax(2个字节)比xorq %rax, %rax(3个字节)快33%。
  2. 如果NI很小并且整个循环很容易适合指令缓存,那么xorl %eax, %eaxxorq %rax, %rax速度相同,并且每个时钟周期可以执行4次。
  3. 然而,这个简单的模型不适用于jmp - 指令。对于jmp - 指令,我的测试代码如下所示:

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

    结果是:

    1. 对于“大”循环大小(已经用于NI>10^4),我测量4.2 ns / jmp - 指令(相当于从RAM加载的42个字节或在我的机器上大约12个时钟周期)。
    2. 对于小的环路大小(NI<10^3),我测量1 ns / jmp-指令(大约3个时钟周期,听起来似乎合理--Agner Fog的表显示了2个时钟周期的成本)。
    3. 指令jmp LX使用2字节eb 00编码。

      因此,我的问题:对于“大型”循环中jmp - 指令的高成本有什么解释?

      PS:如果您想在自己的计算机上试用,可以从here下载脚本,只需在 src中运行sh jmp_test.sh -folder。

      编辑:实验结果证实了彼得的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指令,一个(尚未知的)资源变得稀缺,这会导致ǸI大于4000的性能下降。
      2. 此资源不会与xor等指示共享 - 如果NIjmp之后执行xorje的性能下降仍然有效
      3. 但是,如果跳转,则jmp会共享此资源 - 对于je + NI之后,je的资源将变得稀缺,约为2000。 / LI>
      4. 但是,如果NI根本没有跳转,那么jmp大约4000(第4行)的资源就会变得稀少。
      5. Matt Godbolt's branch-prediction reverse engineering articles确定分支目标缓冲区容量为4096个条目。这是非常有力的证据表明BTB未命中是观察到小型和大型<Context docBase="C:\Images" path="/Images" /> 循环之间吞吐量差异的原因。

1 个答案:

答案 0 :(得分:8)

TL:DR:我目前的猜测是用尽了BTB(分支目标缓冲区)条目。见下文。

即使你的jmp是无操作,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预测下一个要获取的块,而不是获取块中特定分支的确切目标。因此,不必在获取块中预测所有分支的目标,the CPU just needs to predict the address of the next fetch.

是的,在运行像xor-zeroing这样的非常高吞吐量的东西时,内存带宽可能成为瓶颈,但是你会遇到与jmp不同的瓶颈。 CPU有时间从内存中获取42B,但这不是它正在做的事情。预取可以很容易地保持每3个时钟2个字节,因此L1 I-cache丢失应该接近于零。

在带有/不带REX测试的xor中,如果您使用足够大的环路测试不适合L3缓存,主内存带宽实际上可能是那里的瓶颈。我在一个~3GHz的CPU上每个周期消耗4 * 2B,这大约可以达到25GB / s的DDR3-1600MHz。然而,即使是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这样的更详细的内容,那么您很快就会陷入困境。 s Haswell microarch overview,或者我之前链接过的类似Sandybridge的文章。

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