当前位置: 首页 > 知识库问答 >
问题:

慢速 jmp 指令

孔飞翔
2023-03-14

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

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

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

但是有些事情还是应该考虑的。

  1. 如果循环的内部很大(大NI

然而,这个简单的模型对于< code > jmp -指令是站不住脚的。对于< code > jmp -指令,我的测试代码如下所示:

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

其结果是:

  1. 对于 “大 ” 环 路 尺寸 ( 已经 用于 NI

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

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

PS:如果您想在您的机器上试用,可以从这里下载脚本,只需运行<code>sh jmp_test。src文件夹中的sh</code>。

编辑:实验结果证实了Peter的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等指令共享-如果jmp 相继执行, 4000的性能仍会下降
  3. 但是这个资源是与je共享的,如果跳转-对于jmp ,资源在 大约2000年变得稀缺。
  4. 然而,如果je根本不跳转,则资源再次变得稀缺,因为NI大约为4000(第四行)

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


共有1个答案

巴洲
2023-03-14
匿名用户

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

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

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

要了解更多关于跳转及其对流水线CPU的影响,经典RISC流水线中的控制危险应该是一个很好的介绍,说明为什么分支对于流水线CPU是困难的。Agner Fog的指南解释了实际的含义,但我认为假设一些这种背景知识。

您的英特尔Broadwell CPU有一个uop缓存,用于缓存解码指令(与32kiB L1 I缓存分开)。

uop缓存大小为32组8路,每行6 uop,总共1536 uop(如果每行都有6 uop;完美效率)。1536 uops介于1000到10000个测试大小之间。在您编辑之前,我预测慢到快的截止时间将在循环中的1536条指令左右。它在超过1536条指令之前根本不会减速,所以我认为我们可以排除uop缓存的影响。这不是我想的那么简单的问题

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

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

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

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

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

另请参见分支目标缓冲区检测到什么分支预测失误?这个问题有一个很好的答案,在这个Realworldtech的帖子里也有讨论。

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

是的,内存带宽在运行非常高吞吐量的东西时可能是一个瓶颈,比如xor归零,但是您使用jmp遇到了不同的瓶颈。CPU将有时间从内存中获取42B,但这不是它正在做的事情。预取可以很容易地保持每3个时钟2个字节的速度,所以应该有接近零的L1 I缓存未命中。

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

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

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

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

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

即使在或多或少的白盒条件下,现代CPU性能特征的细节也很难理解(阅读英特尔的优化手册,以及他们发布的关于CPU内部结构的内容)。如果你坚持黑匣子测试,而你不读关于新CPU设计的arstechnica文章,或者可能读一些更详细的文章,比如David Kanter的Haswell微阵列概述,或者我之前链接的类似Sandybridge写作,你会很早陷入困境。

如果早点被困,经常被困是可以的,而且你玩得很开心,那么就尽一切办法继续做你正在做的事情。但是,如果你不知道这些细节,人们就很难回答你的问题,比如在本例中e、 这个答案的第一个版本假设你已经读了足够多的书,知道uop缓存是什么。

 类似资料:
  • JMP

    JMP是一个针对Java的剖析器.它被用来追踪对象的使用和计算method的运行时间.它利用JVMPI接口来收集统计信息并与JVM相结合.JMP使用一个GTK+接口来显示运行的状态.

  • 描述 (Description) C库函数void longjmp(jmp_buf environment, int value)在相应的程序调用中使用相应的jmp_buf参数恢复最近调用setjmp()宏所保存的环境。 声明 (Declaration) 以下是longjmp()函数的声明。 void longjmp(jmp_buf environment, int value) 参数 (Par

  • 描述 (Description) C库宏int setjmp(jmp_buf environment)将当前environment保存到变量环境中供以后使用函数longjmp() 。 如果此宏直接从宏调用返回,则返回零但如果从longjmp()函数调用返回,则返回传递给longjmp的值作为第二个参数。 声明 (Declaration) 以下是setjmp()宏的声明。 int setjmp(jm

  • 我正在试用Flutter,我的应用程序在仿真器和实际设备上的响应都非常非常慢。我收到这样的警告 跳过了51帧!应用程序可能在其主线程上做了太多的工作。 我知道Dart是一种单线程编程语言,在Android中,我曾使用用于异步的好的旧块来解决这一问题。我试图在Flutter中应用相同的方法,并且我阅读了和排序,但是当您从Internet读取数据时,这些示例似乎是针对这些示例的。我的应用程序在这个阶段

  • 针对Postgres数据库的某个索引SELECT查询所花费的时间非常可变--从50毫秒到多秒,有时甚至是几分钟,即使在最轻的负载下也是如此。 你能为26秒的差距提出一个解释吗? 关于并发的注意事项:即使只有一个请求也有很大的可变性:端到端50-300毫秒,但是当一个用户提交一批大约100个这样的查找时(可能有10-20个同时运行),很可能有几个查找需要5-10秒。然而C3P0的统计数据从来没有比:

  • 问题内容: 我有以下InnoDB表: 使用这些键: 我只是注意到有时我在此表上有一个INSERT查询,耗时超过1秒 我真的很困惑,为什么要花这么长时间。我如何加快速度? 顺便说一句:这样每天大约有80个缓慢的插入和40个缓慢的更新。 问题答案: 有时,不是查询本身会导致速度降低- 在表上运行的另一个查询可能会由于事务隔离和锁定而很容易导致插入速度降低。您的慢查询可能只是在等待其他事务完成。这在繁忙