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

为什么添加xorps指令会使使用cvtsi2ss和addss的函数快5倍?

巢权
2023-03-14

我在使用Google Benchmark优化函数时遇到了麻烦,在某些情况下,我的代码出乎意料地慢了下来。我开始对它进行实验,查看编译后的html" target="_blank">程序集,最终提出了一个展示该问题的最小测试用例。这是我提出的展示这种减速的组件:

    .text
test:
    #xorps  %xmm0, %xmm0
    cvtsi2ss    %edi, %xmm0
    addss   %xmm0, %xmm0
    addss   %xmm0, %xmm0
    addss   %xmm0, %xmm0
    addss   %xmm0, %xmm0
    addss   %xmm0, %xmm0
    addss   %xmm0, %xmm0
    addss   %xmm0, %xmm0
    addss   %xmm0, %xmm0
    retq
    .global test

此函数遵循GCC/Clang的x86-64调用约定,用于函数声明extern"C"浮点测试(int);注意注释掉的xorps指令。取消注释此指令显着提高了函数的性能。使用我的机器进行i7-8700K测试,Google基准测试显示,没有xorps指令的函数需要8.54ns(CPU),而带有xorps指令的函数需要1.48ns。我已经在具有各种操作系统、处理器、处理器代和不同处理器制造商(Intel和AMD)的多台计算机上进行了测试,它们都表现出类似的性能差异。重复addss指令会使减速更加明显(在一定程度上),并且这种减速仍然会使用这里的其他指令(例如mulss)甚至混合指令发生,只要它们都以某种方式依赖于%xmm0中的值。值得指出的是,只有调用xorps每个函数调用才能提高性能。使用循环(如Google Benchmark所做的那样)对性能进行采样(在循环之外使用xorps调用仍然显示出较慢的性能。

由于这是一个专门添加指令提高性能的情况,这似乎是由CPU中真正低级的东西引起的。由于它发生在各种各样的CPU中,这似乎是故意的。但是,我找不到任何留档来解释为什么会发生这种情况。有人能解释一下这里发生了什么吗?这个问题似乎取决于复杂的因素,因为我在原始代码中看到的减速只发生在特定的优化级别(-O2,有时是-O1,但不是-O),没有内联,并且使用特定的编译器(Clang,但不是GCC)。

共有1个答案

柴深
2023-03-14

异或归零打破了dep链,允许无序exec发挥其魔力。因此,您的瓶颈是吞吐量(0.5个周期),而不是延迟(4个周期)。

您的CPU是Skylake派生的,所以这些是数字;早期的英特尔使用专用的FP-add执行单元而不是在FMA单元上运行它有3个周期延迟,1个周期吞吐量。https://agner.org/optimize/.可能函数调用/ret开销会阻止您从流水线FMA单元中的8个动态addssuops的延迟*带宽乘积中看到完整的8倍预期加速;如果您从单个函数中的循环中删除xorpsdep-中断,您应该会获得该加速。

GCC倾向于对虚假依赖非常“小心”,花费额外的指令(前端带宽)来打破它们以防万一。在前端出现瓶颈的代码中(或者在总代码大小/uop缓存占用是一个因素的情况下),如果寄存器实际上已经及时准备好,那么这会降低性能。

Clang/LLVM对此是鲁莽和漫不经心的,通常不会费心避免对不在当前函数中写入的寄存器的错误依赖。(即假设/假装寄存器对函数条目是“冷的”)。正如您在评论中展示的,clang确实避免了在一个函数内循环时通过异或零来创建循环携带的dep链,而不是通过对同一个函数的多次调用。

在某些情况下,与32位reg相比,Clang甚至无缘无故地使用8位GP整数部分寄存器,这不会节省任何代码大小或指令。通常这可能没问题,但例如,如果调用方(或同级函数调用)在调用时仍有缓存未命中负载,则存在耦合到长dep链或创建循环承载依赖链的风险。

有关OoO exec如何重叠短到中等长度的独立dep链的更多信息,请参阅了解lGeofence对具有两个长依赖链的循环的影响,以增加长度。还相关:为什么mulss在Haswell上只需要3个周期,不同于Agner的指令表?(使用多个累加器展开FP循环)是关于使用多个累加器展开点积以隐藏FMA延迟。

https://www.uops.info/html-instr/CVTSI2SS_XMM_R32.html具有此指令在各种UARCHE中的性能详细信息。

正如我在中提到的,为什么sqrtsd指令的延迟会根据输入而变化?英特尔处理器

这正是AMD64通过将对32位整数寄存器的写入隐式零扩展到完整的64位寄存器而避免的问题,而不是将其保持不变(也称为合并)。为什么32位寄存器上的x86-64指令将完整64位寄存器的上部归零?(写入8位和16位寄存器确实会导致对AMD CPU的错误依赖,以及自Haswell以来的Intel)。

 类似资料:
  • 问题内容: 关于前几天我问的这个问题,我得到了以下评论。 在几乎所有数据库中,列上的几乎所有函数都会阻止使用索引。到处都有例外,但总的来说,函数会阻止使用索引 我四处搜寻,并发现了更多关于此相同行为的提及,但我很难找到比评论已经告诉我的更深入的信息。 有人可以详细说明为什么会发生这种情况,以及避免这种情况的对策吗? 问题答案: 最基本形式的索引只是排序的列数据,因此可以轻松地按某个值查找。例如,一

  • 问题内容: 我使用指令创建联系表格。最初我创建用于显示客户表单的customerForm指令。在这种形式下,我有一个按钮,当我们单击添加按钮时,称为getData函数,该函数内部使用newDirective显示ul列表。为此,我使用$ compile api编译html代码。很好,当我们单击“删除”按钮时,它也显示列表值和“删除”按钮,它称为scope.remove()函数。但是它只能删除一个。之

  • 请参阅 Kotlin 中的此示例代码: 将其反编译为Java代码后(工具- 我注意到生成的 Java 方法有一个未使用 参数。 我有点认为它可能与类中的函数有关,但当反编译此代码时 我得到了这个代码 正如您所看到的,< code>Object参数仍然没有使用,只是放在那里。在额外的测试中,我注意到扩展方法也有同样的行为。当默认参数为last(即< code>fun foo(bar: Int,baz

  • 问题内容: 当我使用最新(1.0)版本的coffee-script时,一个简单的javascript输出看起来像这样(默认): 什么 .CALL(本) 做,哪些是添加它的原因是什么? 问题答案: 它正在创建一个函数,然后使用父函数/对象范围进行调用。 .call和.apply是调用函数的不同方法。您基本上创建了一个函数,除了在自己的范围内设置a = 1之外,什么也不做。 在javascript中,

  • 考虑这个函数: 当我用带有< code>-O3(或< code>-O2或< code>-O1)的x86-64 gcc 8.2编译它时,它会编译成这样: 当我使用时,它编译成这样: 后者长3个字节。不是应该产生尽可能小的代码吗,即使更大的代码会更有效率?为什么这里似乎发生了相反的情况? 戈德波特:https://godbolt.org/z/jzNquk

  • 问题内容: 我已经修改了一些Go代码,以解决与我姐夫玩的电子游戏有关的我的好奇心。 本质上,下面的代码模拟了游戏中与怪物的互动,以及他期望他们在失败后掉落物品的频率。我遇到的问题是,我希望这样的一段代码非常适合并行化,但是当我并发添加时,完成所有模拟所花费的时间往往会使原始代码的速度降低4-6倍没有并发。 为了使您更好地理解代码的工作方式,我有三个主要功能:交互功能,它是玩家和怪物之间的简单交互。