我正在尝试处理MSVC和GCC编译器,同时更新此代码库以使用GCC。但我不确定GCC内联ASM是如何工作的。现在我不擅长将ASM翻译成C,否则我只会使用C而不是ASM。
SLONG Div16(signed long a, signed long b)
{
signed long v;
#ifdef __GNUC__ // GCC doesnt work.
__asm() {
#else // MSVC
__asm {
#endif
mov edx, a
mov ebx, b
mov eax, edx
shl eax, 16
sar edx, 16
idiv ebx
mov v, eax
}
return v;
}
signed long ROR13(signed long val)
{
_asm{
ror val, 13
}
}
我假设ROR13与(val << 13) | (val >> (32 - 13))
类似,但代码不会产生相同的输出。
将此内联ASM转换为GCC的正确方法是什么和/或该代码的C转换是什么?
答案 0 :(得分:4)
GCC uses a completely different syntax for inline assembly比MSVC更好,因此维护这两种形式需要相当多的工作。这也不是一个特别好的主意。 There are many problems with inline assembly。人们经常使用它,因为他们认为它会使代码运行得更快,但通常会产生相反的效果。 Unless you're an expert in both assembly language and the compiler's code-generation strategies, you are far better off letting the compiler's optimizer generate the code
当你尝试这样做时,你必须要小心一点,但是:签名的右移是在C中实现定义的,所以如果你关心可移植性,你需要将值转换为等效的无符号类型:
#include <limits.h> // for CHAR_BIT
signed long ROR13(signed long val)
{
return ((unsigned long)val >> 13) |
((unsigned long)val << ((sizeof(val) * CHAR_BIT) - 13));
}
(另见Best practices for circular shift (rotate) operations in C++)。
这将与原始代码具有相同的语义:ROR val, 13
。事实上,MSVC将精确生成该目标代码,GCC也是如此。 (有趣的是,Clang会做ROL val, 19
,它会产生相同的结果,给定旋转的工作方式.ICC 17生成一个扩展的移位:SHLD val, val, 19
。我不知道为什么;也许这更快在某些英特尔处理器上轮换,或者在英特尔处可能相同,但在AMD上则相反。)
要在纯C中实现Div16
,您需要:
signed long Div16(signed long a, signed long b)
{
return ((long long)a << 16) / b;
}
在可以进行本机64位除法的64位架构上(假设long
仍然是类似于Windows的32位类型),这将转换为:
movsxd rax, a # sign-extend from 32 to 64, if long wasn't already 64-bit
shl rax, 16
cqo # sign-extend rax into rdx:rax
movsxd rcx, b
idiv rcx # or idiv b if the inputs were already 64-bit
ret
不幸的是,在32位x86上,代码并不是那么好。编译器向其内部库函数发出调用,提供扩展的64位除法,因为它们不能证明使用单个64b / 32b =&gt; 32b idiv
instruction不会出错。 (如果商不适合#DE
,它将引发eax
异常,而不仅仅是截断)
换句话说,改造:
int32_t Divide(int64_t a, int32_t b)
{
return (a / b);
}
成:
mov eax, a_low
mov edx, a_high
idiv b # will fault if a/b is outside [-2^32, 2^32-1]
ret
不是合法优化 - 编译器无法发出此代码。语言标准说64/32分区被提升为64/64分区,总是产生64位结果。您稍后将64位结果强制转换为32位值与除法运算本身的语义无关。对a
和b
的某些组合的错误会违反as-if规则,除非编译器能够证明a
和b
的组合是不可能的。 (例如,如果已知b
大于1<<16
,这可能是a = (int32_t)input; a <<= 16;
的合法优化,但即使这会产生与C抽象机相同的行为输入,gcc和clang
目前不做优化。)
没有一种好方法可以覆盖语言标准强加的规则,并强制编译器发出所需的目标代码。 MSVC没有为它提供内在功能(尽管有一个Windows API函数,MulDiv
,它并不快,只是使用内联汇编来实现它自己的实现 - 而a bug in a certain case现在已经巩固了,感谢需要向后兼容)。你基本上别无选择,只能采用内联或从外部模块链接的装配。
所以,你会变得丑陋。它看起来像这样:
signed long Div16(signed long a, signed long b)
{
#ifdef __GNUC__ // A GNU-style compiler (e.g., GCC, Clang, etc.)
signed long quotient;
signed long remainder; // (unused, but necessary to signal clobbering)
__asm__("idivl %[divisor]"
: "=a" (quotient),
"=d" (remainder)
: "0" ((unsigned long)a << 16),
"1" (a >> 16),
[divisor] "rm" (b)
:
);
return quotient;
#elif _MSC_VER // A Microsoft-style compiler (i.e., MSVC)
__asm
{
mov eax, DWORD PTR [a]
mov edx, eax
shl eax, 16
sar edx, 16
idiv DWORD PTR [b]
// leave result in EAX, where it will be returned
}
#else
#error "Unsupported compiler"
#endif
}
这导致Microsoft和GNU样式编译器上的所需输出。
嗯,大多数情况。出于某种原因,当您使用rm
约束时,它允许编译器自由选择是将除数视为内存操作数还是将其加载到寄存器中,Clang生成的对象代码比使用时更糟糕r
(强制它将其加载到寄存器中)。这不会影响GCC或ICC。如果您关心Clang的输出质量,您可能只想使用r
,因为这将在所有编译器上提供同样好的目标代码。
Live Demo on Godbolt Compiler Explorer
(注意:GCC在其输出中使用SAL
助记符,而不是SHL
助记符。这些是相同的指令 - 差异仅对于右移 - 和所有理智的汇编程序员都使用SHL
。我不知道为什么GCC会发出SAL
,但您可以将其精神转换为SHL
。)