什么时候汇编比C快?[闭门]

关闭。这个问题需要更加关注。它目前不接受答案。

五个月前关闭的

已锁定。这个问题及其答案被锁定,因为这个问题离题,但具有历史意义。它目前不接受新的答案或互动。

了解汇编语言的一个原因是,有时,它可以用来编写比用高级语言(特别是C语言)编写代码性能更好的代码。然而,我也多次听到它说,虽然这并不完全是错误的,但是汇编程序可以用来生成更高性能代码的情况非常罕见,需要汇编方面的专家知识和经验

这个问题甚至没有考虑到汇编指令是特定于机器的、不可移植的,或者汇编程序的任何其他方面。当然,除了这一点之外,了解汇编还有很多很好的理由,但这是一个需要示例和数据的特定问题,而不是关于汇编语言与高级语言的扩展论述

有人能提供一些具体的例子吗?在这些例子中,汇编比使用现代编译器编写的C代码要快,你能用分析证据来支持这一说法吗?我很有信心这些案例是存在的,但我真的很想知道这些案例到底有多深奥,因为这似乎是一个争论点

下面是一个真实的例子:旧编译器上的定点乘法

这些不仅在没有浮点运算的设备上很方便,在精度方面也很有用,因为它们可以提供32位精度,并且有一个可预测的错误(浮点运算只有23位,很难预测精度损失)。i、 e.整个范围内的一致绝对精度,而不是接近一致相对精度(浮点


现代编译器很好地优化了这个定点示例,因此对于仍然需要编译器特定代码的更现代的示例,请参阅

  • 获取64位整数乘法的高阶部分:对于32×32=>64位乘法,使用uint64\u t的便携式版本无法在64位CPU上进行优化,因此需要使用内部函数或\u int128,才能在64位系统上实现高效的代码
  • _Windows 32位上的umul128:MSVC在将32位整数转换为64位时并不总是做得很好,因此内部函数帮助很大

C没有完全乘法运算符(N位输入的2N位结果)。用C表示它的通常方法是将输入转换为更广泛的类型,并希望编译器认识到输入的高位不有趣:

//在32位机器上,int可以保存32位定点整数。
int inline FixedPointMul(int a,int b)
{
long long a_long=a;//转换为64位。
长积=a_long*b;//执行乘法
返回值(int)(乘积>>16);//定点偏移
}

这段代码的问题是我们做了一些不能用C语言直接表达的事情。我们想将两个32位的数字相乘,得到一个64位的结果,返回中间的32位。然而,在C语言中,这种乘法并不存在。您所能做的就是将整数提升到64位并执行64*64=64乘法

然而,x86(以及ARM、MIPS和其他)可以在一条指令中完成乘法运算。一些编译器过去常常忽略这一事实,生成调用运行时库函数进行乘法的代码。移位16通常也由库例程完成(x86也可以执行这种移位)

所以我们只剩下一两个库调用来进行乘法运算。这将产生严重后果。不仅移位较慢,还必须在函数调用中保留寄存器,这也无助于内联和代码展开

如果您在(内联)汇编程序中重写相同的代码,您可以获得显著的速度提升

除此之外:使用ASM不是解决问题的最佳方法。如果不能用C语言表达某些汇编指令,大多数编译器允许您使用内在形式的汇编指令。例如,VS.NET2008编译器将32*32=64位mul公开为u_emul,将64位移位公开为u ll_rshift

使用内部函数,您可以重写函数,使C编译器有机会了解发生了什么。这允许代码内联、寄存器分配、公共子表达式消除以及常量传播。这样,与手工编写的汇编代码相比,您将获得巨大的性能改进

供参考:VS.NET编译器定点mul的最终结果为:

int inline FixedPointMul(int a,int b)
{
返回(int)uu ll_ur换档(uuu emul(a,b),16);
}

定点除法的性能差异更大。通过编写两行asm代码,对于除法密集的定点代码,我的改进达到了因子10


使用Visual C++ 2013给出了两种方法的相同汇编代码。

2007年发布的gcc4.1也很好地优化了纯C版本。(Godbolt编译器资源管理器没有安装任何早期版本的gcc,但可能即使是较旧的gcc版本也可以在没有内部函数的情况下安装。)

请参阅Godbolt编译器资源管理器上x86(32位)和ARM的source+asm。(不幸的是,它没有任何足够旧的编译器,无法从简单的纯C版本生成糟糕的代码。)


现代CPU可以做C根本没有运算符的事情,比如popcnt或通过位扫描查找第一个或最后一个设置的位。(POSIX有一个ffs()函数,但其语义与x86bsf/bsr不匹配。请参阅https://en.wikipedia.org/wiki/Find_first_set)

有些编译器有时可以识别一个循环,该循环计算整数中的设置位数,并将其编译为popcnt指令(如果在编译时启用),但在GNU C中使用\u builtin\u popcnt更可靠,或者在x86上,如果您只针对具有SSE4.2的硬件:\u mm\u popcnt\u u32from<immintrin.h&gt

或C++中,分配给 STD::32&gt并使用.count()。(在这种情况下,该语言找到了一种通过标准库可移植地公开popcount优化实现的方法,这种方法将始终编译为正确的内容,并且可以利用目标支持的任何内容。)另请参阅https://en.wikipedia.org/wiki/Hamming_weight#Language_support.

类似地,ntohl可以在一些具有它的C实现上编译为bswap(x86 32位字节交换,用于endian转换)


Intrinsic或手写asm的另一个主要领域是使用SIMD指令进行手动矢量化。编译器具有简单的循环,比如dst[i]+=src[i]*10.0,但当事情变得更复杂时,通常表现不佳或根本不自动矢量化。例如,您不太可能了解如何使用SIMD实现atoi?由编译器根据标量代码自动生成

发表评论