GCC 使用完全不同的语法进行内联汇编 https://gcc.gnu.org/onlinedocs/gcc/Using-Assembly-Language-with-C.html#Using-Assembly-Language-with-C与 MSVC 相比,因此维护这两种形式需要相当多的工作。这也不是一个特别好的主意。内联汇编存在很多问题 https://gcc.gnu.org/wiki/DontUseInlineAsm。人们经常使用它,因为他们认为这会让他们的代码运行得更快,但它通常会产生完全相反的效果。除非您是两种汇编语言的专家and编译器的代码生成策略,让编译器的优化器生成代码会更好 https://stackoverflow.com/questions/43883473/working-inline-assembly-in-c-for-bit-parity/43929095#43929095.
不过,当您尝试这样做时,您必须要小心一点:有符号右移是在 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));
}
(也可以看看C++ 循环移位(旋转)操作的最佳实践 https://stackoverflow.com/questions/776508/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 处理器上的旋转速度更快,或者可能在 Intel 上是相同的但在 AMD 上速度较慢。)
实施Div16
在纯 C 中,你想要:
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 => 32bidiv操作说明 http://felixcloutier.com/x86/IDIV.html不会出错。 (这将引发#DE
如果商不适合则例外eax
,而不仅仅是截断)
换句话说,转变:
int32_t Divide(int64_t a, int32_t b)
{
return (a / b);
}
into:
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
and b
将违反假设规则,除非编译器可以证明这些组合a
and b
是不可能的。 (例如,如果b
已知大于1<<16
,这可能是一个合法的优化a = (int32_t)input; a <<= 16;
但即使这会对所有输入、gcc 和 clang 产生与 C 抽象机相同的行为
目前不进行该优化。)
根本没有一个好方法来覆盖语言标准强加的规则并强制编译器发出所需的目标代码。 MSVC 没有为其提供内在函数(尽管有一个 Windows API 函数,MulDiv
,它的速度并不快,只是使用内联汇编来实现它自己 - 并且某种情况下的错误 https://blogs.msdn.microsoft.com/oldnewthing/20120514-00/?p=7633/,现在由于向后兼容性的需要而得到巩固)。您基本上别无选择,只能求助于汇编,无论是内联还是从外部模块链接。
于是,你就陷入了丑陋之中。它看起来像这样:
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
,因为这将在所有编译器上提供同样好的目标代码。
Godbolt 编译器资源管理器的现场演示 https://gcc.godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAKxAEZSBnVAV2OUxAHIG9gA7TOgDUAG1R9gQgCJ4AbrQBsEHv0GjxkgIakhKgcLEShAIwCUAUgAMAQXMB2AEJXbAJgDMeAGZZPQgPp%2BAOIAcgCqAMIBzkIxurz66kYAjsyoBHiYfATmbk42sXGqBhpCxJgAtpp4fFjEOQ4xAPSNQhDMfMwMgjrGzARCAuwMDJrEAJ5CRIV8miJCyGLGxpjE1cAW%2BbEBmgzlARDmLi54%2BLJzQocArOaXDqd4THWXUocu0QUfQiAXRzlSmq9Wik0hksqZSO9Ph9Xn90ICIGVKtVahtrFCCt90TFXpZAa02nw9GpDOtNBc3OEcuEhIpwZCsQyfi5aHiIGSctgOTSFHSbIcPN5ML5ggB5PwAWWw4r84RFwQAygAVABK1gAksFFfSsTc7nIHiQblImXUjjEIGZnPzMCIutr0br7o8jSbyvDLXz3Jl8J57R9vn6ChZcvSygRWHwhMD0plsiHPW4bV5/OL5ZEAGrYZX07a7en2PJoqHlVCyGKYTQADx0UgA6iLlcaAAoqi63AHPQMlstCQTV3tVwMMBDnCv9xRD0bl9DjhSBk5yaT1xtCFvKtsOYxGwPNUQV2SYUqYBjMET9apCbDWAAaOgA7ggVofCEI73gRHNlkfw8R9Pm7C8CY2namzYl6xDECQTKhISzAAA5wSQBBqGg5Rwe%2BKwwkBNReFaAHOJw4IiFwlycKQfBcJYZGoFwlIuE49G6CwbCHvytBkQQlGEeCADWIBuJYAB0AAclx2C4wkAJwuHYYmXG40mkMRnAACxkRRnBUaQNGcGRDAgJYpCcZphGkHAsAwIgKCoGhGFkBQECoehIgrCgIi0JJfhuC4pCeO%2ByHEPpEABH4liUOCxhcaQqHlLGIp8CIYxRfgZTIOkB76SZ5BZDaUVUCkKxJZwbiCS4gnsUpmTAAQSD0HBZSyCKxhUJgaWUCFYUQERVU1XQpD1ZgjXNa1nHBQEnVUAAiswhXebQli0LQdiSW4dgKBJwl2MJEmWJczKUPgDBwSImhFdxlUSL1tCmDd2lwek4iZY0IouEIjS1rMIh6cx7B0ERJHqVFOmVsJChCOEAAyNKSUILgLXYQjKoqtatLghBQWxOjhDZzkrD8bhuKYHFcbdfHeaVbgKPJCiSdtlyWCpLgqUpXBqaQ5T8SVEm0MJq3yZTlw0wo5FA1wekGUZJNmZZEBIE5dnkJQ8sucQIDAAobi%2Bf5KxBZFWUxXFCVFVpKUjXIx5RdUyHKVR4KoPdeCPVwAC0L1CM7H0fu75Ted9rC/ddLOcKRItZcDoPOwoKlCMAyDIEICiCW4qP4EQxD4/Q4M43ZGdE5LJm3Y%2Bmi1OFQdsxzMmCZcHkKJYUkqWJKmWEcdih1pOni4Zxm29L8Cy9Ztkq4rjnZyrIB4HHS1a2eOuUHrWkG1k8WJcleCpelFtZVbuUF3dD2Ei7bse593u%2B4wP0cIHykhxp7dcCDCiR9HE/x0tKfo%2BnmNZ4PeP8i4efd3OkXEuXUy5kQ5gpcqtNLjyT2pYWuEkfK32omLRgEtAGk34sJcqdgGZRwJipNw1c1pBzcIDMOqCMG9wsn3OWo8VjD2Vq5BYmgJBN3oH5GegU55RUXgQZexsyKmzSubTKWlt421MvbfemVXavWPl7Z2PsXB%2BxYn9ION9RacAfk/eYJ0jAqUEkJWg7804ZyxvQz%2B7gVIAJJuCYBrlQFkyEpJcSTMNZSXrm4RQYD2YgAboJGmdhvIrTWqJFSwtkHaVQfpLuUtzJWSYfZJWli3KsOAOw6eAVda8JsrFJeRtV7r1EZbHKki7YOydpwOR7tPZzCUd5d2UhRQSilDKOUSpVQakVKogO/1g7kLvtoiOUc9HpKEIY4xpiMbuEztjH%2BViiG2ILvYisID%2BnlxAJcQxAkXAKFwetbxDdmZRI7mguJKzSDOMEq4o4ES3CeMkoQnxykyFtxQbpfOtt%2BkqPedEz5GDwQHkCo7CiKkgA
(注:GCC 使用SAL
其输出中的助记符,而不是SHL
助记符。这些都是完全相同的指令——差异只对右移有意义——所有理智的汇编程序员都使用SHL
。我不知道为什么 GCC 会发出SAL
,但你可以在心里把它转换成SHL
.)