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

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

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


当前回答

The question is a bit misleading. The answer is there in your post itself. It is always possible to write assembly solution for a particular problem which executes faster than any generated by a compiler. The thing is you need to be an expert in assembly to overcome the limitations of a compiler. An experienced assembly programmer can write programs in any HLL which performs faster than one written by an inexperienced. The truth is you can always write assembly programs executing faster than one generated by a compiler.

其他回答

这个问题有点毫无意义,因为无论如何c都是编译到汇编程序的。 但是,通过优化编译器生成的汇编程序几乎是完全优化的,所以除非你在优化特定的汇编程序方面做了20个博士学位,否则你无法打败编译器。

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

这些不仅在没有浮点数的设备上很方便,在精度方面也很出色,因为它们可以提供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语言编程。练习是将图形旋转90度。他得到了一个花了几分钟才能完成的解,主要是因为他使用了乘法和除法等。

我向他展示了如何使用位移位重定义问题,在他拥有的非优化编译器上,处理时间缩短到大约30秒。

我刚刚得到了一个优化编译器,相同的代码在< 5秒内旋转图形。我看着编译器生成的汇编代码,从我所看到的,我决定我写汇编程序的日子结束了。

第一点不是答案。 即使你从来没有用它编程,我发现至少知道一个汇编指令集是有用的。这是程序员永无止境的追求的一部分,他们想知道得更多,从而变得更好。当你进入一个没有源代码的框架时,它也很有用,至少对正在发生的事情有一个粗略的了解。它还可以帮助您理解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芯片,业务需要一个新功能,你呢

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

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

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

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

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

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

大卫。

只要有合适的程序员,汇编程序总是可以比C程序快(至少稍微快一点)。如果不能从汇编器中取出至少一条指令,则很难创建一个C程序。