缓慢的jmp

跟着我的问题在x86-64中使用32位寄存器/指令的优势,我开始测量指令的成本。 我知道这已经被多次完成(例如Agner Fog),但我正在为了娱乐和自我教育而努力。

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

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

但还是应该考虑一些事情。

  • 如果循环的内部部分很大( NI>10^7 ),循环的全部内容都不适合指令高速缓存,因此必须反复加载,从而使RAM的速度定义了所需的时间执行。 例如,对于较大的内部部分, xorl %eax, %eax (2个字节)比xorq %rax, %rax (3个字节)快33%。
  • 如果NI很小,并且整个循环很容易进入指令缓存,比xorl %eax, %eaxxorq %rax, %rax同样快,并且可以在每个时钟周期执行4次。
  • 然而这个简单的模型并不适用于jmp指令。 对于jmp instruction,我的测试代码如下所示:

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

    结果是:

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

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

    PS:如果你喜欢在你的机器上试用它,你可以从这里下载脚本,只需在src-folder中运行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|
    

    可以被看见:

  • 对于jmp指令,一个(但未知的)资源变得稀缺,这会导致性能ǸI降低,大于4000。
  • 此资源不与这样的指令作为共享xor在性能退化踢仍然- NI约4000,如果jmpxor是在彼此之后执行。
  • 但是这个资源是与je共享的,如果进行跳转 - 对于jmp + je ,彼此之后,资源对于NI大约2000年变得稀缺。
  • 然而,如果je没有跳跃,那么资源正在再次变得稀缺,因为NI约为4000(第四条线)。
  • Matt Godbolt的分支预测反​​向工程文章确定分支目标缓冲区容量为4096个条目。 这是非常有力的证据,表明BTB未命中是观察到小型和大型jmp循环之间吞吐量差异的原因。


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


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

    要了解有关跳转及其对流水线CPU的影响的更多信息,经典RISC管道中的控制危害应该很好地介绍为什么分支机构对于流水线CPU来说很困难。 Agner Fog的指南解释了实际意义,但我认为假设了一些这样的背景知识。


    您的英特尔Broadwell CPU具有一个uop-cache,用于缓存已解码的指令(与32kiB L1 I-cache不同)。

    uop缓存大小为32组8种方式,每组有6个uops,总共1536个uops(如果每行都包含6个uops;完美的效率)。 1536个uops在1000到10000个测试大小之间。 在你编辑之前,我预测缓慢到快速的截止时间在你的循环中大约有1536个指令。 在超过1536条指令之前,它一点都不放慢速度,所以我认为我们可以排除uop-cache效应。 这不像我想的那样简单。 :)

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

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


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

    缓存某个代码块中存在分支的事实及其目标地址,允许前端在jmp rel32编码实际解码之前开始从分支目标中获取代码。 请记住,解码可变长度的x86指令非常困难:您不知道一条指令从哪里开始,直到前一条指令被解码。 所以你不能只是模式匹配的指令流寻找无条件的跳转/调用只要它被提取。

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

    另请参见分支目标缓冲区检测到哪个分支预测失误? 这有一个很好的答案,并在这Realworldtech线程讨论。

    一个非常重要的观点:BTB预测下一个块的取值,而不是取块中特定分支的确切目标。 因此,CPU不必预测提取块中所有分支的目标,而只需要预测下一个提取的地址。


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

    在使用/不使用REX测试的xor ,如果您测试了足够大的循环而不适合L3缓存,则主内存带宽实际上可能会成为瓶颈。 我在〜3GHz的CPU上每个周期消耗4 * 2B,这在DDR3-1600MHz的25GB / s时最大。 不过,即使是L3缓存也足够快,能够保持每个周期4 * 3B的速度。

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


    还要注意的是,在核心时钟周期中测量时间更加正常。 但是,当你在查看内存时,你的测量结果很有用,我猜,因为低节能的时钟速度会改变核心时钟速度与内存速度的比率。 (即在最低CPU时钟速度下内存瓶颈问题不大。)

    要在时钟周期中进行基准测试,请使用perf stat ./a.out 。 还有其他有用的性能计数器对于理解性能特性至关重要。

    有关Core2的性能计数器结果(每个jmp 8个周期)以及一些未知的微体系结构(每个jmp约10c),请参阅x86-64相对jmp性能。


    即使在更多或更少的白盒条件下(阅读英特尔的优化手册以及他们已经发布的有关CPU内部部件的内容),现代CPU性能特性的细节仍然很难理解。 如果你坚持进行黑盒测试,你不会阅读诸如关于新CPU设计的arstechnica文章等东西,或者像David Kanter的Haswell微观概述或类似的更详细的东西,桑迪布里克写我早先链接。

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

    链接地址: http://www.djcxy.com/p/72415.html

    上一篇: Slow jmp

    下一篇: What does the compiler do in this assembly code?