来自VS 2008/2010的x86 MUL指令

时间:2010-10-28 02:49:46

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

Visual Studio或Visual C ++ Express的现代(2008/2010)版本是否会在编译代码中生成x86 MUL指令(无符号乘法)?我似乎无法找到或设想它们出现在编译代码中的例子,即使使用无符号类型也是如此。

如果VS不使用MUL进行编译,是否有理由说明原因?

6 个答案:

答案 0 :(得分:25)

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

286 added an imul dest(reg), src(reg/mem), immediate形式,386添加了imul r32, r/m32形式,两者都只计算结果的下半部分。 (来自标签wiki的链接。)

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

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

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

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

答案 1 :(得分:9)

x86上有三种不同类型的乘法指令。第一个是MUL reg,它通过reg执行EAX的无符号乘法,并将(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指令在内部看起来像而不是跟踪一个写两个的指令。

答案 2 :(得分:4)

根据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

答案 3 :(得分:2)

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

答案 4 :(得分:2)

在我看了这个问题之后,我在生成的代码中找到了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相比是~75或IMUL(大约3或4个周期),因此即使您必须添加各种额外指令,尝试摆脱DIV也是有意义的。

我不完全理解这里的优化,但是如果您希望看到使用编译时间和乘法来划分常数的理性和数学解释,请参阅此paper

这是一个例子,编译器利用完整的64乘64位未截断乘法的性能和能力,而没有向c ++编码器显示任何符号。

答案 5 :(得分:1)

如前所述,C / C ++不执行word*word to double-word操作,这是mul指令最适合的操作。但是在某些情况下你需要word*word to double-word,所以你需要扩展到C / C ++。

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

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