2017-09-15 269 views
1

我试图同时处理MSVC和GCC编译器,同时更新这个代码库对GCC工作。但我不确定GCC内联ASM的工作原理。现在我不擅长将ASM转换为C,否则我只会使用C而不是ASM。MSVC内联汇编GCC的

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译法?

+1

“asm”指令的不同语法不是你唯一的问题。即使对于汇编程序指令,GCC也使用[不同语法](https://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax)。 –

+0

'ror' rotate ** right ** so'(val >> 13)| (val <<(32 - 13))' – Jester

+0

[编译器内部函数](https://en.wikipedia.org/wiki/Intrinsic_function)可能对您有所帮助。例如,这个Visual Studio [x86 Intrinsics List](https://msdn.microsoft.com/en-us/library/hh977023.aspx) –

回答

3

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。我不确定为什么;也许这比某些Intel处理器上的旋转要快,或者。也许是英特尔相同的,但对AMD慢)

要在纯C实现Div16,你想:

signed long Div16(signed long a, signed long b) 
{ 
    return ((long long)a << 16)/b; 
} 

在64位架构,可以做原生64位除法,(假设long仍然是一个32位的类型等在Windows上),这将转化为:

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 => 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位值与分割操作本身的语义无关。对于ab的某些组合的错误将违反假定规则,除非编译器可以证明ab的这些组合是不可能的。(例如,如果知道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约束条件时,编译器可以自由选择将除数作为内存操作数处理还是将其加载到寄存器中,但与仅使用r(其中的force它将其加载到一个寄存器中)。这不影响GCC或ICC。如果您关心Clang的输出质量,那么您可能只想使用r,因为这会在所有编译器上提供同样好的目标代码。

Live Demo on Godbolt Compiler Explorer

(注:GCC使用SAL记忆在它的输出,而不是SHL助记符这些相同指令,区别仅事项右移和所有健全的组装程序员使用SHL。我不知道为什么海湾合作委员会发出SAL,但你可以把它精神上转换成SHL。)

+0

为什么ICC轮换SHLD:愤世嫉俗的原因是它在AMD上的速度较慢(6 uops,3c延迟,在Bulldozer/Ryzen上),但与Nehalem之后的Intel的ROR成本相同。不那么愤世嫉俗的是,它并没有太多的认识旋转习惯用法,而是在移位计数正确互补的情况下,用SHLD实现移位和和运算。 –

+0

虽然可以识别旋转并使用BMI2 RORX。 https://godbolt.org/g/GL5YQw。 (另见https://stackoverflow.com/questions/776508/best-practices-for-circular-shift-rotate-operations-in-c为安全无UB变量计数旋转,这也使得你真的点想要命名你的函数'ror32',而不是通过'long'的宽度来旋转,如果你想要创建一个便携函数!) –

+0

你的64位原生分割例子很奇怪/错误。C编译器将使用64位操作数大小来处理'idiv'以及这些转换。所以它会将'movsx rcx,b' /'movsx rax,a' /'shl rax,16' /'cqo'(不是cdq)/'idiv rcx'(https://godbolt.org/g/9Gf9Z3),因为输入还没有被扩展到64位(假设你的输入是'int32_t',比如Windows上的'long'或者一般的32位)。在复制到已知寄存器后显示'shl a,16'而不是移动是奇怪的。 –