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

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

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


当前回答

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

其他回答

几乎任何时候编译器看到浮点代码,如果你使用的是旧的糟糕的编译器,手写的版本会更快。(2019年更新:对于现代编译器来说,这并不普遍。特别是在编译x87以外的东西时;编译器更容易使用SSE2或AVX进行标量数学运算,或任何具有平面FP寄存器集的非x86,不像x87的寄存器堆栈。)

主要原因是编译器不能执行任何健壮的优化。关于这个主题的讨论,请参阅来自MSDN的这篇文章。下面是一个例子,其中汇编版本的速度是C版本的两倍(用VS2K5编译):

#include "stdafx.h"
#include <windows.h>

float KahanSum(const float *data, int n)
{
   float sum = 0.0f, C = 0.0f, Y, T;

   for (int i = 0 ; i < n ; ++i) {
      Y = *data++ - C;
      T = sum + Y;
      C = T - sum - Y;
      sum = T;
   }

   return sum;
}

float AsmSum(const float *data, int n)
{
  float result = 0.0f;

  _asm
  {
    mov esi,data
    mov ecx,n
    fldz
    fldz
l1:
    fsubr [esi]
    add esi,4
    fld st(0)
    fadd st(0),st(2)
    fld st(0)
    fsub st(0),st(3)
    fsub st(0),st(2)
    fstp st(2)
    fstp st(2)
    loop l1
    fstp result
    fstp result
  }

  return result;
}

int main (int, char **)
{
  int count = 1000000;

  float *source = new float [count];

  for (int i = 0 ; i < count ; ++i) {
    source [i] = static_cast <float> (rand ()) / static_cast <float> (RAND_MAX);
  }

  LARGE_INTEGER start, mid, end;

  float sum1 = 0.0f, sum2 = 0.0f;

  QueryPerformanceCounter (&start);

  sum1 = KahanSum (source, count);

  QueryPerformanceCounter (&mid);

  sum2 = AsmSum (source, count);

  QueryPerformanceCounter (&end);

  cout << "  C code: " << sum1 << " in " << (mid.QuadPart - start.QuadPart) << endl;
  cout << "asm code: " << sum2 << " in " << (end.QuadPart - mid.QuadPart) << endl;

  return 0;
}

和一些数字从我的PC运行默认版本*:

  C code: 500137 in 103884668
asm code: 500137 in 52129147

出于兴趣,我用dec/jnz交换了循环,它对计时没有影响——有时更快,有时更慢。我想内存有限的方面使其他优化相形见绌。(编者注:更可能的情况是,FP延迟瓶颈足以隐藏循环的额外成本。对奇数/偶数元素并行进行两个Kahan求和,并在最后添加它们,可能会加快2倍的速度。)

哎呀,我正在运行一个稍微不同的代码版本,它输出的数字是错误的(即C更快!)修正并更新了结果。

只有在使用编译器不支持的特殊用途指令集时。

为了最大限度地利用具有多个管道和预测分支的现代CPU的计算能力,您需要以这样一种方式来构造汇编程序:a)人类几乎不可能编写b)甚至更不可能维护。

此外,更好的算法、数据结构和内存管理将为您提供至少一个数量级的性能,而不是在汇编中进行的微观优化。

我不能给出具体的例子,因为那是很多年前的事情了,但是在很多情况下,手工编写的汇编程序可以胜过任何编译器。原因:

您可以偏离调用约定,在寄存器中传递参数。 您可以仔细考虑如何使用寄存器,避免将变量存储在内存中。 对于跳转表之类的东西,可以避免检查索引的边界。

基本上,编译器在优化方面做得很好,这几乎总是“足够好”,但在某些情况下(如图形渲染),你要为每一个周期付出高昂的代价,你可以走捷径,因为你知道代码,而编译器不能,因为它必须在安全的方面。

事实上,我听说过一些图形渲染代码,其中一个例程,如直线绘制或多边形填充例程,实际上在堆栈上生成了一小块机器代码并在那里执行,以避免关于线条样式、宽度、模式等的连续决策。

也就是说,我想让编译器为我生成好的汇编代码,但又不太聪明,它们通常都是这样做的。事实上,我讨厌Fortran的一个原因是它为了“优化”而打乱代码,通常没有什么重要的目的。

通常,当应用程序出现性能问题时,都是由于浪费的设计造成的。这些天,我永远不会推荐汇编程序的性能,除非整个应用程序已经在它的生命周期内进行了调优,仍然不够快,并且把所有的时间都花在了紧凑的内部循环中。

补充:我见过很多用汇编语言编写的应用程序,与C、Pascal、Fortran等语言相比,汇编语言的主要速度优势是因为程序员在用汇编语言编码时要谨慎得多。他或她每天要写大约100行代码,不管哪种语言,在编译器语言中,这将等于3或400条指令。

答案很简单……一个非常了解汇编的人(也就是他身边有参考资料,并利用每一个小处理器缓存和管道特性等)保证能够产生比任何编译器更快的代码。

然而,如今在典型的应用程序中,这种差异并不重要。

长波克,只有一个限制时间。当你没有足够的资源来优化每一个代码的变化,并花时间分配寄存器,优化一些溢出和诸如此类的事情时,编译器每次都会赢。对代码进行修改、重新编译和度量。如有必要重复。

此外,你可以在高水平方面做很多事情。此外,检查生成的程序集可能会给人一种代码是垃圾的印象,但实际上它的运行速度比您想象的要快。例子:

Int y = data[i]; //在这里做一些事情。 call_function (y,…);

编译器将读取数据,将其推入堆栈(溢出),然后从堆栈读取并作为参数传递。听起来屎?它实际上可能是非常有效的延迟补偿,并导致更快的运行时。

//优化版本 call_function(数据[我],…);//毕竟不是那么优化。

优化版本的想法是,我们降低了寄存器压力,避免溢出。但事实上,“垃圾”版本更快!

看看汇编代码,只看指令,然后得出结论:指令越多,速度越慢,这将是一个错误的判断。

这里需要注意的是:许多组装专家认为他们知道很多,但知道的很少。规则也会随着架构的变化而变化。例如,x86代码并不存在总是最快的银弹。如今,最好还是按照经验法则行事:

记忆很慢 缓存速度快 尽量更好地使用缓存 你多久会错过一次?你有延迟补偿策略吗? 对于一个cache miss,你可以执行10-100个ALU/FPU/SSE指令 应用程序架构很重要。 . .但是当问题不在架构上时,它就没有帮助了

此外,过于相信编译器会神奇地将考虑不周到的C/ c++代码转换为“理论上最优”的代码是一厢情愿的想法。如果你关心这个低级别的“性能”,你必须知道你使用的编译器和工具链。

C/ c++中的编译器通常不太擅长重新排序子表达式,因为对于初学者来说,函数有副作用。函数式语言没有受到这个警告的影响,但它不太适合当前的生态系统。有一些编译器选项可以允许宽松的精确规则,允许编译器/链接器/代码生成器改变操作的顺序。

这个话题有点死路一条;对于大多数人来说,这是无关紧要的,而剩下的人,他们已经知道自己在做什么了。

这一切都归结为:“理解你在做什么”,这与知道你在做什么有点不同。