【问题标题】:x86 MUL Instruction from VS 2008/2010来自 VS 2008/2010 的 x86 MUL 指令
【发布时间】:2010-10-28 02:49:45
【问题描述】:

Visual Studio 或 Visual C++ Express 的现代 (2008/2010) 咒语会在编译后的代码中生成 x86 MUL 指令(无符号乘法)吗?即使使用无符号类型,我似乎也找不到或设计出它们出现在编译代码中的示例。

如果 VS 不使用 MUL 编译,有什么理由吗?

【问题讨论】:

  • 否则它会使用什么指令?
  • @Jeff M 我认为海报的意思可能是在编译的代码中使用了 IMUL。
  • @pst:我只是问,因为我无权访问编译器,也看不到实际使用了哪些指令。我屈服并启动了我的开发机器来解决这个问题。 :)
  • @Jeff M 我是古玩(但不是那么好奇 ;-),并试图提示发帖人添加说明:p
  • (编辑不起作用)为了进一步澄清我的问题,我基本上想知道英特尔是否发布了一些围绕 MUL 与 IMUL 的优化建议。或者 MS 是否为他们使用的说明提供了理由(不太可能)。

标签: c++ visual-studio compiler-construction assembly x86


【解决方案1】:

imul(有符号)和mul(无符号)都有一个与edx:eax = eax * src 相同的单操作数形式。即 32x32b => 64b 全乘(或 64x64b => 128b)。

186 added an imul dest(reg), src(reg/mem), immediate 形式,386 增加了imul r32, r/m32 形式,两者都只计算结果的下半部分。 (根据NASM's appendix B,另见x86 tag wiki

当两个 32 位值相乘时,结果的最低有效 32 位是相同的,无论您认为这些值是有符号还是无符号。换句话说,有符号乘法和无符号乘法之间的区别只有在您查看结果的“上”半部分时才会变得明显,其中一个操作数 imul/mul 放入 edx 和两个或三个操作数imul 无处可去。因此,imul 的多操作数形式可用于有符号和无符号值,英特尔也无需添加新形式的 mul。 (他们本可以将多操作数 mul 设为 imul 的同义词,但这会使反汇编输出与源不匹配。)

在 C 中,算术运算的结果与操作数具有相同的类型(在窄整数类型的整数提升之后)。如果将两个int 相乘,则得到int,而不是long long:“上半部分”不保留。因此,C 编译器只需要imul 提供的内容,并且由于imulmul 更易于使用,C 编译器使用imul 以避免需要mov 指令来将数据输入/输出@987654346 @。

作为第二步,由于 C 编译器大量使用 imul 的多操作数形式,英特尔和 AMD 投入精力使其尽可能快。它只写入一个输出寄存器,而不是e/rdx:e/rax,因此CPU 可以比单操作数形式更容易地对其进行优化。这让imul 更具吸引力。

mul/imul 的单操作数形式在实现大数运算时很有用。在 C 中,在 32 位模式下,您应该通过将 unsigned long long 值相乘来获得一些 mul 调用。但是,取决于编译器和操作系统,那些mul 操作码可能隐藏在某些专用函数中,因此您不一定会看到它们。在 64 位模式下,long long 只有 64 位,而不是 128,编译器会简单地使用imul

【讨论】:

  • 您确定 IMUL/MUL 优化的因果关系吗? VS 是否可能更喜欢 IMUL,因为它恰好已经更快(副编译器更喜欢它,导致 Intel/AMD 让它更快)?
  • @Mike:在 80386 上,mulimul 提供相同的速度,并且 C 编译器已经在使用 imul,因为选择寄存器很方便。所以我认为编译器首先选择,处理器供应商紧随其后,而不是相反。
  • 在 64 位模式下,它将使用 mul 表示 __int128
  • 在 64 位模式下,mul 是否返回高 64 位字?我的意思是rdx 中的低位 64 位字和 rdx 中的高位字?
  • imul 指令的直接形式是在 186 上添加的,而不是在 286 上。
【解决方案2】:

x86 上有三种不同类型的乘法指令。第一个是MUL reg,它将EAX 与reg 相乘,并将(64 位)结果放入EDX:EAX。第二个是IMUL reg,它与有符号乘法相同。第三种是IMUL reg1, reg2(将reg1与reg2相乘并将32位结果存储到reg1中)或IMUL reg1, reg2, imm(将reg2乘以imm并将32位结果存储到reg1中)。

由于在 C 中,两个 32 位值的乘积产生 32 位结果,编译器通常使用第三种类型(符号无关紧要,低 32 位在有符号和无符号 32x32 乘法之间一致)。如果您实际使用完整的 64 位结果,VC++ 将生成 MUL/IMUL 的“长乘”版本,例如这里:

unsigned long long prod(unsigned int a, unsigned int b)
{
  return (unsigned long long) a * b;
}

IMUL 的 2 操作数(和 3 操作数)版本比单操作数版本更快,因为它们不会产生完整的 64 位结果。宽乘法器又大又慢;如有必要,使用 Microcode 构建更小的乘法器和合成长乘法器要容易得多。此外,MUL/IMUL 写入两个寄存器,这通常通过在内部将其分解为多个指令来解决 - 指令重新排序硬件更容易跟踪两个相关指令,每个指令都写入一个寄存器(大多数 x86 指令在内部看起来像这样) 而不是跟踪一条写入两条的指令。

【讨论】:

  • 现代英特尔 CPU(SnB 系列)的问题在于拆分结果,而不是实际进行宽乘。 imul r,r(任何大小,包括 64 位)为 1 uop。 imul r/m32 是 3 uops,而imul r/m64 只有 2 uops,可能是因为它不必拆分 64 位乘法器硬件的输出? imul r/m8 是 1 uop,大概是因为结果进入 AX,所以它仍然只有一个寄存器。顺便说一句,CPU 架构师会说大多数 x86 整数指令写入两个输出寄存器,包括标志。
【解决方案3】:

根据http://gmplib.org/~tege/x86-timing.pdfIMUL 指令具有更低的延迟和更高的吞吐量(如果我正确读取表格的话)。也许VS只是使用更快的指令(假设IMULMUL总是产生相同的输出)。

我没有方便的 Visual Studio,所以我尝试使用 GCC 来获得其他东西。我也总是得到IMUL 的一些变化。

这个:

unsigned int func(unsigned int a, unsigned int b)
{ 
    return a * b;
}

组装到这个(使用-O2):

_func:
LFB2:
        pushq   %rbp
LCFI0:
        movq    %rsp, %rbp
LCFI1:
        movl    %esi, %eax
        imull   %edi, %eax
        movzbl  %al, %eax
        leave
        ret

【讨论】:

    【解决方案4】:

    我的直觉告诉我编译器任意选择了IMUL(或两者中较快的那个),因为无论是使用无符号MUL 还是有符号IMUL,这些位都是相同的。任何 32 位整数乘法将是 64 位,跨越两个寄存器 EDX:EAX。溢出进入EDX,这基本上被忽略了,因为我们只关心EAX 中的32 位结果。使用 IMUL 将根据需要将符号扩展到 EDX,但同样,我们不在乎,因为我们只对 32 位结果感兴趣。

    【讨论】:

      【解决方案5】:

      在我查看这个问题之后,我在除法时在生成的代码中发现了 MULQ。

      完整的代码将一个大的二进制数转换为十亿的块,以便可以轻松地将其转换为字符串。

      C++ 代码:

      for_each(TempVec.rbegin(), TempVec.rend(), [&](Short & Num){
          Remainder <<= 32;
          Remainder += Num;
          Num = Remainder / 1000000000;
          Remainder %= 1000000000;//equivalent to Remainder %= DecimalConvert
      });
      

      优化的生成程序集

      00007FF7715B18E8  lea         r9,[rsi-4]  
      00007FF7715B18EC  mov         r13,12E0BE826D694B2Fh  
      00007FF7715B18F6  nop         word ptr [rax+rax] 
      00007FF7715B1900  shl         r8,20h  
      00007FF7715B1904  mov         eax,dword ptr [r9]  
      00007FF7715B1907  add         r8,rax  
      00007FF7715B190A  mov         rax,r13  
      00007FF7715B190D  mul         rax,r8  
      00007FF7715B1910  mov         rcx,r8  
      00007FF7715B1913  sub         rcx,rdx  
      00007FF7715B1916  shr         rcx,1  
      00007FF7715B1919  add         rcx,rdx  
      00007FF7715B191C  shr         rcx,1Dh  
      00007FF7715B1920  imul        rax,rcx,3B9ACA00h  
      00007FF7715B1927  sub         r8,rax  
      00007FF7715B192A  mov         dword ptr [r9],ecx  
      00007FF7715B192D  lea         r9,[r9-4]  
      00007FF7715B1931  lea         rax,[r9+4]  
      00007FF7715B1935  cmp         rax,r14  
      00007FF7715B1938  jne         NumToString+0D0h (07FF7715B1900h)  
      

      注意 MUL 指令 5 行以下。 我知道,这个生成的代码非常不直观,实际上它看起来与编译后的代码完全不同,但 DIV 非常慢,对于 32 位 div 约 25 个周期,根据现代 PC 上的 chart 与 MUL 或 IMUL 相比,约 75 个周期(大约 3 或 4 个周期),因此即使您必须添加各种额外的指令,尝试摆脱 DIV 也是有意义的。

      我不完全理解这里的优化,但是如果您想了解使用编译时间和乘法来除常量的理性和数学解释,请参阅paper

      这是编译器利用完整的 64 x 64 位未截断乘法的性能和能力而不向 c++ 编码器显示任何迹象的示例。

      【讨论】:

      • 这是使用 VS 2013 编译的,具有默认的发布设置。而且我还在 GCC -O2 上找到了相同的优化。
      • 在 90 年代,大量 CPU 甚至没有 硬件除法指令。你找到的那篇论文的作者负责了大部分工作。如果您好奇,请通读expmed.c,其中大部分是在实现此优化。我不知道 MSVC 也可以做到这一点,但这并不让我感到惊讶。
      • 过去的爆炸 ^_^ 这是一个有趣的例子,感谢您提供论文链接。 @zwol 感谢您提供额外的上下文。
      【解决方案6】:

      正如已经解释过的,C/C++ 不执行 word*word to double-word 操作,而这是 mul 指令最适合的。但在某些情况下,您需要 word*word to double-word,因此您需要扩展 C/C++。

      GCC、Clang 和 ICC 提供了一个内置类型 __int128,您可以使用它来间接获取 mul 指令。

      使用 MSVC,它提供了 _umul128 内在函数(至少从 VS 2010 开始),它生成 mul 指令。使用此内在函数和 _addcarry_u64 内在函数,您可以使用 MSVC 构建自己的高效 __int128 类型。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-05-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多