“条件调用”在 amd64 上的性能

2023-11-25

当考虑代码关键部分中的条件函数调用时,我发现 gcc 和 clang 都会围绕该调用进行分支。例如,对于以下(诚然微不足道的)代码:

int32_t __attribute__((noinline)) negate(int32_t num) {
    return -num;
}

int32_t f(int32_t num) {
    int32_t x = num < 0 ? negate(num) : num;
    return 2*x + 1;
}

GCC 和 clang 基本上都编译为以下内容:

.global _f
_f:
    cmp     edi, 0
    jg      after_call
    call    _negate
after_call:
    lea     rax, [rax*2+1]
    ret

这让我开始思考:如果 x86 有像 ARM 这样的条件调用指令会怎样?想象一下如果有这样一条指令“ccallcc" 具有像 cmov 这样的语义cc。然后你可以做类似的事情:

.global _f
_f:
    cmp     edi, 0
    ccalll  _negate
    lea     rax, [rax*2+1]
    ret

虽然我们无法避免分支预测,但我们确实消除了分支。也就是说,在实际的 GCC/clang 输出中,我们被迫分支,无论是否num < 0或不。而如果num < 0我们必须分支两次。这看起来很浪费。

现在amd64中不存在这样的指令,但是我设计了一种方法来模拟这样的指令。我通过打破做到了这一点call func进入其组成部分:push rip(从技术上来说[rip+label_after_call_instruction]) 进而jmp func。我们可以使jmp有条件,但没有条件push。我们可以通过计算来模拟这一点[rip+label_after_call_instruction]并将其写入堆栈上的适当位置,然后有条件地更新rsp如果我们计划调用该函数(实际上“推动”[rip+label_after_call_instruction])。它看起来像这样:

.global _f
_f:
    cmp     edi, 0

    # ccalll _negate
    lea     rax, [rip+after_ccall]  # Compute return address
    mov     [rsp-8], rax            # Prepare to "push" return address
    lea     rax, [rsp-8]            # Compute rsp (after push)
    cmovl   rsp, rax                # Conditionally push (by actually changing rsp)
    jl      _negate                 # "Conditional call"
after_ccall:

    lea     rax, [rax*2+1]
    ret

这种方法有一些潜在的缺点:

  • 它引入了几条指令(但它们的总周期数少于分支错误预测惩罚)
  • 它需要写入内存(但堆栈可能已缓存?)
  • 它总是执行2leas and mov即使没有拨打电话(但我的理解是这并不重要,因为 cmovcc例如,与 mov 的周期数相同)

为了检查每种方法的属性,我运行了关键部分iaca。如果您安装了它(并且您克隆了下面的基准测试要点),您可以运行make iaca亲自看看。经过IACAFLAGS='-arch=...'指定不同的拱门。

分支方法的输出:

Intel(R) Architecture Code Analyzer Version -  v3.0-28-g1ba2cbb build date: 2017-10-30;16:57:45
Analyzed File -  ./branch_over_call_iaca.o
Binary Format - 64Bit
Architecture  -  SKL
Analysis Type - Throughput

Throughput Analysis Report
--------------------------
Block Throughput: 0.82 Cycles       Throughput Bottleneck: Dependency chains
Loop Count:  36
Port Binding In Cycles Per Iteration:
--------------------------------------------------------------------------------------------------
|  Port  |   0   -  DV   |   1   |   2   -  D    |   3   -  D    |   4   |   5   |   6   |   7   |
--------------------------------------------------------------------------------------------------
| Cycles |  0.5     0.0  |  0.0  |  0.3     0.0  |  0.3     0.0  |  1.0  |  0.0  |  0.5  |  0.3  |
--------------------------------------------------------------------------------------------------

DV - Divider pipe (on port 0)
D - Data fetch pipe (on ports 2 and 3)
F - Macro Fusion with the previous instruction occurred
* - instruction micro-ops not bound to a port
^ - Micro Fusion occurred
# - ESP Tracking sync uop was issued
@ - SSE instruction followed an AVX256/AVX512 instruction, dozens of cycles penalty is expected
X - instruction not supported, was not accounted in Analysis

| Num Of   |                    Ports pressure in cycles                         |      |
|  Uops    |  0  - DV    |  1   |  2  -  D    |  3  -  D    |  4   |  5   |  6   |  7   |
-----------------------------------------------------------------------------------------
|   1      | 0.5         |      |             |             |      |      | 0.5  |      | jnle 0x6
|   4^#    |             |      | 0.3         | 0.3         | 1.0  |      |      | 0.3  | call 0x5
Total Num Of Uops: 5

条件调用方法的输出:

Intel(R) Architecture Code Analyzer Version -  v3.0-28-g1ba2cbb build date: 2017-10-30;16:57:45
Analyzed File -  ./conditional_call_iaca.o
Binary Format - 64Bit
Architecture  -  SKL
Analysis Type - Throughput

Throughput Analysis Report
--------------------------
Block Throughput: 1.94 Cycles       Throughput Bottleneck: Dependency chains
Loop Count:  35
Port Binding In Cycles Per Iteration:
--------------------------------------------------------------------------------------------------
|  Port  |   0   -  DV   |   1   |   2   -  D    |   3   -  D    |   4   |   5   |   6   |   7   |
--------------------------------------------------------------------------------------------------
| Cycles |  1.0     0.0  |  1.0  |  0.5     0.0  |  0.5     0.0  |  1.0  |  1.0  |  1.0  |  0.0  |
--------------------------------------------------------------------------------------------------

DV - Divider pipe (on port 0)
D - Data fetch pipe (on ports 2 and 3)
F - Macro Fusion with the previous instruction occurred
* - instruction micro-ops not bound to a port
^ - Micro Fusion occurred
# - ESP Tracking sync uop was issued
@ - SSE instruction followed an AVX256/AVX512 instruction, dozens of cycles penalty is expected
X - instruction not supported, was not accounted in Analysis

| Num Of   |                    Ports pressure in cycles                         |      |
|  Uops    |  0  - DV    |  1   |  2  -  D    |  3  -  D    |  4   |  5   |  6   |  7   |
-----------------------------------------------------------------------------------------
|   1      |             | 1.0  |             |             |      |      |      |      | lea rax, ptr [rip]
|   2^     |             |      | 0.5         | 0.5         | 1.0  |      |      |      | mov qword ptr [rsp-0x8], rax
|   1      |             |      |             |             |      | 1.0  |      |      | lea rax, ptr [rsp-0x8]
|   1      | 1.0         |      |             |             |      |      |      |      | cmovl rsp, rax
|   1      |             |      |             |             |      |      | 1.0  |      | jl 0x6
Total Num Of Uops: 6

我看起来条件调用方法似乎使用了更多的硬件。但我发现有趣的是,条件方法仅多了 1 个 uop(分支方法有 5 个 uop)。我想这是有道理的,因为在底层调用变成了push和jmp(并且push变成了rsp math和内存mov)。这对我来说意味着条件调用方法大约是等效的(尽管我的简单化分析可能在这里有缺陷?)。

至少,我的总体怀疑是通过在cmp and jl,我会让结果成为可能cmp将在之前可用jl可以推测性地执行(从而完全阻止分支预测)。尽管管道可能比这更长?这涉及到的领域(尽管已经阅读并保持了中等深度的理解)Agner Fog 的优化手册)我不是很熟悉。

我的假设是,对于(负和正)的均匀分布nums (其中分支预测将无法预测call)我的“条件调用”方法将优于围绕调用的分支。

我写了一个利用工具对这两种方法的性能进行基准测试。你可以git clone https://gist.github.com/baileyparker/8a13c22d0e26396921f501fe87f166a9 and make在您的机器上运行基准测试。

以下是对 1,048,576 个数字(均匀分布在int32_t最小值和最大值)。

|                    CPU                    | Conditional Call | Branch Over |
|-------------------------------------------|-----------------:|------------:|
| Intel(R) Core(TM) i7-7920HQ CPU @ 3.10GHz |       10.9872 ms |   8.4602 ms |
| Intel(R) Xeon(R) CPU E3-1240 v6 @ 3.70GHz |        8.8132 ms |   7.0704 ms |

这些结果在运行中是一致的,尽管通过增加数组大小(或迭代次数)而放大,但分支总是获胜。

我还尝试重新排序条件调用步骤(计算和有条件更新rsp首先,然后写入堆栈)但这执行类似。

我遗漏(或误解)的哪些硬件细节可以解释这一点?根据我的计算,额外的指令会增加大约 6-7 个周期,但分支错误预测会花费 15 个周期。因此,平均有一半的数字被预测错误,因此每次迭代都会花费 15/2 个周期(对于分支转移方法),并且总是 6-条件调用需要 7 个周期。 iaca 的 uops 表明,在这方面,这些方法更加接近。那么,性能不应该更接近吗?我的示例代码是否太做作/简短?我的基准测试技术是否不适合这种低级别关键部分测试?有没有办法重新排序/更改条件调用以使其性能更高(也许比分支方法更好或相当)?

tl;dr为什么我的条件调用代码(第四个代码片段)的性能比 gcc/clang 生成的(条件跳转call) (第二个代码片段)(对于第一个代码片段中的代码)我的基准?


您可以准确地确定为什么conditional_call方法比branch_over_call。您已经在两个 KBL 处理器上完成了实验,但是博客文章你被提到没有讨论 RAS 如何在 KBL 上工作。因此,分析的第一步是确定ret in the negate函数是否被错误预测(就像早期微架构上会发生的情况一样)。第二步是确定错误预测的成本是多少ret总执行时间的指令。我拥有的最接近 KBL 的东西是 CFL,结果我的数据与你的很接近。两者之间唯一相关的区别是 LSD 在 CFL 中启用,但在 KBL 中禁用。然而,LSD 在这种情况下是无关紧要的,因为call循环中的指令可防止 LSD 检测到任何循环。您还可以轻松地在 KBL 上重复相同的分析。

有多种方法可以分析分支指令的行为。但在这种特殊情况下,代码足够简单,事件计数方法可以显示我们需要的有关每个静态分支指令的所有信息。

The BR_INST_RETIRED_*性能事件可用于计算退休的动态分支指令的总数以及退休的特定类型的分支指令(包括条件、调用和返回)的总数。这BR_MISP_RETIRED_*events 可用于计算总错误预测、总条件错误预测和总呼叫错误预测。

完整的控制辉光图conditional_call看起来像这样:

           total   misp
call         1      0
    jl       1     0.5
       ret  0.5     1
    ret      1      0
jne          1      0

首先call指令调用conditional_call函数,其中包含jl and ret. The jl指令有条件跳转到negate函数,其中包含ret. The jne指令用于for循环。第一列和第二列中显示的数字分别通过迭代总数和动态指令总数进行归一化。从程序的静态结构我们知道call, jl, conditional_call's ret, and jne在每次迭代中都执行一次。最里面的ret仅当执行jl分支被采取。使用性能事件,我们可以计算执行的返回指令的总数,并从中减去迭代的总数,以获得最内层的次数ret被执行。因为输入是根据均匀分布随机化的,所以最内部的ret执行了一半的时间。

The call指令永远不会被错误预测。这jne除了指令的最后一次执行(退出循环的地方)之外,指令也永远不会被错误预测。因此,我们可以将条件错误预测的总数归因于jl操作说明。可以从错误预测的总数中减去它,以获得可以归因于任一或两个返回指令的返回错误预测的数量。第二ret当第一个错误预测时可能会错误预测ret破坏或错位 RAS。确定是否第二种的一种方法ret曾经被错误预测的是通过使用精确的采样BR_MISP_RETIRED.ALL_BRANCHES。另一种方法是使用您引用的博客文章中描述的方法。确实,只有最内心的ret被错误预测。事实是jl一半时间被错误预测表明该指令要么被预测为总是被执行,要么总是不被执行。

完整的控制辉光图branch_over_call看起来像这样:

           total   misp
call         1      0
    jg       1     0.5
    call    0.5     0
        ret 0.5     0
    ret      1      0
jne          1      0

唯一被错误预测的指令是jg,有一半的时间是错误预测的。

衡量单个项目的平均成本ret中的错误预测conditional_call方法,将ret指令可以替换为lea/jmp序列,以便使用 BTB 而不是 RAS 进行预测。通过此更改,唯一被错误预测的指令是jl。执行时间的差异可以被视为对总成本的估计ret错误预测。在我的 CFL 处理器上,每个周期大约为 11.3 个周期ret错误预测。此外,conditional_call速度比之前快约 3%branch_over_call。您在 KBL 上的数字表明,平均成本ret错误预测大约是13个周期。我不确定这种差异的原因是什么。它可能不是微架构。我使用了 gcc 7.3,但您使用了 gcc 8,因此可能代码中存在一些差异或不同代码片段的对齐方式导致了我们的结果之间的差异。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

“条件调用”在 amd64 上的性能 的相关文章

  • 如何知道寄存器是否是“通用寄存器”?

    我试图了解寄存器必须具备什么标准才能被称为 通用寄存器 我相信通用寄存器是一个可以用于任何用途的寄存器 用于计算 将数据移入 移出等 并且是一个没有特殊用途的寄存器 现在我读到了ESP寄存器是通用寄存器 我猜是ESP寄存器可以用于任何事情
  • 从 NASM 调用 C 函数 _printf 会导致分段错误

    我一直在尝试使用 NASM 在 Mac OS 和 Windows 上学习 64 位汇编 我的代码是 extern printf section data msg db Hello World 10 0 section text global
  • ARMv8 A64 汇编中立即值的范围

    我的理解是 ARMv8 A64 汇编中的立即参数可以是 12 位长 如果是这样的话 为什么这行汇编代码是 AND X12 X10 0xFEF 产生此错误 使用 gcc 编译时 Error immediate out of range at
  • Linux内核页表更新

    在linux x86 中分页 每个进程都有它自己的页面目录 页表遍历从 CR3 指向的页目录开始 每个进程共享内核页目录内容 假设三个句子是正确的 假设某个进程进入内核 模式并更新他的内核页目录内容 地址映射 访问 权利等 问题 由于内核地
  • 阴影空间示例

    EDIT 我接受了下面的答案 并添加了我自己的代码的最终修订版 希望它向人们展示影子空间分配的实际示例 而不是更多的文字 编辑 2 我还设法在 YouTube 视频 所有内容 的注释中找到了一个调用约定 PDF 的链接 其中有一些关于 Li
  • 如何使用movntdqa避免缓存污染?

    我正在尝试编写一个 memcpy 函数 该函数不会将源内存加载到 CPU 缓存中 目的是避免缓存污染 下面的 memcpy 函数可以工作 但会像标准 memcpy 一样污染缓存 我正在使用带有 Visual C 2008 Express 的
  • MikeOS 引导加载程序中的堆栈段

    我不明白这段代码 mov ax 07C0h Set up 4K of stack space above buffer add ax 544 8k buffer 512 paragraphs 32 paragraphs loader cli
  • 这个反斜杠在这段汇编代码中起什么作用?

    我不确定这些推线有什么区别 修剪下来来自 Linux 的 x86 entry calling h https github com torvalds linux blob 241e39004581475b2802cd63c111fec43b
  • FreePascal x64 上系统单元函数的汇编调用

    我有一些 Delphi 汇编代码 可以在 Win32 Win64 和 OSX 32 上编译并正常工作 XE2 但是 由于我需要它在 Linux 上工作 所以我一直在考虑编译它的 FPC 版本 到目前为止 Win32 64 Linux32 6
  • 为什么 clang 使用 -O0 生成低效的 asm(对于这个简单的浮点和)?

    我正在 llvm clang Apple LLVM 版本 8 0 0 clang 800 0 42 1 上反汇编此代码 int main float a 0 151234 float b 0 2 float c a b printf f c
  • 在 x86 程序集中存储大量布尔值的最佳方法是什么?

    最近我一直在处理充满布尔值的大型数组 目前 我将它们存储在 bss部分有一个 space指令 它允许我创建字节数组 但是 由于我只需要存储布尔值 因此我希望从数组中逐位读取和写入数据 目前 我能想到的最好方法是有一个 space指令所需存储
  • 如何使用 Bochs 运行汇编代码?

    我想使用 Bochs 作为 8086 模拟器 是否有捷径可寻 我想要的是类似 emu8086 的东西 http www emu8086 com http www emu8086 com 如果程序的初始部分适合 512 字节 并且您不介意将自
  • 添加冗余赋值可以在未经优化的情况下编译时加快代码速度

    我发现一个有趣的现象 include
  • NASM 中的 equ 和 db 有什么区别?

    len equ 2 len db 2 它们是否相同 产生可以用来代替的标签2 如果不是 那么每种申报表的优点或缺点是什么 它们可以互换使用吗 第一个是equate 与 C 类似 define len 2 因为它实际上并没有在最终代码中分配任
  • 32 位到 64 位内联汇编移植

    我有一段 C 代码 在 GNU Linux 环境下用 g 编译 它加载一个函数指针 它如何执行并不重要 使用一些内联汇编将一些参数推送到堆栈上 然后调用该函数 代码如下 unsigned long stack 1 23 33 43 save
  • 使用按位运算符相乘

    我想知道如何使用按位运算符将一系列二进制位相乘 但是 我有兴趣这样做来查找二进制值的十进制小数值 这是我正在尝试做的一个例子 假设 1010010 我想使用每个单独的位 以便将其计算为 1 2 1 0 2 2 1 2 3 0 2 4 虽然我
  • SIMD 和 VLIW 指令是一样的吗?

    SIMD 单指令多数据 和 VLIW 超长指令字 到底有什么区别 其中一个是另一个的子集吗 或者它们是两个完全不同的东西 完全不相关且正交 一台机器可以有一个或两个 或者两者都没有 SIMD 指令可以作为扩展添加到 VLIW ISA 但 V
  • 为什么X86中没有NAND、NOR和XNOR指令?

    它们是您可以在计算机上执行的最简单的 指令 之一 它们是我亲自实施的第一个指令 执行 NOT AND x y 会使执行时间和依赖链长度和代码大小加倍 BMI1 引入了 andnot 这是一个有意义的补充 是一个独特的操作 为什么不是这个问题
  • movzbl(%rdi, %rcx, 1), %ecx 在 x86-64 汇编中意味着什么?

    我想我明白 movzbl rdi rcx 1 ecx 意思是 将零扩展字节移至长整型 并表示将 ecx 扩展为 32 位 但我不完全确定语法 rdi rcx 1 指的是什么 我在某处看到该语法指的是 Base Index Scale 但我找
  • 英特尔的最后分支记录功能是英特尔处理器独有的吗?

    最后分支记录是指存储与最近执行的分支相关的源地址和目标地址的寄存器对 MSR 的集合 它们受英特尔酷睿 2 英特尔至强和英特尔凌动处理器系列的支持 http css csail mit edu 6 858 2012 readings ia3

随机推荐

  • 带有 4 个 MediaPlayers 的 Android 4.2 =“无法播放此视频”

    每当我尝试加载至少 4 个媒体播放器时 其中一个会损坏它尝试加载的视频并触发 Android 操作系统消息 无法播放此视频 其他信息 对于 3 个媒体播放器 一切正常 在其他 Android 版本上 与 4 2 不同 相同的代码可以使用相同
  • C语言中如何获取目录的大小?

    是否有一个 POSIX 函数可以给我一个目录 包括所有子文件夹 的大小 大致相当于 du s somepath man nftw NAME ftw nftw 文件树行走 描述 ftw 遍历目录树 位于该目录下 dirpath 并调用fn 每
  • runif() 是否真的有一个范围:0<= runif(n) <= 1,如文档中所述?

    我是 R 新手 但文档指出 runif n 返回 0 到 1 范围内的数字 这令我感到惊讶包括的 我期望 0 我用 n 100 000 000 对其进行了测试 发现它从未产生 0 或 1 我意识到实际达到浮点特定值的概率非常小 但仍然 之间
  • 在magento中发送邮件

    如何在magento中发送电子邮件 在索引控制器中编写操作 我的索引控制器 public function postAction post this gt getRequest gt getPost if post exit transla
  • 如何在 bash 脚本中使用 bash 配置文件中定义的函数?

    我的 bash profile 中有一个投影函数 现在我尝试从 bash 脚本调用此函数 但出现未找到错误 如何使投影函数对 bash 脚本可见 您必须导出该函数 export f foo ref
  • Firebase 挂起的动态链接不起作用

    根据 Firebase 动态链接文档 即使未安装应用程序 如果用户在设备上打开链接 Appstore 上的应用程序页面也会打开 并且一旦安装应用程序 应用程序就会在首次启动时处理该链接 经过一番调查后 我发现 Firebase 有一种称为
  • “在 MATLAB 路径中隐藏它”是什么意思?如何在文件中做到这一点?

    我需要在运行 unitTester 文件之前始终执行此操作 我不明白为什么需要这样做 这是什么意思 为什么是 Add to Path gt Selected Folders and Subfolders 不够 Update This her
  • 如何定位直接文本而不是标签内的文本?

    有没有办法只定位直接文本 h1 Tag 这是我想做的事情的一个例子 h1 I want to select this text with a css selector h1 h1
  • Swift 中的 UTTypeCreatePreferredIdentifierForTag 和 CFStringRef

    import Foundation import MobileCoreServices func checkFileExtension fileName NSString println fileName var fileExtension
  • RecyclerView notificationItemRangeChanged 不显示新数据

    我遇到了一个问题RecyclerView Adapter关于notifyItemRangeChanged 看来如果Adapter认为它的大小为0从上次通话到getItemCount 然后我打电话Adapter notifyItemRange
  • git 拒绝在没有代理的情况下连接

    我在 Windows 环境中使用 Linux 系统 为了使用 NT 代理服务器进行身份验证 我进行了设置cntlm并配置系统程序以通过设置使用它http proxy环境变量中的 etc environment file 现在我想删除此代理设
  • 在 Pyspark Dataframe 上透视字符串列

    我有一个像这样的简单数据框 rdd sc parallelize 0 A 223 201603 PORT 0 A 22 201602 PORT 0 A 422 201601 DOCK 1 B 3213 201602 DOCK 1 B 321
  • 如何在 swift 中使用 .a 静态库?

    我想在 swift 中使用我的 webrtc a 静态库 你能帮忙吗 我读到你不能在 swift 中使用静态库 是真的吗 你问的这个问题解决了吗 我今天也遇到这个问题了 一会儿就解决了 如果您尚未解决此问题 您可以尝试以下步骤 p s 这两
  • 如何验证国际化域名[关闭]

    Closed 这个问题需要多问focused 目前不接受答案 我想验证 php 中的域名 url 该域名可能采用国际化域名格式 如希腊语 域名 http 他们有什么方法可以使用正则表达式来验证它吗 这是一个所谓的国际化域名 支持 IDN 域
  • 通过浏览器操作/图标禁用/启用 Chrome 扩展

    我正在开发的 chrome 扩展将内容脚本和 CSS 插入到网站的每个页面上 但是 用户可能有一个或多个页面 他或她不希望扩展程序在其上运行 因此如果我可以将浏览器操作设置为基本上打开 关闭切换 那就太好了 我想做的是这样的 chrome
  • 将 React Redux 与 next.js 结合使用

    我尝试将 Redux 与next js 启动器项目和我安装的next redux wrapper在该项目中 但我不确定该项目中的根文件在哪里 我尝试按照显示的教程进行操作next redux 包装器但没有成功 没变化 请帮助我如何将 Red
  • 如何在 iOS 上重置沙盒应用内购买以进行测试?

    我做了一个沙盒 iTunes 用户 购买了一个项目 这有效 但我的显示该项目的代码中存在一些问题 所以我想重新买来测试一下 问题是 我无法清除我的购买 我注销了我的沙盒用户 删除了应用程序并重新安装了它 更改了 iTunes 用户几次 该项
  • 如何使用 Eigen 3 表达“ = <= ”?

    我正在移植一些MATLAB代码到C 使用Eigen 3模板库 我正在寻找这个常见的良好映射MATLAB idiom K gt gt 1 2 3 4 5 lt 3 ans 1 1 1 0 0 因此 比较数组和标量 返回具有相同形状的布尔值数组
  • 用于检查多个值的 jQuery 'if' 条件

    在下面的代码中 是否有更好的方法使用 jQuery 检查条件 if test1 val first value test2 val second value test3 val third value test4 val fourth va
  • “条件调用”在 amd64 上的性能

    当考虑代码关键部分中的条件函数调用时 我发现 gcc 和 clang 都会围绕该调用进行分支 例如 对于以下 诚然微不足道的 代码 int32 t attribute noinline negate int32 t num return n