下面是一个真实的例子:旧编译器上的定点乘法.
这些不仅在没有浮点的设备上很方便,在精度方面也很有用,因为它们可以提供32位精度,并带有可预测的错误(浮点只有23位,精度损失更难预测).i、 e.整个范围内的统一absolute精度,而不是接近统一relative精度(float
).
现代编译器很好地优化了这个定点示例,因此,对于仍然需要编译器特定代码的更现代示例,请参阅
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等)可以在一条指令中完成乘法运算.一些编译器过go 常常忽略这一事实,生成调用运行时库函数进行乘法的代码.16的移位通常也由库 routine 完成(x86也可以进行这种移位).
所以我们只剩下一两个库调用来进行乘法运算.这会造成严重后果.不仅移位速度较慢,还必须在函数调用中保留寄存器,这也无助于内联和代码展开.
如果在(内联)汇编程序中重写相同的代码,可以显著提高速度.
除此之外:使用ASM并不是解决问题的最佳方法.如果不能用C语言表达某些汇编指令,大多数编译器都允许您以内在形式使用它们.例如,VS.NET2008编译器将32*32=64位mul作为_emul公开,将64位移位作为_ll_rshift公开.
使用intrinsic,您可以以C编译器有机会了解情况的方式重写函数.这允许代码内联、寄存器分配、公共子表达式消除和常量传播也可以完成.这样,与手工编写的汇编代码相比,您的性能将提高huge%.
供参考:VS.NET编译器的定点MUL的最终结果为:
int inline FixedPointMul (int a, int b)
{
return (int) __ll_rshift(__emul(a,b),16);
}
定点除法的性能差异更大.通过编写几个ASM行,我对除法繁重的定点代码进行了高达10倍的改进.
使用Visual C++ 2013给出两种方式相同的汇编代码.
gcc4.2007年发布的1也很好地优化了纯C版本.(Godbolt compiler explorer没有安装任何早期版本的gcc,但可能即使是较旧的gcc版本也可以在没有内部函数的情况下安装.)
See source + asm for x86 (32-bit) and ARM on the Godbolt compiler explorer. (Unfortunately it doesn't have any compilers old enough to produce bad code from the simple pure C version.)
Modern CPUs can do things C doesn't have operators for at all, like 101 or bit-scan to find the first or last set bit.(POSIX有一个ffs()
函数,但它的语义与x86 bsf
/bsr
不匹配.参见https://en.wikipedia.org/wiki/Find_first_set).
有些编译器有时可以识别一个循环,该循环计算整数中的设置位数,并将其编译为popcnt
指令(如果在编译时启用),但在GNU C中使用__builtin_popcnt
更可靠,或者在x86上使用__builtin_popcnt
更可靠,如果您只针对具有SSE4的硬件.2: _mm_popcnt_u32
from <immintrin.h>
.
或者在C++中,赋值为std::bitset<32>
并使用.count()
.(这种情况下,该语言找到了一种方法,可以通过标准库可移植地公开popcount的优化实现,这种方式将始终编译为正确的内容,并且可以利用目标支持的任何东西.)另请参见https://en.wikipedia.org/wiki/Hamming_weight#Language_support.
类似地,在一些具有ntohl
的C实现上,ntohl
可以编译为bswap
(用于字节序转换的x86 32位字节交换).
Intrinsic或手写asm的另一个主要领域是使用SIMD指令进行手动矢量化.对于dst[i] += src[i] * 10.0;
这样的简单循环,编译器并不差,但当事情变得更复杂时,编译器通常表现不好,或者根本不自动矢量化.例如,编译器不太可能从标量代码自动生成类似How to implement atoi using SIMD?的代码.