【问题标题】:x86 assembly instructions optimisationx86 汇编指令优化
【发布时间】:2025-09-27 18:35:02
【问题描述】:

我正在尝试优化循环中的指令块,称为数千次,这是我算法的瓶颈。

此代码块计算 N 个矩阵 3x3(iA 数组)与 N 个向量 3(iV 数组)的乘法,并将 N 个结果存储在 oV 数组中。 (N 不固定,通常在 3000 到 15000 之间)

每行矩阵和向量都是 128 位对齐(4 个浮点数)以利用 SSE 优化(忽略第 4 个浮点值)。

C++ 代码:

  __m128* ip = (__m128*)iV;
  __m128* op = (__m128*)oV;
  __m128* A = (__m128*)iA;

  __m128 res1, res2, res3;
  int i;

  for (i=0; i<N; i++)
  {
    res1 = _mm_dp_ps(*A++, *ip, 0x71);
    res2 = _mm_dp_ps(*A++, *ip, 0x72);
    res3 = _mm_dp_ps(*A++, *ip++, 0x74);

    *op++ = _mm_or_ps(res1, _mm_or_ps(res2, res3));
  }

编译器生成这些指令:

000007FEE7DD4FE0  movaps      xmm2,xmmword ptr [rsi]               //move "ip" in register
000007FEE7DD4FE3  movaps      xmm1,xmmword ptr [rdi+10h]           //move second line of A in register
000007FEE7DD4FE7  movaps      xmm0,xmmword ptr [rdi+20h]           //move third line of A in register
000007FEE7DD4FEB  inc         r11d                                 //i++
000007FEE7DD4FEE  add         rbp,10h                              //op++
000007FEE7DD4FF2  add         rsi,10h                              //ip++
000007FEE7DD4FF6  dpps        xmm0,xmm2,74h                        //dot product of 3rd line of A against ip
000007FEE7DD4FFC  dpps        xmm1,xmm2,72h                        //dot product of 2nd line of A against ip
000007FEE7DD5002  orps        xmm0,xmm1                            //"merge" of the result of the two dot products
000007FEE7DD5005  movaps      xmm3,xmmword ptr [rdi]               //move first line of A in register
000007FEE7DD5008  add         rdi,30h                              //A+=3
000007FEE7DD500C  dpps        xmm3,xmm2,71h                        //dot product of 1st line of A against ip
000007FEE7DD5012  orps        xmm0,xmm3                            //"merge" of the result
000007FEE7DD5015  movaps      xmmword ptr [rbp-10h],xmm0           //move result in memory (op)
000007FEE7DD5019  cmp         r11d,dword ptr [rbx+28h]             //compare i
000007FEE7DD501D  jl          MyFunction+370h (7FEE7DD4FE0h)       //loop

我对低级优化不是很熟悉,所以问题是:如果我自己编写汇编代码,你会看到一些可能的优化吗?

例如,如果我改变它会运行得更快吗:

add         rbp,10h
movaps      xmmword ptr [rbp-10h],xmm0

通过

movaps      xmmword ptr [rbp],xmm0
add         rbp,10h

我还读到 ADD 指令比 INC 快...

【问题讨论】:

  • 对于您询问的特定微优化(add / movapsaddinc 的顺序)可能取决于您编码的特定 CPU 类型,阶段月亮等。我想即使您尝试过,您也不会看到可测量的差异,因为速度上的任何差异都会与主要计算中花费的时间重叠。从大局来看,我没有看到该代码有很多脂肪。也许 SSE 专家可以评论矢量化映射到英特尔矢量指令的程度。
  • “我还读到 ADD 指令比 INC 快...” -- 真的吗?我不知道组装的废话,但这似乎很奇怪。如果是这样的话,当你可以使用带有参数 1 的 ADD 时,你为什么还要使用 INC 呢?为什么 INC 会存在?
  • addinc 的事情与标志有关,在 inc 之后读取进位标志可能会带来麻烦。在 P4 上它有点相反,inc 必须等待标志。在这种情况下,您会立即覆盖标志,所以这个inc 在 P4 上不是问题(严重的是,谁在乎 P4)
  • @BenjaminLindley 因为它已经存在。他们不能只是删除它。inc 的问题在 8086 中不存在,它只是在当时串行执行一条指令。
  • @harold:好的。这就说得通了。谢谢。

标签: c++ optimization assembly x86 sse


【解决方案1】:

用偏移量计算间接地址,比如rbp-10 是很便宜的,因为在“有效地址计算”单元中有专门的硬件来进行这类计算[我认为有不同的名称,但想不到的或有任何成功与谷歌找到它的名字]。

但是,add rbp,10h[rbp-10h] 之间存在依赖关系,这可能会导致问题 - 但在这种特殊情况下我对此表示怀疑。在你的情况下,rbp-10 和使用它之间有很长的距离,所以这不是问题。编译器可能把它放得那么远,因为那时它是“免费的”,因为处理器将等待数据从外部进入之前读取的 xmm 寄存器。换句话说,我们可以在循环开始的xmm0xmm1xmm2 的读取之间以及使用xmm0xmm1xmm2dpps 指令之间进行的任何工作将是有益的,因为处理器将等待该数据“到达”,然后才能计算dpps 结果。

【讨论】:

    【解决方案2】:

    我做了很多 x86 汇编优化,我可以告诉你这是一次很棒的学习经历。它教会了我很多关于编译器如何工作的知识,我学到的最重要的事情是编译器通常非常擅长它们的工作。我知道这是一个轻率的评论,但这是真的......

    我还了解到,您进行的优化可能会对一个处理器系列产生积极影响,而对另一个处理器系列产生负面影响。流水线、分支预测和处理器缓存之类的东西发挥着巨大的作用......因此,除非您的目标是非常特定的硬件配置,否则请注意您所做的改进假设。

    对于您关于重新排序添加以删除rbp-10h 偏移量的具体问题......它看起来像是一个明显的改进,您必须通过阅读说明手册来验证,但我猜-10h该指令中的内存偏移量是免费的。移动add 可能会抛出流水线指令,实际上会导致时钟周期丢失。您必须进行实验。

    【讨论】:

    • 是的,现代 x86 是进行细粒度优化的喜怒无常的野兽。甚至个别指令的字节对齐也会产生影响,具体取决于处理器。
    【解决方案3】:

    您可以对上面的代码做一些事情来改进它。通常,在值被更改后使用它会导致处理器在等待结果时停止。所以这些行会受到惩罚:-

    add         rbp,10h
    movaps      xmmword ptr [rbp-10h],xmm0
    

    但是在这两行上面的代码 sn-p 中相距很远,所以这不是一个真正的问题。正如其他人已经说过的,rbp-10h 是“免费的”,因为地址计算硬件会处理它。

    您可以将movaps xmm3,xmmword ptr [rdi] 上移一行,也可以重新排列其他几行。

    值得吗?

    你很幸运能看到任何真正的性能提升,因为你的算法是

    <blink> memory bandwidth limited </blink>*
    

    这意味着将数据从 RAM 读取到 CPU 所花费的时间大于 CPU 进行处理所花费的时间。在最坏的情况下,读取内存地址可能涉及页面错误和磁盘读取。 prefetch 指令也无济于事,它被称为“流式 SIMD 扩展”,因为它针对将数据流式传输到 CPU 进行了优化(内存接口可以处理四个独立的流 IIRC)。

    如果您对一小部分数据(可能是 FFT)进行大量计算,那么您可以从手工制作汇编程序中获益良多。但是您的算法非常简单,因此 CPU 大部分时间都在空闲等待数据到达。如果您知道 RAM 的速度,您可以计算出算法的最大吞吐量,并将其与当前实现的吞吐量进行比较(尽管您永远不会达到最大理论吞吐量)。

    您可以采取一些措施来最大程度地减少内存停滞,这是一种更高级别的更改,而不是摆弄单个指令(通常,优化算法会获得更好的结果)。最简单的是双缓冲输入数据。将寄存器组分成两组(可以在这里做,因为您只使用了四个 SIMD 寄存器):-

      load set 1
    mainloop:
      load set 2
      do processing on set 1
      save set 1 result
      load set 1
      do processing on set 2
      save set 2 result
      goto mainloop
    

    希望这能给你一些想法。即使它没有走得更快,它仍然是一个有趣的练习,你可以从中学到很多东西。

    • RIP 闪烁。

    【讨论】: