缓慢的jmp指令

ead*_*ead 6 performance x86 assembly intel cpu-architecture

作为我的问题后续问题在x86-64中使用32位寄存器/指令的优点,我开始测量指令的成本.我知道这已经多次完成了(例如Agner Fog),但我这样做是为了娱乐和自我教育.

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

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

但是应该考虑一些事情.

  1. 如果循环的内部部分很大(大NI>10^7),则循环的整个内容不适合指令高速缓存,因此必须一遍又一遍地加载,使得RAM的速度定义执行所需的时间.例如,对于大的内部部分,xorl %eax, %eax(2个字节)比xorq %rax, %rax(3个字节)快33%.
  2. 如果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
    ....
}
Run Code Online (Sandbox Code Playgroud)

结果是:

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

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

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

PS:如果您想在机器上试用,可以从这里下载脚本,只需sh jmp_test.shsrc -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|
Run Code Online (Sandbox Code Playgroud)

可以被看见:

  1. 对于该jmp指令,(尚未知的)资源变得稀缺,这导致性能降低?I超过4000.
  2. 此资源不会与这样的指令共享xor- 性能下降仍会持续NI约4000,如果jmp并且 xor相继执行.
  3. 但是,je如果进行跳转,则共享此资源- 对于jmp+ je之后,资源变得稀缺NI大约2000.
  4. 然而,如果je根本没有跳跃,资源再次变得稀少NI,大约是4000(第4行).

Matt Godbolt的分支预测逆向工程文章确定了分支目标缓冲容量为4096个条目.这是非常有力的证据表明BTB未命中是观察到小循环和大jmp循环之间吞吐量差异的原因.

Pet*_*des 10

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


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

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


您的Intel Broadwell CPU 具有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指令很难:在前一个指令被解码之前,您不知道一条指令的起始位置.所以你不能只是模式匹配指令流,一旦获取它就会寻找无条件的跳转/调用.

我目前的理论是,当你用完分支目标缓冲区条目时,你的速度会变慢.

另请参阅分支目标缓冲区检测到哪些分支错误预测?这个Realworldtech主题中有一个很好的答案和讨论.

一个非常重要的一点是:BTB根据下一个要获取的块来预测,而不是取回块中特定分支的确切目标.因此,CPU不需要预测获取块中所有分支的目标,而只需要预测下一次获取的地址.


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

xor有/无REX测试中,如果您使用足够大的环路测试不适合L3缓存,主内存带宽实际上可能是那里的瓶颈.我在一个~3GHz的CPU上每个周期消耗4*2B,这大约可以达到25GB/s的DDR3-1600MHz.尽管如此,即使是L3缓存也足够快,以便在每个周期内保持4*3B的速度.

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


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

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

参见x86-64来自Core2的perf-counter结果的相对jmp性能(每个jmp 8个周期),以及一些未知的微体系结构,其中每个jmp约为10c.


现代CPU性能特征的细节很难理解,即使在或多或少的白盒条件下(阅读英特尔的优化手册,以及他们发布的有关CPU内部的内容).如果你坚持进行黑盒测试而不去阅读有关新CPU设计的arstechnica文章,或者像David Kanter的Haswell微观概述或者类似的更详细的东西,你会很早就陷入困境.我之前联系过Sandybridge的文章.

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