了解汇编程序的理由之一是,有时可以使用它来编写比用更高级语言编写代码更具有性能的代码,特别是C语言。然而,我也多次听到它指出,虽然这并不完全是错误的,但是汇编程序可以使用实际使用来生成更高性能的代码的情况是非常罕见的,并且需要具有组装方面的专家知识和经验。
这个问题甚至没有进入汇编程序指令将是特定于机器和不可移植的事实,或汇编程序的任何其他方面。当然,除了这个程序集之外,还有很多很好的理由来了解程序集,但这是一个具体的问题,它涉及示例和数据,而不是关于汇编语言和高级语言的详细讨论。
有谁能提供一些特定的例子--使用现代编译器汇编比编写良好的C代码更快的情况--,您能用分析证据来支持这种说法吗?我很有信心这些案例的存在,但我真的很想知道这些案例到底有多深奥,因为这似乎是一个争论的焦点。
发布于 2009-02-23 14:48:58
下面是一个真实的例子:旧式编译器上的定点乘数。
这些不仅适用于没有浮点的设备,而且当涉及到精度时,它们会发光,因为它们提供了32位精度和可预测的误差(浮点数只有23位,很难预测精度损失)。即在整个距离内的均匀绝对精度,而不是接近均匀的相对精度(float).
现代编译器很好地优化了这个定点示例,因此,关于仍然需要编译器特定代码的更多现代示例,请参见
uint64_t进行32x32 => 64位乘法器的可移植版本无法在64位CPU上进行优化,因此您需要在64位系统上进行有效代码的内部处理或__int128。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编译器有机会了解发生了什么。这允许代码内联、寄存器分配、公共子表达式消除和常量传播也可以完成。与手工编写的汇编程序代码相比,您将获得极大的性能改进。
供参考:用于VS.NET编译器的定点mul的最终结果是:
int inline FixedPointMul (int a, int b)
{
return (int) __ll_rshift(__emul(a,b),16);
}不动点划分的性能差异更大。通过编写几行asm代码,我对除法重定点代码的因子10进行了改进。
使用VisualC++ 2013会为这两种方式提供相同的组装代码。
2007年的gcc4.1也很好地优化了纯C版本。(戈德波特编译器浏览器没有安装gcc的早期版本,但可以推测,即使是更老的GCC版本也可以做到这一点。)
有关x86 (32位)和ARM on 戈德波特编译器浏览器,请参见源代码+ asm .(不幸的是,它没有足够老的编译器从简单的纯C版本中生成错误的代码。)
现代CPU可以做一些事情,C在所有的popcnt 中都没有的操作符,比如或位扫描来查找第一个或最后一个设置的位。(POSIX有一个ffs()函数,但它的语义与x86 bsf / bsr不匹配。见设置)。
有些编译器有时可以识别一个循环,该循环计数整数中的set位数,并将其编译为popcnt指令(如果在编译时启用),但在GNU中使用__builtin_popcnt更可靠,如果只针对SSE4.2:的硬件,则在x86上使用该循环要可靠得多。
或者在C++中,分配给std::bitset<32>并使用.count()。(在这种情况下,语言找到了一种方法,通过标准库可移植地公开popcount的优化实现,这种方式总是能够编译到正确的地方,并且可以利用目标支持的任何东西。)另见支持。
类似地,ntohl可以在一些C实现上编译为bswap (用于endian转换的x86 32位字节交换)。
本质或手写asm的另一个主要领域是使用SIMD指令的手工矢量化.对于像dst[i] += src[i] * 10.0;这样的简单循环,编译器并不坏,但是当事情变得更加复杂时,编译器往往做得不好,或者根本不自动矢量化。例如,您不太可能从标量代码中获得编译器自动生成的任何类似如何用SIMD实现atoi?。
发布于 2009-02-23 13:44:42
很多年前,我教别人在C中编程,练习是通过90度旋转一个图形。他带着一个需要几分钟才能完成的解决方案回来,主要是因为他使用的是乘法和除法等。
我向他展示了如何用位移位来重铸这个问题,在他所拥有的非优化编译器上,处理的时间缩短到大约30秒。
我刚刚得到了一个优化的编译器,相同的代码在<5秒内旋转了图形。我看了编译器正在生成的汇编代码,根据我看到的结果,然后我编写汇编程序的日子结束了。
发布于 2009-02-23 13:17:06
在不提供任何特定示例或分析器证据的情况下,当您知道比编译器更多的信息时,您可以编写比编译器更好的汇编程序。
在一般情况下,现代C编译器对如何优化所讨论的代码有更多的了解:它知道处理器流水线是如何工作的,它可以尝试以比人类更快的速度重新排序指令,等等--它基本上和计算机一样好,或者说比最好的人类玩家更好,只是因为它可以比大多数人更快地在问题空间中进行搜索。虽然理论上您可以在特定情况下与计算机一样执行任务,但是您肯定不能以相同的速度执行,这使得它在许多情况下是不可行的(也就是说,如果您试图在汇编程序中编写多个例程,那么编译器的性能肯定会优于您)。
另一方面,在某些情况下,编译器没有那么多的信息--我想说的是,主要是在使用不同形式的外部硬件时,编译器并不了解这些信息。最主要的例子可能是设备驱动程序,在这种情况下,汇编程序结合人们对硬件的熟悉可以产生比C编译器更好的结果。
其他人提到了特殊用途说明,这就是我在上面一段中所说的--编译器可能对这些指令了解有限或根本不了解,从而使人类编写更快的代码成为可能。
https://stackoverflow.com/questions/577554
复制相似问题