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

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

有没有人能提供大约specific examples种使用现代编译器汇编比编写良好的C代码更快的情况,你能用剖析证据来支持这一说法吗?我很有信心这些案件是存在的,但我真的很想知道这些案件究竟有多深奥,因为这似乎是一个有争议的问题.

推荐答案

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

这些不仅在没有浮点的设备上很方便,在精度方面也很有用,因为它们可以提供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?的代码.

C++相关问答推荐

C中char数组指针的问题

理解C中的指针定义

与unions 的未定义行为

如何正确地索引C中的 struct 指针数组?

C语言中字符数组声明中的标准

如何将已分配的数组(运行时已知的大小)放入 struct 中?

cairo 剪辑区域是否存在多个矩形?

将变量或参数打包到 struct /联合中是否会带来意想不到的性能损失?

通过k&;r语法的c声明无效

在下面的C程序中,.Ap0是如何解释的?

OMP并行嵌套循环

为什么GCC 13没有显示正确的二进制表示法?

如果类型是新的,offsetof是否与typeof一起工作?

解密Chrome加密密钥

将char*数组深度复制到 struct 中?

在C中交换字符串和数组的通用交换函数

Struct 内的数组赋值

System V 消息队列由于某种原因定期重置

添加/删除链表中的第一个元素

创建 makefile 来编译位于不同目录中的多个源文件