MSVC 内联 ASM 到 GCC
MSVC Inline ASM to 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 << 13) | (val >> (32 - 13))
,但代码不会产生相同的输出。
将此内联 ASM 转换为 GCC 的正确方法是什么and/or此代码的 C 翻译是什么?
GCC uses a completely different syntax for inline assembly than MSVC does, so it's quite a bit of work to maintain both forms. It's not an especially good idea, either. There are many problems with inline assembly. People often use it because they think it'll make their code run faster, but it usually has quite the opposite effect. .
当你尝试这样做时,你必须要小心一点,不过:带符号的右移是在 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 => 32b idiv
instruction 不会出错。 (如果商不适合 eax
,它会引发 #DE
异常,而不仅仅是截断)
换句话说,转换:
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
的某些组合进行错误将违反假设规则,除非编译器可以证明 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
。)
我正在尝试处理 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 的正确方法是什么and/or此代码的 C 翻译是什么?
GCC uses a completely different syntax for inline assembly than MSVC does, so it's quite a bit of work to maintain both forms. It's not an especially good idea, either. There are many problems with inline assembly. People often use it because they think it'll make their code run faster, but it usually has quite the opposite effect.
当你尝试这样做时,你必须要小心一点,不过:带符号的右移是在 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 => 32b idiv
instruction 不会出错。 (如果商不适合 eax
,它会引发 #DE
异常,而不仅仅是截断)
换句话说,转换:
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
的某些组合进行错误将违反假设规则,除非编译器可以证明 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
。)