MSVC内联ASM到GCC

我正在尝试处理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 <> (32 - 13)) (val <> (32 - 13))但代码不会产生相同的输出。

将此内联ASM转换为GCC的正确方法是什么和/或该代码的C转换是什么?

GCC使用与MSVC 完全不同的内联汇编语法 ,因此维护这两种forms需要相当多的工作。 这也不是一个特别好的主意。 内联汇编存在许多问题 。 人们经常使用它,因为他们认为它会使代码运行得更快,但通常会产生相反的效果。 除非您是汇编语言编译器代码生成策略的专家,否则让编译器的优化器生成代码要好得多 。

当你尝试这样做时,你必须在这里小心一点:签名的右移是在C中实现定义的,所以如果你关心可移植性,你需要将值转换为等效的无符号类型:

 #include  // for CHAR_BIT signed long ROR13(signed long val) { return ((unsigned long)val >> 13) | ((unsigned long)val << ((sizeof(val) * CHAR_BIT) - 13)); } 

(另请参阅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位除法,因为它们无法certificate使用单个64b / 32b => 32b idiv指令不会idiv 。 (如果商不适合eax ,它会引发#DEexception,而不仅仅是截断)

换句话说,改造:

 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位值与除法运算本身的语义无关。 对ab某些组合的故障会违反as-if规则,除非编译器能够certificateab那些组合是不可能的。 (例如,如果已知b大于1<<16 ,则这可能是a = (int32_t)input; a <<= 16;的合法优化a = (int32_t)input; a <<= 16;但即使这会产生与C摘要相同的行为所有输入的机器,gcc和clang目前都没有进行优化。)


根本没有一种好方法可以覆盖语言标准强加的规则,并强制编译器发出所需的目标代码。 MSVC没有为它提供内在function(尽管有一个Windows API函数, MulDiv ,它并不快,并且只是为自己的实现使用内联汇编 - 并且在某些情况下出现了错误 ,现在由于需要向后兼容性)。 你基本上别无选择,只能采用内联或从外部模块链接的程序集。

所以,你会变得丑陋。 它看起来像这样:

 signed long Div16(signed long a, signed long b) { #ifdef __GNUC__ // A GNU-style compiler (eg, 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 (ie, 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 ,因为这将在所有编译器上提供同样好的目标代码。

Godbolt Compiler Explorer上的现场演示

(注意:GCC在其输出中使用SAL助记符,而不是SHL助记符。这些是相同的指令 - 差异只对正确的移位很重要 - 所有理智的汇编程序员都使用SHL 。我不知道为什么GCC会发出SAL ,但是你可以将它精神上转换为SHL 。)