这是一个现实世界的示例:旧编译器上的定点乘法。
这些不仅在没有浮点的设备上很方便,而且在精度方面也很出色,因为它们为您提供 32 位精度和可预测的误差(浮点数只有 23 位,并且更难以预测精度损失)。即制服absolute整个范围内的精度,而不是接近均匀relative精确 (float
).
现代编译器很好地优化了这个定点示例,因此对于仍然需要特定于编译器的代码的更现代示例,请参阅
-
获取 64 位整数乘法的高位部分 https://stackoverflow.com/questions/28868367/getting-the-high-part-of-64-bit-integer-multiplication/50958815#50958815:便携式版本使用
uint64_t
对于 32x32 => 64 位乘法无法在 64 位 CPU 上进行优化,因此您需要内在函数或__int128
在 64 位系统上实现高效代码。
-
Windows 32 位上的 _umul128 https://stackoverflow.com/questions/46870373/umul128-on-windows-32-bits:在将 32 位整数乘以 64 时,MSVC 并不总是做得很好,因此内在函数有很大帮助。
C 没有全乘运算符(N 位输入的 2N 位结果)。用 C 语言表达它的通常方法是将输入转换为更宽的类型,并希望编译器认识到输入的高位不重要:
// on a 32-bit machine, int can hold 32-bit fixed-point integers.
int inline FixedPointMul (int a, int b)
{
long long a_long = a; // cast to 64 bit.
long long product = a_long * b; // perform multiplication
return (int) (product >> 16); // shift by the fixed point bias
}
这段代码的问题在于我们做了一些无法直接用 C 语言表达的事情。我们想要将两个 32 位数字相乘并得到 64 位结果,我们返回其中的中间 32 位。然而,在 C 语言中,这种乘法并不存在。您所能做的就是将整数提升为 64 位并进行 64*64 = 64 乘法。
然而,x86(以及 ARM、MIPS 等)可以在单个指令中执行乘法。一些编译器过去常常忽略这一事实,并生成调用运行时库函数来执行乘法的代码。移位 16 也通常由库例程完成(x86 也可以进行此类移位)。
因此,我们只剩下一两个库调用来进行乘法运算。这会产生严重的后果。不仅移位速度较慢,而且必须在函数调用之间保留寄存器,而且它也无助于内联和代码展开。
如果您在(内联)汇编器中重写相同的代码,您可以获得显着的速度提升。
除此之外:使用ASM并不是解决问题的最好方法。大多数编译器允许您以内在形式使用一些汇编指令(如果您无法用 C 语言表达它们)。例如,VS.NET2008 编译器将 32*32=64 位 mul 公开为 __emul,将 64 位移位公开为 __ll_rshift。
使用内在函数,您可以以 C 编译器有机会了解发生了什么的方式重写函数。这允许内联代码、分配寄存器、消除公共子表达式和常量传播。你会得到一个huge通过这种方式,可以提高手写汇编代码的性能。
供参考:VS.NET 编译器的定点 mul 的最终结果是:
int inline FixedPointMul (int a, int b)
{
return (int) __ll_rshift(__emul(a,b),16);
}
定点除法的性能差异甚至更大。通过编写几行汇编代码,我将重除法的定点代码改进了 10 倍。
使用 Visual C++ 2013 为两种方式提供相同的汇编代码。
2007年的gcc4.1也很好地优化了纯C版本。 (Godbolt 编译器资源管理器没有安装任何早期版本的 gcc,但想必即使是较旧的 GCC 版本也可以在没有内在函数的情况下执行此操作。)
请参阅 x86(32 位)和 ARM 的源代码 + asmGodbolt 编译器浏览器 https://godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(j:1,lang:c%2B%2B,source:'//+compiled+with+gcc+-mregparm+so+args+are+in+regs,+like+when+inlining%0A//+Remove+-m32+to+see+x86-64+code-gen%0A%0A//+static+inline%0Aint+FixedPointMul+(int+a,+int+b)+%7B%0A++long+long+a_long+%3D+a%3B+//+cast+to+64+bit.%0A++long+long+product+%3D+a_long+*+b%3B+//+perform+multiplication%0A++return+(int)+(product+%3E%3E+16)%3B++//+shift+by+the+fixed+point+bias%0A%7D%0A%0A//+Modern+compilers+know+that+32-bit+integers+cast+to+64%0A//+still+only+have+32+significant+bits,%0A//+so+one+32-bit+signed+multiply+is+sufficient%0A%0A%23ifdef+_MSC_VER%0A%23include+%3Cintrin.h%3E%0A//+static+inline%0Aint+FixedPointMul_msvc+(int+a,+int+b)+%7B%0A++++return+(int)+__ll_rshift(__emul(a,b),16)%3B%0A%7D%0A%23endif%0A%0A%0A/*+Intrinsics+are+more+useful+for+extended+precision%0A+*+when+there+isn!'t+a+wide-enough+type.%0A+*+e.g.+128-bit+integer+on+compilers+without+__int128%0A+*/%0A'),l:'5',n:'0',o:'C%2B%2B+source+%231',t:'0')),k:32.75251522372254,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((g:!((h:compiler,i:(compiler:g412,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'1',trim:'1'),lang:c%2B%2B,libs:!(),options:'-xc+-O3+-m32++-fomit-frame-pointer',source:1),l:'5',n:'0',o:'x86-64+gcc+4.1.2+(Editor+%231,+Compiler+%231)+C%2B%2B',t:'0')),k:34.10775747948107,l:'4',m:50,n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:arm710,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'1'),lang:c%2B%2B,libs:!(),options:'-xc+-O3+-mthumb+-mcpu%3Dcortex-m4',source:1),l:'5',n:'0',o:'ARM+gcc+7.2.1+(none)+(Editor+%231,+Compiler+%232)+C%2B%2B',t:'0')),header:(),l:'4',m:50,n:'0',o:'',s:0,t:'0')),k:33.91415144294414,l:'3',n:'0',o:'',t:'0'),(g:!((g:!((h:compiler,i:(compiler:clang30,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'1'),lang:c%2B%2B,libs:!(),options:'-xc+-O3+-m32',source:1),l:'5',n:'0',o:'x86-64+clang+3.0.0+(Editor+%231,+Compiler+%233)+C%2B%2B',t:'0')),k:33.33333333333333,l:'4',m:50,n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:cl19_2015_u3_32,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'1'),lang:c%2B%2B,libs:!(),options:'-Ox',source:1),l:'5',n:'0',o:'x86+MSVC+19+2015+U3+(Editor+%231,+Compiler+%234)+C%2B%2B',t:'0')),header:(),l:'4',m:50,n:'0',o:'',s:0,t:'0')),k:33.33333333333333,l:'3',n:'0',o:'',t:'0')),l:'2',n:'0',o:'',t:'0')),version:4。 (不幸的是,它没有足够老的编译器来从简单的纯 C 版本生成错误的代码。)
现代 CPU 可以做 C 语言没有运算符可以完成的事情at all, like popcnt
或位扫描以查找第一个或最后一个设置位。 (POSIX 有一个ffs()
函数,但其语义与 x86 不匹配bsf
/ bsr
. See https://en.wikipedia.org/wiki/Find_first_set https://en.wikipedia.org/wiki/Find_first_set).
某些编译器有时可以识别一个循环,该循环计算整数中设置的位数并将其编译为popcnt
指令(如果在编译时启用),但使用起来更可靠__builtin_popcnt
在 GNU C 中,或者在 x86 上(如果您仅针对具有 SSE4.2 的硬件):.
或者在 C++ 中,分配给std::bitset<32>
并使用.count()
。 (在这种情况下,该语言找到了一种通过标准库可移植地公开 popcount 的优化实现的方法,这种方式始终会编译为正确的内容,并且可以利用目标支持的任何内容。)另请参阅https://en.wikipedia.org/wiki/Hamming_weight#Language_support https://en.wikipedia.org/wiki/Hamming_weight#Language_support.
相似地,ntohl
可以编译为bswap
(用于字节序转换的 x86 32 位字节交换)在某些具有它的 C 实现上。
内在函数或手写汇编的另一个主要领域是使用 SIMD 指令进行手动矢量化。编译器对于简单的循环来说还不错,比如dst[i] += src[i] * 10.0;
,但当事情变得更复杂时,通常会做得很糟糕或者根本不自动矢量化。例如,你不太可能得到类似的东西如何使用SIMD实现atoi? https://stackoverflow.com/questions/35127060/how-to-implement-atoi-using-simd由编译器根据标量代码自动生成。