【问题标题】:MUL/DIV instructions vs. MOV & SHL/SHR (Pentium Pro)MUL/DIV 指令与 MOV & SHL/SHR (Pentium Pro)
【发布时间】:2016-12-03 20:44:58
【问题描述】:

为什么要使用:

MOV EAX, 22 
SHL EAX, 2

...当乘以 4 而不是仅使用 MUL 指令时?
我知道这也可以用SHR 而不是DIV 来完成。

这样做有什么好处?
你也可以用奇数来做这个,还是只能是偶数?

【问题讨论】:

  • 想想以 10 为底,左/右移动乘以 10 的幂比进行真正的乘法要快得多(而且没有人这样做)。同样的事情也适用于在任何基础上乘以基础的幂
  • 要详细了解 asm 中的快速特性,请参阅 x86 tag wiki,尤其是 Agner Fog's guides。另请参阅this answer I wrote,了解 shift 和 LEA 与 DIV 相比究竟有多快。现代 Intel CPU 具有极高的性能乘法硬件(例如 3 周期延迟,每 1c 吞吐量一个 imul r64, r64),但即时移位甚至更快(1c 延迟,每时钟 tput 两个)。
  • 为什么“Pentium Pro”在这个问题中发挥了重要作用? a)问题主体中未提及,b)它们早已过时,c)答案相对稳定且对现代架构有用。从问题标题中删除?

标签: assembly x86 opcodes


【解决方案1】:

有许多代码习语比“MUL 常量”更快。

现代 x86 CPU 至少在几个时钟内执行 MUL。因此,任何在 1-2 个时钟内计算乘积的代码序列都将优于 MUL。您可以使用快速指令(ADD、SHL、LEA、NEG)以及处理器可以在单个时钟中并行执行其中一些指令来替换 MUL 的事实。可以说,这意味着如果您避免某些数据依赖性,您可以在 2 个时钟内以多种组合执行其中的 4 条指令。

LEA 指令特别有趣,因为它可以乘以一些小常数 (1,2,3,4,5,8,9) 以及将乘积移动到另一个寄存器,这是破坏数据的一种简单方法依赖关系。这允许您在不破坏原始操作数的情况下计算子积。

一些例子:

将 EAX 乘以 5,将产品移至 ESI:

   LEA ESI, [EAX+4*EAX]    ; this takes 1 clock

将 EAX 乘以 18:

   LEA  EAX, [EAX + 8*EAX]
   SHL  EAX, 1

将 EAX 乘以 7,将结果移动到 EBX:

   LEA  EBX, [8*EAX]
   SUB  EBX, EAX

将 EAX 乘以 28:

   LEA  EBX, [8*EAX]
   LEA  ECX, [EAX+4*EAX]  ; this and previous should be executed in parallel
   LEA  EAX, [EBX+4*ECX]

乘以 1020:

   LEA  ECX, [4*EAX]
   SHL  EAX, 10         ; this and previous instruction should be executed in parallel
   SUB  EAX, ECX

乘以 35

   LEA  ECX, [EAX+8*EAX]
   NEG  EAX             ; = -EAX
   LEA  EAX, [EAX+ECX*4]

所以,当你想达到乘以一个适度的大小常数的效果时,你必须考虑如何将它“分解”到 LEA 指令可以产生的各种乘积中,以及如何移位、相加、 或减去部分结果得到最终答案。

通过这种方式可以产生多少乘以常数是惊人的。 您可能认为这仅对非常小的常量有用,但正如您从上面的 1020 示例中看到的那样,您也可以得到一些令人惊讶的中等大小的常量。这在索引到结构数组时非常方便,因为您必须将索引乘以结构的大小。 通常在索引这样的数组时,您想要计算元素地址并获取值;在这种情况下,您可以将最终的 LEA 指令合并到 MOV 指令中,而这对于真正的 MUL 是无法做到的。这会为您购买额外的时钟周期,以便通过这种类型的习语在其中执行 MUL。

[我已经构建了一个编译器,它使用这些指令通过对指令组合进行小而详尽的搜索来计算“最佳乘以常数”;然后它会缓存该答案以供以后重用]。

【讨论】:

  • imul r, r/m, imm32 作为 mov-and-multiply 相当不错。在现代 Intel CPU 上,它只有 3 个周期延迟(即使对于 64 位操作数大小),并且每个时钟吞吐量只有一个。但是,许多乘法常数可以在 2 个周期内完成,正如您通过指令级并行性示例很好地演示的那样。 gcc 和 clang 做同样的事情。 (clang-3.6 和更早版本通常更喜欢 IMUL,如果它不能只使用一个 LEA 来完成这项工作,但现代 clang 更喜欢延迟而不是指令 / uop 计数,就像 gcc 所做的那样。)
【解决方案2】:

使用SHL/SHR指令一般来说比MUL/DIV快很多。

要回答您的第二个问题,您也可以使用奇数进行此操作,但您必须添加另一条指令。所以从技术上讲,你不能只使用SHL/SHR

例如:下面的代码在不使用MUL指令的情况下乘以5:

mov num, 5
mov eax, num
mov ebx, num
shl eax, 2    ; MULs by 4
add eax, ebx  ; ADD the x1 to make = 5

【讨论】:

  • 移位的周期数取决于 cpu 型号,但很长一段时间都不是每比特 1 个时钟(如果有的话)。他也没有问关于乘以 5 的问题,而你在那里使用了ADD:P
  • 仅在 80186 上,每位移位花费 1 个周期。在 8086 上,它每比特花费 4 个周期,并且它不支持像您在这里使用的移位计数的立即操作数。 80286 和更高版本的 CPU 都具有桶形移位器,可以在单个周期内执行任何大小的移位。现代乱序 CPU 可能同时执行两个班次,有效地将班次成本降低到半个周期。
  • @ninjalj 不,Pentium 4 也不例外。由于 Pentium 4 也有一个桶形移位器,因此无论移位的位数如何,延迟都是恒定的。许多旧 CPU 有额外的开销,但转换本身只需要一个周期。自 '286 以来,移位指令的成本不取决于移位的位数。
  • 是的,我已经阅读了 Darek 的所有文章。它们不是特别权威,而且他以发表一些缺乏根据的咆哮而闻名,但他仍然是一个聪明的人,如果你有时间感兴趣的话,关于 x86 架构的相关文章值得一读。但是这个特殊的问题是我在多个来源中看到的,所以我很好奇为什么罗斯里奇声称相反。也许他知道一些我不知道的事情。
  • @CodyGray 我没有来源,这没有任何意义。也许它是一个缓慢的桶形移位器,其中传播延迟将执行时间推到一个周期,但对我来说更可能的解释是延迟来自其他地方。我怀疑这与 Pentium 4 (pre-Prescott) 上的移位指令有关,该指令被列为在 Agner Fog 表中的“mmxsh”子单元上执行。这表明额外的循环是使用 MMX 桶形移位器进行整数移位的结果,类似于 MUL/DIV 的额外循环,因为它们是使用 FP 单元执行的。
猜你喜欢
  • 2018-09-25
  • 1970-01-01
  • 2011-03-13
  • 1970-01-01
  • 1970-01-01
  • 2020-07-24
  • 2012-06-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多