为什么引入无用的MOV指令会加速x86_64汇编中的紧凑循环?

tan*_*orm 217 optimization performance assembly freepascal x86-64

背景:

在使用嵌入式汇编语言优化某些Pascal代码时,我注意到了一条不必要的MOV指令,并将其删除.

令我惊讶的是,删除不必要的指令会导致我的程序变慢.

我发现添加任意无用的MOV指令可以进一步提高性能.

效果不稳定,并且基于执行顺序进行更改:相同的垃圾指令向上或向下移动一行会产生减速.

我知道CPU会进行各种优化和精简,但这看起来更像是黑魔法.

数据:

我的代码版本有条件地在运行时间的循环中编译三个垃圾操作2**20==1048576.(周围的程序只计算SHA-256哈希值).

在我相当老的机器(英特尔(R)Core(TM)2 CPU 6400 @ 2.13 GHz)上的结果:

avg time (ms) with -dJUNKOPS: 1822.84 ms
avg time (ms) without:        1836.44 ms
Run Code Online (Sandbox Code Playgroud)

程序在循环中运行25次,每次运行顺序随机变化.

摘抄:

{$asmmode intel}
procedure example_junkop_in_sha256;
  var s1, t2 : uint32;
  begin
    // Here are parts of the SHA-256 algorithm, in Pascal:
    // s0 {r10d} := ror(a, 2) xor ror(a, 13) xor ror(a, 22)
    // s1 {r11d} := ror(e, 6) xor ror(e, 11) xor ror(e, 25)
    // Here is how I translated them (side by side to show symmetry):
  asm
    MOV r8d, a                 ; MOV r9d, e
    ROR r8d, 2                 ; ROR r9d, 6
    MOV r10d, r8d              ; MOV r11d, r9d
    ROR r8d, 11    {13 total}  ; ROR r9d, 5     {11 total}
    XOR r10d, r8d              ; XOR r11d, r9d
    ROR r8d, 9     {22 total}  ; ROR r9d, 14    {25 total}
    XOR r10d, r8d              ; XOR r11d, r9d

    // Here is the extraneous operation that I removed, causing a speedup
    // s1 is the uint32 variable declared at the start of the Pascal code.
    //
    // I had cleaned up the code, so I no longer needed this variable, and 
    // could just leave the value sitting in the r11d register until I needed
    // it again later.
    //
    // Since copying to RAM seemed like a waste, I removed the instruction, 
    // only to discover that the code ran slower without it.
    {$IFDEF JUNKOPS}
    MOV s1,  r11d
    {$ENDIF}

    // The next part of the code just moves on to another part of SHA-256,
    // maj { r12d } := (a and b) xor (a and c) xor (b and c)
    mov r8d,  a
    mov r9d,  b
    mov r13d, r9d // Set aside a copy of b
    and r9d,  r8d

    mov r12d, c
    and r8d, r12d  { a and c }
    xor r9d, r8d

    and r12d, r13d { c and b }
    xor r12d, r9d

    // Copying the calculated value to the same s1 variable is another speedup.
    // As far as I can tell, it doesn't actually matter what register is copied,
    // but moving this line up or down makes a huge difference.
    {$IFDEF JUNKOPS}
    MOV s1,  r9d // after mov r12d, c
    {$ENDIF}

    // And here is where the two calculated values above are actually used:
    // T2 {r12d} := S0 {r10d} + Maj {r12d};
    ADD r12d, r10d
    MOV T2, r12d

  end
end;
Run Code Online (Sandbox Code Playgroud)

亲自尝试一下:

如果您想自己试用,代码在GitHub上在线.

我的问题:

  • 为什么无用地将寄存器的内容复制到RAM会不会提高性能?
  • 为什么同样无用的指令会在某些线路上提供加速,而在其他线路上则会减速?
  • 这种行为是否可以被编译器可预测地利用?

Ray*_*ger 141

速度提升的最可能原因是:

  • 插入MOV会将后续指令移到不同的存储器地址
  • 其中一个移动指令是一个重要的条件分支
  • 由于分支预测表中的混叠,该分支被错误预测
  • 移动分支消除了别名并允许正确预测分支

您的Core2没有为每个条件跳转保留单独的历史记录.相反,它保留了所有条件跳转的共享历史记录.全局分支预测的一个缺点是,如果不同的条件跳转是不相关的,则历史被无关信息稀释.

这个小分支预测教程展示了分支预测缓冲区如何工作.缓存缓冲区由分支指令的地址的下部索引.除非两个重要的不相关分支共享相同的低位,否则这很有效.在这种情况下,您最终会出现别名,这会导致许多错误预测的分支(这会拖延指令管道并减慢程序速度).

如果您想了解分支机构误预测会如何影响性能,请查看以下优秀答案:https: //stackoverflow.com/a/11227902/1001643

编译器通常没有足够的信息来知道哪些分支将是别名,以及这些别名是否重要.但是,可以使用CachegrindVTune等工具在运行时确定该信息.

  • 对Bochs的一个贡献者遇到的类似问题进行了绝对优秀的讨论,您可能希望将其添加到您的答案中:http://www.emulators.com/docs/nx25_nostradamus.htm (3认同)
  • 保持对齐不仅仅是分支目标.对于Core2和Nehalem来说,解码瓶颈是一个巨大的问题:它经常很难保持执行单元的繁忙.Sandybridge推出的uop缓存大大增加了前端吞吐量.对齐分支目标是*因为*这个问题,但它影响所有代码. (3认同)
  • 唔。这听起来很有希望。这个 sha256 实现中唯一的条件分支是对 FOR 循环结束的检查。当时,我在 git 中将这个修订标记为一个奇怪的东西并继续优化。我接下来的步骤之一是在汇编中自己重写 pascal FOR 循环,此时这些额外的指令不再产生积极影响。也许 free pascal 生成的代码对于处理器来说比我用它替换的简单计数器更难预测。 (2认同)

小智 79

您可能需要阅读http://research.google.com/pubs/pub37077.html

TL; DR:在程序中随机插入nop指令可以轻松地将性能提高5%或更多,不然,编译器不能轻易利用它.它通常是分支预测器和缓存行为的组合,但它也可以是例如保留站停顿(即使在没有任何依赖链被破坏或明显的资源超额预订的情况下).

  • 汇编程序未经过优化. (8认同)
  • 编译器可以通过执行非常昂贵的优化来利用它,例如重复构建和分析,然后使用模拟退火或遗传算法改变编译器输出.我已经阅读了该领域的一些工作.但我们说的是至少需要5-10分钟的100%CPU来编译,最终的优化可能是CPU核心模型,甚至是特定的核心或微码修订版. (4认同)

小智 14

我相信现代CPU中的汇编指令虽然是程序员向CPU提供执行指令的最后一个可见层,但实际上是CPU实际执行的几个层.

现代CPU是RISC/CISC混合,它将CISC x86指令转换为更符合RISC行为的内部指令.此外,还有无序执行分析器,分支预测器,英特尔的"微操作融合",试图将指令分组到更大批量的同步工作中(有点像VLIW/Itanium泰坦尼克号).甚至有缓存边界可以使代码运行得更快,因为上帝知道 - 为什么如果它更大(可能是缓存控制器更智能地插槽,或保持更长时间).

CISC总是有一个汇编到微码的转换层,但重点是现代CPU的事情要复杂得多.利用现代半导体制造工厂中所有额外的晶体管空间,CPU可以并行应用多种优化方法,然后在最后选择提供最佳加速的方法.额外的指令可能会偏向CPU以使用一个比其他更好的优化路径.

额外指令的效果可能取决于CPU型号/生成/制造商,并且不太可能是可预测的.以这种方式优化汇编语言需要针对许多CPU架构生成执行,可能使用特定于CPU的执行路径,并且仅对于真正非常重要的代码段才是可取的,尽管如果您正在进行汇编,您可能已经知道了.

  • 没有人能够确切地知道为什么OP正在观察这种奇怪的行为,除非它是英特尔的工程师,他可以使用特殊的诊断设备.所以其他所有人都可以猜到.那不是@ cowarldlydragon的错. (7认同)
  • 你的答案有点令人困惑.在许多地方,你似乎在猜测,虽然你说的大多数是正确的. (6认同)
  • 也许我应该澄清一下.我觉得令人困惑的是缺乏确定性 (2认同)
  • 猜测是有道理的并且有充分的论证是完全有效的. (2认同)
  • Downvote; 没有你所说的解释了OP所看到的行为.你的答案毫无用处. (2认同)
  • 其中一些猜测是错误的."CPU ......并行应用几种优化方法......"不.OOO核心中的东西是非常确定的."也许缓存控制器更智能地插入它":不,没有办法让猜测与现实相符.回答这个问题的唯一方法就是说"因为现代的OOO核心很复杂,并且附近的指令之间存在相互作用".请参阅http://agner.org/optimize/上的microarch pdf,了解具体内容.在这种情况下,它很可能是对齐 - >解码器问题,因为Core2 ...... (2认同)

归档时间:

查看次数:

34991 次

最近记录:

8 年,3 月 前