Visual Studio或Visual C ++ Express的现代(2008/2010)版本是否会在编译代码中生成x86 MUL指令(无符号乘法)?我似乎无法找到或设想它们出现在编译代码中的例子,即使使用无符号类型也是如此。
如果VS不使用MUL进行编译,是否有理由说明原因?
答案 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
形式,两者都只计算结果的下半部分。 (来自x86标签wiki的链接。)
当将两个32位值相乘时,无论您认为值是有符号还是无符号,结果的最低有效32位都是相同的。换句话说,只有当你查看结果的“上半部分”时,有符号和无符号乘法之间的差异才会变得明显,一个操作数imul
/ mul
放入{{1}并且两个或三个操作数edx
无处可去。因此,imul
的多操作数形式可用于有符号和无符号值,并且英特尔也无需添加新形式的imul
。 (他们可以使多操作数mul
成为mul
的同义词,但这会使反汇编输出与源不匹配。)
在C中,算术运算的结果与操作数具有相同的类型(在窄整数类型的整数提升之后)。如果您将两个imul
相乘,则得到int
,而不是int
:不保留“上半部分”。因此,C编译器只需要long long
提供的内容,并且由于imul
比imul
更容易使用,因此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.pdf,IMUL
指令具有较低的延迟和较高的吞吐量(如果我正确读取表)。也许VS只是使用更快的指令(假设IMUL
和MUL
总是产生相同的输出)。
我没有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
类型。