MSVC 内联 ASM 到 GCC

Bra*_*ley 3 c assembly gcc mingw

我正在尝试同时处理 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
    }
}
Run Code Online (Sandbox Code Playgroud)

我假设 ROR13 的工作原理类似,(val << 13) | (val >> (32 - 13))但代码不会产生相同的输出。

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

Cod*_*ray 6

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

但是,当您尝试这样做时,您必须在这里小心一点:有符号右移是在 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));
}
Run Code Online (Sandbox Code Playgroud)

(另请参阅C++ 中循环移位(旋转)操作的最佳实践)。

这将与您的原始代码具有相同的语义:ROR val, 13. 事实上,MSVC 将准确地生成该目标代码,GCC 也是如此。(有趣的是,Clang 会做ROL val, 19,它会产生相同的结果,考虑到旋转的工作方式。ICC 17 生成一个扩展的移位:SHLD val, val, 19。我不知道为什么;也许这比某些 Intel 处理器上的旋转快,或者它可能是在 Intel 上相同,但在 AMD 上速度较慢。)

Div16在纯 C 中实现,您需要:

signed long Div16(signed long a, signed long b)
{
    return ((long long)a << 16) / b;
}
Run Code Online (Sandbox Code Playgroud)

在可以进行本地 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
Run Code Online (Sandbox Code Playgroud)

不幸的是,在 32 位 x86 上,代码几乎没有那么好。编译器会调用提供扩展 64 位除法的内部库函数,因为它们无法证明使用单个 64b/32b => 32bidiv指令不会出错。(#DE如果商不适合,它将引发异常eax,而不仅仅是截断)

换句话说,转换:

int32_t Divide(int64_t a, int32_t b)
{
    return (a / b);
}
Run Code Online (Sandbox Code Playgroud)

进入:

mov   eax, a_low
mov   edx, a_high
idiv  b                 # will fault if a/b is outside [-2^32, 2^32-1]
ret
Run Code Online (Sandbox Code Playgroud)

不是合法的优化——编译器无法发出此代码。语言标准说 64/32 除法提升为 64/64 除法,它总是产生 64 位结果。稍后将 64 位结果强制转换为 32 位值与除法运算本身的语义无关。对于断层的一些组合a,并b违反了AS-if规则,除非编译器可以证明这些组合ab是不可能的。(例如,如果b已知大于1<<16,这可能是对 的合法优化,a = (int32_t)input; a <<= 16; 但即使这会为所有输入产生与 C 抽象机相同的行为,gcc 和 clang 当前不执行该优化。)


根本没有一种好方法可以覆盖语言标准强加的规则并强制编译器发出所需的目标代码。MSVC 不提供它的内在函数(虽然有一个 Windows API 函数,MulDiv但它并不快,并且只是使用内联汇编来实现自己的实现——并且在某些情况下一个错误,现在由于需要向后而巩固兼容性)。您基本上别无选择,只能求助于汇编,无论是内联还是从外部模块链接。

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

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
}
Run Code Online (Sandbox Code Playgroud)

这会在 Microsoft 和 GNU 风格的编译器上产生所需的输出。

嗯,主要是。出于某种原因,当您使用rm约束时,编译器可以自由选择是将除数视为内存操作数还是将其加载到寄存器中,Clang 会生成比您仅使用更糟糕的目标代码r(这迫使它将其加载到寄存器中)。这不会影响 GCC 或 ICC。如果您关心 Clang 上的输出质量,您可能只想使用r,因为这将在所有编译器上提供同样好的目标代码。

Godbolt Compiler Explorer 上的现场演示

(注意:GCCSAL在其输出中使用助记符,而不是SHL助记符。这些是相同的指令——区别只对右移很重要——并且所有理智的汇编程序员都使用SHL。我不知道 GCC 为什么会发出SAL,但你可以直接转换它精神上进入SHL。)