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

为什么引入无用的MOV指令会加速x86\u 64汇编中的紧密循环?

谷梁楷
2023-03-14

背景:

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

令我惊讶的是,删除不必要的指令导致我的程序速度减慢。

我发现添加任意、无用的MOV指令会进一步提高性能。

效果是不稳定的,并且基于执行顺序的更改:由单行向上或向下转置的相同垃圾指令会产生减速。

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

数据:

我的代码的一个版本在一个循环的中间有条件地编译了三个垃圾操作,该循环运行了1048576次。(周围的程序只计算SHA-256散列)。

我相当旧的机器(Intel(R)Core(TM)2 CPU 6400@2.13)上的结果

avg time (ms) with -dJUNKOPS: 1822.84 ms
avg time (ms) without:        1836.44 ms

这些程序循环运行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;

自己试试:

如果你想亲自试用,代码可以在GitHub上在线下载。

我的问题:

  • 为什么无用地将寄存器的内容复制到RAM会提高性能

共有3个答案

吕翰飞
2023-03-14

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

现代CPU是RISC/CISC的混合体,它们将CISC x86指令转换为行为更RISC的内部指令。此外,还有无序执行分析器、分支预测器、英特尔的“微操作融合”,它们试图将指令分组为更大批量的同时工作(有点像VLIW/安腾泰坦尼克号)。甚至还有缓存边界,如果代码更大(也许缓存控制器更智能地插入它,或者让它保持更长时间),它可以让代码运行得更快,这是天知道的。

CISC总是有一个汇编到微码的转换层,但问题是,使用现代CPU时,事情要复杂得多。由于现代半导体制造厂中存在着所有额外的晶体管不动产,CPU可能可以并行应用几种优化方法,然后在最后选择一种提供最佳加速比的方法。额外的指令可能会使CPU偏向使用一条优于其他路径的优化路径。

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

杨轶
2023-03-14

你可能想看看http://research.google.com/pubs/pub37077.html

TL;DR:在程序中随机插入nop指令可以很容易地将性能提高5%或更多,不,编译器不能轻易利用这一点。它通常是分支预测器和缓存行为的组合,但也可以是例如预订站暂停(即使没有中断的依赖链或明显的资源过度订阅)。

顾兴昌
2023-03-14

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

  • 插入MOV会将后续指令转移到不同的内存地址

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

这个分支预测小教程展示了分支预测缓冲区的工作原理。缓存缓冲区由分支指令地址的较低部分索引。除非两个重要的不相关分支共享相同的低位,否则这种方法效果很好。在这种情况下,最终会出现别名,这会导致许多预测失误的分支(这会暂停指令管道并减慢程序的速度)。

如果您想了解分支预测失误是如何影响性能的,请看下面这个极好的答案:https://stackoverflow.com/a/11227902/1001643

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

 类似资料:
  • 我有两段代码,它们从gdb转储中生成了以下装配线指令。 然后是这段代码。 可以看出,唯一的区别是这条线的位置: 在一个版本中,它在循环内部,在另一个版本中,它在循环外部。我期望循环内部较少的版本运行得更快,然而它却运行得更慢。 这是为什么呢? 如果相关,以下是我自己实验的细节以及产生它的c代码。 我在运行红帽企业 Linux 工作站(版本 7.5)或 Windows 10 的多台计算机上对此进行了

  • 我反汇编(使用objdump -d)这个操作码(c7 45 fc 05 00 00 00)并得到这个(移动DWORD PTR [rbp-0x4],0x5)。然后我尝试解码自己,我认为应该是(移动DWORD PTR [ebp-0x4],0x5)。为什么它是RBP寄存器而不是EBP寄存器?我错过了什么吗? 在这里,我尝试:首先,我看看C7操作码的mov操作码。 C7 /0 iw |视场角 r/m16,

  • 该程序应该使用int 0x10在ASCII中打印一个具有给定字符的金字塔,3行的预期结果(下面代码中使用的数量)将是: A. a a a a a 要编译和运行代码,我使用nasm编译它,然后使用qemu进行仿真: 然而,程序get无法打印所有ASCII值。此外,如果有任何针对nasm代码的调试器,可以让您逐行运行,允许您检查寄存器值,这对学习也很有帮助。

  • 我正在尝试创建一个愚蠢的自旋锁版本。浏览网页时,我在x86中遇到了一条名为“PAUSE”的汇编指令,该指令用于向处理器提示该CPU上当前正在运行自旋锁。英特尔手册和其他可用信息声明: 在大多数情况下,处理器使用此提示来避免内存顺序冲突,这大大提高了处理器性能。因此,建议在所有自旋等待循环中放置暂停指令。文档还提到“等待(一些延迟)”是指令的伪实现。 上段最后一行很直观,如果我抢锁不成功,我必须等一

  • 我想知道如何将指针参数传递给过程? 我必须创建具有2个参数的函数: 单词数组 数组的大小 该函数获取数组的大小并对列求和。 这就是我所编码的: 问题 - 它正在添加偏移指针(这里是,,)而不是偏移值(这里是:,,,)。 因此,结果将为 6 而不是 10。 有人能帮我吗?

  • 我读过,x86的INC指令不是原子指令。我的问题是为什么会这样?假设我们在x86-64上递增一个64位整数,我们可以用一条指令来递增,因为INC指令同时处理内存变量和寄存器。那么为什么它不是原子的呢?