了解汇编程序的原因之一是,有时可以使用汇编程序来编写比用高级语言(特别是C语言)编写的代码性能更好的代码。然而,我也听人说过很多次,尽管这并非完全错误,但实际上可以使用汇编程序来生成性能更好的代码的情况极其罕见,并且需要汇编方面的专业知识和经验。

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

谁能提供一些具体的例子,说明使用现代编译器汇编代码比编写良好的C代码更快,并且您能否用分析证据支持这一说法?我相信这些案例确实存在,但我真的很想知道这些案例到底有多深奥,因为这似乎是一个有争议的问题。


当前回答

下面是一个真实的例子:固定点在旧编译器上进行乘法运算。

这些不仅在没有浮点数的设备上很方便,在精度方面也很出色,因为它们可以提供32位精度和可预测的错误(浮点数只有23位,很难预测精度损失)。即在整个范围内均匀的绝对精度,而不是接近均匀的相对精度(浮点数)。


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

获得64位整数乘法的高部分:使用uint64_t for 32x32 => 64位乘法的便携版本在64位CPU上无法优化,因此你需要intrinsic或__int128来在64位系统上实现高效的代码。 Windows 32位上的_umul128: MSVC在将32位整数转换为64时并不总是做得很好,因此intrinsic有很大帮助。


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。

使用intrinsic,你可以以一种c编译器有机会理解发生了什么的方式重写函数。这允许代码内联,寄存器分配,公共子表达式消除和常量传播也可以完成。与手工编写的汇编程序代码相比,您将获得巨大的性能改进。

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

int inline FixedPointMul (int a, int b)
{
    return (int) __ll_rshift(__emul(a,b),16);
}

定点除法的性能差异更大。通过编写几行asm代码,我对除法重的定点代码进行了10倍的改进。


使用Visual c++ 2013为这两种方式提供了相同的汇编代码。

2007年的gcc4.1也很好地优化了纯C版本。(Godbolt编译器资源管理器没有安装任何早期版本的gcc,但即使是较旧的gcc版本也可以在没有intrinsic的情况下做到这一点。)

在Godbolt编译器资源管理器上查看用于x86(32位)和ARM的source + asm。(不幸的是,它没有任何旧到足以从简单的纯C版本生成糟糕代码的编译器。)


现代cpu可以做一些C语言根本没有操作符的事情,比如popcnt或位扫描来查找第一个或最后一个设置位。POSIX有一个ffs()函数,但是它的语义不匹配x86 bsf / bsr。见https://en.wikipedia.org/wiki/Find_first_set)。

一些编译器有时可以识别一个计数整数中设置位数的循环,并将其编译为popcnt指令(如果在编译时启用),但在GNU C中使用__builtin_popcnt要可靠得多,或者在x86上(如果你的目标硬件是SSE4.2: _mm_popcnt_u32 from < immintrinh >)。

或者在c++中,赋值给std::bitset<32>并使用.count()。(在这种情况下,该语言已经找到了一种方法,可以通过标准库可移植地公开popcount的优化实现,以一种总是编译为正确的方式,并且可以利用目标支持的任何东西。)参见https://en.wikipedia.org/wiki/Hamming_weight#Language_support。

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


intrinsic或手写asm的另一个主要领域是使用SIMD指令进行手工向量化。编译器对于dst[i] += src[i] * 10.0;这样的简单循环并不糟糕,但是当事情变得更复杂时,编译器通常做得很糟糕,或者根本不自动向量化。例如,你不太可能得到任何像如何实现atoi使用SIMD?由编译器从标量代码自动生成。

其他回答

我曾经和一个人一起工作过,他说“如果编译器笨到不能弄清楚你要做什么,并且不能优化它,那么你的编译器就坏了,是时候换一个新的了”。我确信在某些情况下汇编程序会打败你的C代码,但是如果你发现自己经常使用汇编程序来“赢得”编译器,那么你的编译器就完蛋了。

对于编写试图强制查询计划器执行操作的“优化”SQL也是如此。如果您发现自己重新安排查询以让计划器执行您想要的操作,那么您的查询计划器就完蛋了——请更换一个新的计划器。

一个更著名的组装片段来自Michael Abrash的纹理映射循环(在这里详细解释):

add edx,[DeltaVFrac] ; add in dVFrac
sbb ebp,ebp ; store carry
mov [edi],al ; write pixel n
mov al,[esi] ; fetch pixel n+1
add ecx,ebx ; add in dUFrac
adc esi,[4*ebp + UVStepVCarry]; add in steps

现在,大多数编译器将高级CPU特定指令表示为intrinsic,即编译为实际指令的函数。MS Visual c++支持MMX、SSE、SSE2、SSE3和SSE4的intrinsic,因此您不必太过担心使用特定于平台的指令来进行汇编。Visual c++还可以通过适当的/ARCH设置来利用您所针对的实际体系结构。

使用SIMD指令的矩阵操作可能比编译器生成的代码更快。

我想说的是,当你比编译器更擅长一组给定的指令时。所以我认为没有通用的答案

第一点不是答案。 即使你从来没有用它编程,我发现至少知道一个汇编指令集是有用的。这是程序员永无止境的追求的一部分,他们想知道得更多,从而变得更好。当你进入一个没有源代码的框架时,它也很有用,至少对正在发生的事情有一个粗略的了解。它还可以帮助您理解JavaByteCode和. net IL,因为它们都类似于汇编程序。

To answer the question when you have a small amount of code or a large amount of time. Most useful for use in embedded chips, where low chip complexity and poor competition in compilers targeting these chips can tip the balance in favour of humans. Also for restricted devices you are often trading off code size/memory size/performance in a way that would be hard to instruct a compiler to do. e.g. I know this user action is not called often so I will have small code size and poor performance, but this other function that look similar is used every second so I will have a larger code size and faster performance. That is the sort of trade off a skilled assembly programmer can use.

我还想补充一点,这里有很多中间地带,您可以用C编译代码并检查生成的程序集,然后更改C代码或调整并作为程序集进行维护。

我的朋友从事微控制器的工作,目前是用于控制小型电动机的芯片。他在低级c和汇编的组合中工作。他曾经告诉我,有一天他在工作中把主循环从48条指令减少到43条。他还面临着各种选择,比如代码已经增长到填满256k芯片,业务需要一个新功能,你呢

删除现有功能 减少部分或全部现有特性的大小,可能会以性能为代价。 提倡改用成本更高、功耗更高、外形更大的更大芯片。

我想补充一点,作为一个商业开发人员,我有很多的投资组合或语言、平台、应用程序类型,我从来没有觉得有必要深入编写程序集。我一直都很感激我所学到的知识。有时会被调试进去。

我知道我已经回答了“为什么我要学习汇编器”这个问题,但我觉得这是一个更重要的问题,而不是什么时候更快。

所以让我们再试一次 你应该考虑组装

致力于底层操作系统功能 在编译器上工作。 工作在一个极其有限的芯片,嵌入式系统等

记住比较你的程序集和生成的编译器,看看哪个更快/更小/更好。

大卫。