什么时候装配比C快?

时间:2009-02-23 13:03:27

标签: c performance assembly

了解汇编程序的一个原因是,有时可以使用它来编写比在更高级语言中编写代码更高效的代码,特别是C.但是,我也听过很多次说虽然这并非完全错误,但汇编程序可以实际用于生成更高性能代码的情况极为罕见,需要专业知识和经验部件。

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

任何人都可以提供一些特定示例的情况,其中汇编比使用现代编译器的编写良好的C代码更快,并且您是否可以通过分析证据支持该声明?我非常有信心这些案例存在,但我真的想知道这些案件究竟有多深奥,因为这似乎是一些争论的焦点。

40 个答案:

答案 0 :(得分:255)

这是一个真实世界的例子:固定点在旧编译器上相乘。

这些不仅可以在没有浮点的设备上使用,它们在精度方面也会发光,因为它们可以提供32位精度并且具有可预测的误差(浮点数只有23位且预测精度损失更难)。即在整个范围内均匀绝对精度,而不是接近均匀的相对精度(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和其他公司)可以在单个指令中进行乘法运算。一些编译器过去忽略了这个事实并生成了调用运行时库函数来执行乘法的代码。 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-lines,我对分区重定点代码进行了10倍的改进。


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

2007年的gcc4.1也很好地优化了纯C版本。 (Godbolt编译器浏览器没有安装任何早期版本的gcc,但可能更老的GCC版本可以在没有内在函数的情况下执行此操作。)

请参阅x86(32位)的源+ asm和the Godbolt compiler explorer的ARM。 (不幸的是,它没有足够的编译器来生成简单纯C版本的错误代码。)


现代CPU可以做的事情C根本没有的操作符,如popcnt或位扫描找到第一个或最后一个设置位 。 (POSIX具有ffs()函数,但其​​语义与x86 bsf / bsr不匹配。请参阅https://en.wikipedia.org/wiki/Find_first_set)。

有些编译器有时可以识别一个循环,该循环计算整数中的设置位数并将其编译为popcnt指令(如果在编译时启用),但使用{{1}更可靠在GNU C中,或者在x86上,如果你只是使用SSE4.2定位硬件:_mm_popcnt_u32 from <immintrin.h>

或者在C ++中,分配给__builtin_popcnt并使用std::bitset<32>。 (这种情况下,语言已经找到了一种通过标准库可移植地公开popcount的优化实现的方法,其方式总是可以编译为正确的,并且可以利用目标支持的任何内容。)另请参阅{ {3}}

类似地,.count()可以在具有它的某些C实现上编译为ntohl(x86 32位字节交换以进行字节序转换)。


内在函数或手写asm的另一个主要领域是使用SIMD指令的手动矢量化。编译器对于像bswap这样的简单循环并不错,但是当事情变得更复杂时,编译器通常会很糟糕或者根本不会自动矢量化。例如,您不太可能得到编译器从标量代码自动生成的https://en.wikipedia.org/wiki/Hamming_weight#Language_support之类的内容。

答案 1 :(得分:131)

许多年前,我在教某人用C编程。练习是将图形旋转90度。他带着一个需要几分钟才能完成的解决方案回来了,主要是因为他使用的是乘法和除法等等。

我向他展示了如何使用位移来重新解决这个问题,并且在他所使用的非优化编译器上,处理时间缩短到大约30秒。

我刚刚获得了一个优化编译器,并且相同的代码在&lt;中旋转了图形。 5秒。我查看了编译器生成的汇编代码,从我看到的决定那里,然后我编写汇编程序的日子结束了。

答案 2 :(得分:59)

几乎在编译器看到浮点代码的任何时候,手写版本都会更快。主要原因是编译器无法执行任何强大的优化。 See this article from 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;
}

我的电脑上运行默认版本的一些数字构建 *

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

出于兴趣,我用dec / jnz交换了循环,它对时间没有任何影响 - 有时更快,有时更慢。我想内存有限的方面相形见绌。

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

答案 3 :(得分:55)

在不提供任何具体示例或探查器证据的情况下,如果您不仅仅了解编译器,则可以编写比编译器更好的汇编程序。

在一般情况下,现代C编译器更多地了解如何优化有问题的代码:它知道处理器管道如何工作,它可以尝试比人类更快地重新排序指令,等等 - 它基本上与计算机一样好或者比最好的人类游戏玩家更好等等,仅仅因为它可以使问题空间内的搜索速度比大多数人快。虽然理论上你在特定情况下可以像计算机一样运行,但你当然不能以相同的速度执行它,使它在不止一些情况下变得不可行(例如,如果你尝试编写,编译器肯定会胜过你汇编程序中的一些例程。)

另一方面,有些情况下编译器没有那么多的信息 - 我主要说的是在使用不同形式的外部硬件时,编译器不知道。主要的例子可能是设备驱动程序,其中汇编程序结合人类对所讨论硬件的深入了解可以产生比C编译器更好的结果。

其他人已经提到了特殊目的指令,这就是我在上面的段落中所说的 - 编译器可能有限或根本没有知识的指令,使得人类可以编写更快的代码。

答案 4 :(得分:45)

在我的工作中,我有三个理由知道并使用装配。按重要性排序:

  1. 调试 - 我经常会得到包含错误或文档不完整的库代码。我通过踩踏装配层来弄清楚它在做什么。我必须每周一次这样做。我还将它用作调试问题的工具,在这些问题中我的眼睛没有发现C / C ++ / C#中的惯用错误。看着大会就过去了。

  2. 优化 - 编译器在优化方面表现相当不错,但我玩的方式与大多数人不同。我编写的图像处理代码通常以如下代码开头:

    for (int y=0; y < imageHeight; y++) {
        for (int x=0; x < imageWidth; x++) {
           // do something
        }
    }
    
    “做某事”通常发生在数百万次(即3到30次)之间。通过在“做某事”阶段中抓取周期,性能增益被大大放大。我通常不会从那里开始 - 我通常首先编写代码来开始工作,然后尽力重构C以使其更好(更好的算法,更少的循环负载等)。我通常需要读取汇编以查看正在发生的事情并且很少需要编写它。我这可能每两到三个月做一次。

  3. 做一些语言不会让我。这些包括 - 获得处理器架构和特定的处理器功能,访问不在CPU中的标志(男人,我真的希望C让你访问进位标志)等等。我这样做可能一年或两年。

答案 5 :(得分:40)

仅在使用某些专用指令集时,编译器才支持。

为了最大化具有多个流水线和预测分支的现代CPU的计算能力,您需要以这样的方式构建汇编程序:a)人类几乎不可能编写b)更难以维护。

此外,更好的算法,数据结构和内存管理将比您在汇编时可以进行的微优化提供至少一个数量级的性能。

答案 6 :(得分:38)

虽然C与8位,16位,32位,64位数据的低级操作“接近”,但是C不支持一些数学运算,这些运算通常可以优雅地执行某些汇编指令集:

  1. 定点乘法:两个16位数的乘积是32位数。但C中的规则表明两个16位数的乘积是16位数,两个32位数的乘积是32位数 - 两种情况下都是下半部。如果你想要 top 16x16乘法的一半或32x32乘法,你必须使用编译器玩游戏。一般方法是转换为大于必要的位宽,乘法,向下移位和强制转换:

    int16_t x, y;
    // int16_t is a typedef for "short"
    // set x and y to something
    int16_t prod = (int16_t)(((int32_t)x*y)>>16);`
    

    在这种情况下,编译器可能足够聪明,知道你真的只是试图获得16x16乘法的上半部分并使用机器的原生16x16乘法做正确的事情。或者它可能是愚蠢的并且需要库调用来执行32x32乘法,因为你只需要产品的16位 - 但C标准并没有给你任何表达自己的方法。

  2. 某些位移操作(旋转/进位):

    // 256-bit array shifted right in its entirety:
    uint8_t x[32];
    for (int i = 32; --i > 0; )
    {
       x[i] = (x[i] >> 1) | (x[i-1] << 7);
    }
    x[0] >>= 1;
    

    这在C语言中并不太优雅,但同样,除非编译器足够智能以实现您正在做的事情,否则它将会做很多“不必要的”工作。许多汇编指令集允许您在进位寄存器中左右旋转或左移,因此您可以在34条指令中完成上述操作:将指针加载到数组的开头,清除进位,然后执行32 8-位右移,使用指针自动递增。

    另一个例子,有linear feedback shift registers(LFSR)在汇编中优雅地执行:取一大块N位(8,16,32,64,128等),将整个事物转移到右边1(参见上面的算法),然后如果结果进位为1,那么你在表示多项式的位模式中进行异或。

  3. 话虽如此,除非我有严重的性能限制,否则我不会采用这些技术。正如其他人所说,汇编比C代码更难记录/调试/测试/维护:性能提升带来了一些严重的成本。

    编辑 3.可以在汇编中进行溢出检测(在C中无法实现),这使得某些算法更加容易。

答案 7 :(得分:23)

简短回答?有时。

从技术上讲,每个抽象都有成本,编程语言是CPU工作方式的抽象。然而,C非常接近。几年前我记得当我登录我的UNIX帐户时大声笑出来并得到以下财富信息(当这些事情很受欢迎时):

  

C编程语言 - A.   语言结合了   汇编语言的灵活性   汇编语言的力量。

这很有趣,因为它是真的:C就像便携式汇编语言。

值得注意的是,只要您编写汇编语言,它就会运行。然而,在C和它生成的汇编语言之间有一个编译器,这非常重要,因为你的C代码有多快与你的编译器有多好有关。

当gcc出现在现场时,其中一个让它如此受欢迎的事情是,它通常比带有许多商业UNIX风格的C编译器好得多。它不仅是ANSI C(没有这个K&amp; R C垃圾),更强大并且通常产生更好(更快)的代码。不总是但经常。

我告诉你这一切,因为没有关于C和汇编程序速度的一揽子规则,因为C没有客观标准。

同样,汇编程序会有很大差异,具体取决于您运行的处理器,系统规格,您正在使用的指令集等等。历史上,有两种CPU架构系列:CISC和RISC。 CISC中最大的参与者是英特尔x86架构(和指令集)。 RISC主宰了UNIX世界(MIPS6000,Alpha,Sparc等)。 CISC为心灵和思想赢得了胜利。

无论如何,当我还是一个年轻的开发人员时,流行的智慧是手写的x86通常比C快得多,因为架构的工作方式,它的复杂性受益于人类的做法。另一方面,RISC似乎是为编译器设计的,所以没有人(我知道)写过Sparc汇编程序。我相信这些人确实存在,但毫无疑问他们已经疯了,现在已经制度化了。

即使在同一系列处理器中,指令集也是重要的一点。某些英特尔处理器具有SSE到SSE4等扩展。 AMD有他们自己的SIMD指令。像C这样的编程语言的好处是有人可以编写他们的库,因此它针对您运行的任何处理器进行了优化。这对汇编程序来说很难。

在汇编程序中仍然可以进行优化,没有编译器可以进行优化,并且编写良好的汇编程序algoirthm将比它的C等价物快或快。更大的问题是:值得吗?

最终虽然汇编程序是它的时代产品,但在CPU周期昂贵的时候更受欢迎。如今制造成本为5-10美元的CPU(英特尔凌动)可以完成任何人想要的任何事情。这些天编写汇编程序的唯一真正原因是低级操作系统的某些部分(即使是绝大多数Linux内核都是用C语言编写),设备驱动程序,可能是嵌入式设备(尽管C往往在那里占主导地位)也)等等。或者只是为了踢(有点自虐)。

答案 8 :(得分:15)

第一点不是答案。
即使你从来没有编程,我发现至少知道一个汇编指令集很有用。这是程序员永远不断寻求了解更多并因此更好的一部分。在进入框架时也很有用,你没有源代码,并且至少知道发生了什么。它还可以帮助您理解JavaByteCode和.Net IL,因为它们与汇编程序类似。

当您有少量代码或大量时间时回答问题。最适用于嵌入式芯片,其中低芯片复杂性和针对这些芯片的编译器的竞争不足可能会使人们有利于平衡。此外,对于受限设备,您通常会以难以指示编译器执行的方式处理代码大小/内存大小/性能。例如我知道这个用户操作不经常被调用,所以我的代码大小很小,性能很差,但是这个看起来很相似的其他函数每秒都会被使用,所以我将拥有更大的代码大小和更快的性能。这是熟练的汇编程序员可以使用的那种权衡。

我还想补充一下,你可以在C编译中编写代码并检查生成的汇编,然后更改C代码或调整并维护为汇编。

我的朋友在微控制器上工作,目前用于控制小型电动机的芯片。他的工作是低级别c和汇编。他曾告诉我工作中的好日子,他将主要循环从48条指令减少到43条。他还面临着代码已经增长到填充256k芯片以及业务需要新功能的选择,你< / p>

  1. 删除现有功能
  2. 可能会以性能为代价减少部分或全部现有功能的大小。
  3. 倡导更高成本,更高功耗和更大外形尺寸的更大芯片。

我想作为一个商业开发人员添加一个或多种语言,平台,应用类型,我从来没有觉得有必要深入编写汇编。我一直都很欣赏我所获得的知识。有时调试进去。

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

让我们再试一次 你应该考虑装配

  • 致力于低级操作系统功能
  • 使用编译器。
  • 使用极其有限的芯片,嵌入式系统等

请记住将您的程序集与生成的编译器进行比较,以查看哪个更快/更小/更好。

大卫。

答案 9 :(得分:15)

一个用例可能不再适用,但为了你的书呆子乐趣:在Amiga上,CPU和图形/音频芯片将争取访问某个RAM区域(前2MB的RAM是特定的)。因此,当你只有2MB RAM(或更少)时,显示复杂的图形和播放声音会破坏CPU的性能。

在汇编程序中,您可以巧妙地交错代码,以便当图形/音频芯片在内部忙时(即总线空闲时),CPU只会尝试访问RAM。因此,通过重新排序您的指令,巧妙地使用CPU缓存,总线时序,您可以实现一些使用任何更高级别语言无法实现的效果,因为您必须对每个命令进行计时,甚至在此处插入NOP以保持各种彼此的雷达芯片。

这也是为什么CPU的NOP(无操作 - 什么也不做)指令实际上可以使整个应用程序运行得更快的另一个原因。

[编辑]当然,该技术取决于特定的硬件设置。这是许多Amiga游戏无法应对更快CPU的主要原因:指令的时间关闭。

答案 10 :(得分:14)

我很惊讶没人说这个。如果用汇编编写,strlen()函数会快得多!在C中,你能做的最好的事情是

int c;
for(c = 0; str[c] != '\0'; c++) {}

在装配时你可以大大加快速度:

mov esi, offset string
mov edi, esi
xor ecx, ecx

lp:
mov ax, byte ptr [esi]
cmp al, cl
je  end_1
cmp ah, cl
je end_2
mov bx, byte ptr [esi + 2]
cmp bl, cl
je end_3
cmp bh, cl
je end_4
add esi, 4
jmp lp

end_4:
inc esi

end_3:
inc esi

end_2:
inc esi

end_1:
inc esi

mov ecx, esi
sub ecx, edi

长度在ecx中。这比较了4个字符,因此速度提高了4倍。并且考虑使用eax和ebx的高阶词,它将比以前的C例程快8倍

答案 11 :(得分:13)

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

  • 您可以偏离调用约定,在寄存器中传递参数。

  • 您可以仔细考虑如何使用寄存器,并避免将变量存储在内存中。

  • 对于像跳转表这样的东西,你可以避免必须检查索引。

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

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

那就是说,我想让编译器做的就是为我生成好的汇编代码,但不要太聪明,而且他们大多数都是这样做的。事实上,我讨厌Fortran的一个问题就是它试图“优化”代码来扰乱代码,通常没有明显的目的。

通常,当应用程序出现性能问题时,这是由于浪费的设计。这些天,我永远不会推荐汇编程序的性能,除非整个应用程序已经在其生命的一英寸范围内调整,仍然不够快,并且花费所有时间在紧密的内循环。

补充:我见过很多用汇编语言编写的应用程序,而且比C,Pascal,Fortran等语言的主要速度优势是因为程序员在汇编语言编写时要小心得多。无论语言如何,他或她每天都要编写大约100行代码,而且编译器语言将等于3或400条指令。

答案 12 :(得分:12)

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

答案 13 :(得分:11)

根据我的经验举几个例子:

  • 访问无法从C访问的指令。例如,许多体系结构(如x86-64,IA-64,DEC Alpha和64位MIPS或PowerPC)支持64位乘64位乘法产生128位结果。 GCC最近添加了一个扩展,提供对此类指令的访问,但在此之前需要进行组装。在实现类似RSA的操作时,访问此指令可以对64位CPU产生巨大影响 - 有时可以提高性能提升4倍。

  • 访问特定于CPU的标志。困扰我的那个是携带标志;当进行多精度加法时,如果你无法访问CPU进位,则必须比较结果以查看它是否溢出,每个肢体需要3-5个指令;更糟糕的是,这在数据访问方面是相当连续的,这会破坏现代超标量处理器的性能。当连续处理数千个这样的整数时,能够使用addc是一个巨大的胜利(在进位位上存在争用的超标量问题,但现代CPU处理得相当好。)

  • SIMD。即使是自动向量化编译器也只能做相对简单的情况,所以如果你想要良好的SIMD性能,不幸的是经常需要直接编写代码。当然,您可以使用内在函数而不是汇编,但是一旦您处于内在函数级别,您基本上就是编写汇编,只需使用编译器作为寄存器分配器和(名义上)指令调度程序。 (我倾向于将内在函数用于SIMD,因为编译器可以为我生成函数序言和诸如此类的东西,因此我可以在Linux,OS X和Windows上使用相同的代码,而无需处理函数调用约定等ABI问题,但其他比起SSE内在函数真的不是很好 - 虽然我对它们没有多少经验,但Altivec似乎更好。作为(当天)矢量化编译器无法弄清楚的事情的例子,请阅读bitslicing AESSIMD error correction - 可以想象一个编译器可以分析算法并生成这样的代码,但我觉得像这样一个智能编译器至少距离现有(最好)30年。

另一方面,多核计算机和分布式系统已经在许多方面取得了最大的性能提升 - 在组装中编写内部循环可以获得额外的20%加速,或者通过在多个核心上运行它们可以获得300%的加速,或者通过在一组计算机上运行它们来获得10000%。当然,高级优化(诸如期货,记忆等等)通常在诸如ML或Scala之类的高级语言中比C或asm更容易,并且通常可以提供更大的性能获胜。因此,一如既往,需要做出权衡。

答案 14 :(得分:10)

比你想象的更频繁,C需要从大会编码器的角度做一些看似不必要的事情,因为C标准是这样说的。

例如,

整数推广。如果你想在C中移动一个char变量,人们通常会期望代码实际上就是这样,一个位移。

然而,标准强制编译器在移位之前对符号进行扩展,并在之后将结果截断为char,这可能会使代码复杂化,具体取决于目标处理器的体系结构。

答案 15 :(得分:10)

紧密循环,就像播放图像一样,因为图像可能需要数百万像素。坐下来弄清楚如何充分利用有限数量的处理器寄存器可以产生影响。这是一个现实生活中的样本:

http://danbystrom.se/2008/12/22/optimizing-away-ii/

然后,处理器通常会有一些深奥的指令,这些指令过于专业化,无法让编译器烦恼,但有时汇编程序员可以很好地利用它们。以XLAT指令为例。非常好,如果你需要在循环中进行表查找表限制为256字节!

更新:哦,当我们谈到循环时,我们只想到最重要的事情:编译器通常不知道常见情况下会有多少次迭代!只有程序员知道一个循环会被迭代很多次,因此为一些额外的工作准备循环是有益的,或者如果它将被迭代这么多次以至于设置实际上将花费比迭代更长的时间预期

答案 16 :(得分:9)

如果你没有看过编译器生成的反汇编,你实际上并不知道你编写良好的C代码是否真的很快。很多时候你看它并且看到“写得好”是主观的。

所以没有必要用汇编语言来获得最快的代码,但出于同样的原因,知道汇编程序当然是值得的。

答案 17 :(得分:8)

我认为汇编程序更快的一般情况是智能汇编程序员查看编译器的输出并说“这是性能的关键路径,我可以写这个更高效”然后那个人调整汇编程序或者从头开始重写。

答案 18 :(得分:7)

这一切都取决于你的工作量。

对于日常操作,C和C ++很好,但是有一些工作负载(任何涉及视频(压缩,解压缩,图像效果等)的转换)都需要组装才能实现。

它们通常还涉及使用针对这些操作进行调整的CPU专用芯片组扩展(MME / MMX / SSE /无论如何)。

答案 19 :(得分:6)

可能值得一看Optimizing Immutable and Purity by Walter Bright它不是一个分析测试,但向您展示了手写编译器和编译器生成的ASM之间差异的一个很好的例子。 Walter Bright编写了优化编译器,因此可能值得查看其他博客文章。

答案 20 :(得分:6)

我已经阅读了所有答案(超过30个)并且没有找到一个简单的理由:如果你已经阅读并练习了Intel® 64 and IA-32 Architectures Optimization Reference Manual,那么汇编程序比C更快,所以之所以如此装配速度可能较慢的是编写这种较慢装配的人没有阅读优化手册。

在英特尔80286的旧时代,每条指令都以固定的CPU周期执行,但自从1995年发布的Pentium Pro以来,英特尔处理器变得超标量,利用复杂流水线技术:无序执行&amp;注册重命名。在此之前,在1993年生产的Pentium上,有U和V管道:双管线,如果它们不相互依赖,可以在一个时钟周期执行两条简单的指令;但这无法比较什么是乱序执行&amp;注册重命名出现在Pentium Pro中,现在几乎保持不变。

用几句话来解释,最快的代码是指令不依赖于先前的结果,例如,你应该总是清除整个寄存器(通过movzx)或使用add rax, 1代替或inc rax来删除对先前状态标志的依赖等。

您可以阅读有关无序执行的更多信息。如果时间允许,注册重命名,互联网上有大量信息。

还有其他重要问题,如分支预测,加载和存储单元数,执行微操作的门数等,但最重要的是要考虑乱序执行。

大多数人根本不了解无序执行,因此他们编写了类似于80286的汇编程序,期望无论上下文如何,他们的指令都需要一段固定的时间来执行;而C编译器知道无序执行并正确生成代码。这就是为什么这些不知情的人的代码速度较慢,但​​如果你意识到,你的代码会更快。

答案 21 :(得分:6)

我有一个需要完成的位转换操作,每次中断192或256位,每50微秒发生一次。

它通过固定地图(硬件约束)发生。使用C,需要大约10微秒。当我将其转换为Assembler时,考虑到此映射的特定功能,特定的寄存器缓存以及使用面向位的操作;执行时间不到3.5微秒。

答案 22 :(得分:5)

简单的回答......一个知道汇编的人(也就是他旁边有一个参考,并且正在利用每个小处理器缓存和管道功能等)保证能够生成比任何编译器快得多的代码。

然而,这些日子的差异在典型应用中并不重要。

答案 23 :(得分:5)

LInux assembly howto,询问这个问题并给出使用汇编的优点和缺点。

答案 24 :(得分:4)

CPP-M-86版本的PolyPascal(兄弟对Turbo Pascal)的一个可能性是用机器语言程序取代“使用bios-to-output-characters-to-the-screen”设施在essense中给出了x,和y以及放在那里的字符串。

这样可以比以前更快地更新屏幕!

二进制文件中有空间嵌入机器代码(几百个字节),还有其他东西,所以必须尽可能多地挤压。

事实证明,由于屏幕是80x25,两个坐标每个都可以放在一个字节中,所以两者都可以放在一个双字节的字中。这允许以更少的字节进行所需的计算,因为单个添加可以同时操作这两个值。

据我所知,没有C编译器可以在寄存器中合并多个值,对它们执行SIMD指令并稍后再将它们拆分(我认为机器指令不会更短)。

答案 25 :(得分:4)

在运行时创建机器代码怎么样?

我的兄弟曾经(大约2000年)通过在运行时生成代码来实现极快的实时光线跟踪器。我不记得细节,但是有一些主要模块循环遍历对象,然后它正在准备和执行一些特定于每个对象的机器代码。

然而,随着时间的推移,这种方法被新的图形硬件所淹没,并且变得毫无用处。

今天,我认为可以通过这种方法优化对大数据(数百万条记录)的一些操作,如数据透视表,钻孔,即时计算等。问题是:值得吗?

答案 26 :(得分:4)

这个问题有点误导。你的帖子本身就是答案。始终可以为特定问题编写汇编解决方案,该问题的执行速度比编译器生成的任何问题都快。问题是你需要成为装配专家才能克服编译器的局限。 经验丰富的汇编程序员可以在任何HLL中编写程序,其执行速度比没有经验的人编写的程序快。事实上,您总是可以编写比编译器生成的程序集执行速度更快的汇编程序。

答案 27 :(得分:4)

Longpoke,只有一个限制:时间。如果您没有资源来优化代码的每一次更改并花费时间分配寄存器,优化少量溢出,那么编译器每次都会获胜。您对代码进行了修改,重新编译和测量。必要时重复。

另外,你可以在高层面做很多事情。此外,检查生成的程序集可能会给IMPRESSION代码废话,但实际上它会比你想象的更快。例如:

int y = data [i]; //在这做一些事情.. call_function(y,...);

编译器将读取数据,将其推送到堆栈(溢出),然后从堆栈读取并作为参数传递。听起来很害羞?它实际上可能是非常有效的延迟补偿,并且可以提高运行时间。

//优化版 call_function(data [i],...); //毕竟不是那么优化..

优化版本的想法是,我们减少了套准压力并避免溢出。但事实上,“蹩脚”的版本更快!

查看汇编代码,只需查看说明并总结:更多指令,更慢,将是一个误判。

这里需要注意的是:许多装配专家认为他们知道很多,但知之甚少。规则也从架构变为下一个。例如,没有银弹x86代码,它始终是最快的。这些日子最好按照经验法则进行:

  • 内存很慢
  • 缓存很快
  • 尝试使用缓存更好
  • 你多久会错过一次?你有延迟补偿策略吗?
  • 您可以针对一次缓存未命中执行10-100 ALU / FPU / SSE指令
  • 应用程序架构很重要..
  • ..但是当问题不在架构中时它没有帮助

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

C / C ++中的编译器通常不太擅长重新排序子表达式,因为这些函数对于初学者来说有副作用。功能语言不会受到这种警告的影响,但不能很好地适应当前的生态系统。有一些编译器选项允许放宽精度规则,允许编译器/链接器/代码生成器更改操作顺序。

这个话题有点死路一条;对于大多数人来说,这是不相关的,其余的,他们知道他们在做什么。

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

答案 28 :(得分:4)

gcc已成为一种广泛使用的编译器。它的优化总体上并不那么好。远比一般程序员编写汇编程序好,但对于真正的性能,并没有那么好。有些编译器在它们生成的代码中简直令人难以置信。因此,作为一般性答案,您可以在许多地方进入编译器的输出并调整汇编程序的性能,和/或简单地从头开始重新编写例程。

答案 29 :(得分:4)

http://cr.yp.to/qhasm.html有很多例子。

答案 30 :(得分:4)

鉴于合适的程序员,汇编程序总是比C语言程序更快(至少是略微)。很难创建一个C程序,你无法取出至少一个汇编程序的指令。

答案 31 :(得分:4)

其中一个更着名的装配片段来自Michael Abrash的纹理映射循环(expained in detail here):

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特定指令表达为内在函数,即可以编译为实际指令的函数。 MS Visual C ++支持MMX,SSE,SSE2,SSE3和SSE4的内在函数,因此您不必担心下降到汇编以利用特定于平台的指令。 Visual C ++还可以利用适当的/ ARCH设置所针对的实际体系结构。

答案 32 :(得分:1)

在以MHz为单位测量处理器速度且屏幕尺寸低于1百万像素的日子里,一个众所周知的具有更快显示效果的技巧是展开循环:对屏幕的每条扫描线进行写入操作。它避免了维护循环索引的开销!再加上检测屏幕刷新,它非常有效 这是C编译器不会做的事情......(虽然通常你可以选择速度或大小的优化,但我认为前者使用了一些类似的技巧。)

我知道有些人喜欢用汇编语言编写Windows应用程序。他们声称他们更快(难以证明)和更小(确实!) 显然,虽然这很有趣,但可能浪费时间(当然除了学习目的!),特别是对于GUI操作...... 现在,也许一些操作,比如在文件中搜索字符串,可以通过仔细编写的汇编代码进行优化。

答案 33 :(得分:1)

这很难回答,因为问题非常不明确:“现代编译器”究竟是什么?

理论上,任何手动汇编程序优化都可以由编译器完成 - 无论是否实际 完成,都不能说一般,只能说明特定编译器的特定版本。许多人可能需要付出很多努力来确定它们是否可以在编译器编写者不打扰它们的特定上下文中应用而没有副作用。

答案 34 :(得分:1)

实际上你可以在大型模型模式中构建大规模程序,segaments可以限制为64kb代码,但是你可以编写很多段,人们给出反对ASM的论据,因为它是一种旧语言,我们不需要保留内存如果是这样的话,我们为什么要用内存打包我们的PC,我能用ASM找到的唯一一个缺点就是它基于处理器,因此大多数为intel架构编写的程序很可能无法运行在AMD上建筑。至于C比ASM更快,没有语言比ASM更快,ASM可以做很多事情的C而其他HLL在处理器级别不能做。 ASM是一门难学的语言,但一旦你学会了它,HLL就无法比你更好地翻译它。如果您只能看到HLL对您做的一些代码,并了解它正在做什么,您会想知道为什么更多的人不使用ASM以及为什么不再更新成员(无论如何一般公众使用)。所以没有C不比ASM快。甚至经验C ++程序员仍然使用和编写代码块在ASM中添加了C ++代码以提高速度。其他语言还有一些人认为过时或可能没有好处是一个神话,例如Photoshop是用Pascal / ASM编写的第一个版本已经提交给技术历史博物馆,而且喷绘工具专业版仍然是用Python编写的, TCL和ASM ......这些的一个共同点是“快速和伟大的图像处理器是ASM,虽然photoshop可能已升级到delphi现在它仍然是pascal。并且任何速度问题都来自pascal但这是因为我们喜欢这种方式程序看起来而不是他们现在做的事情。我想在纯粹的ASM中制作一个Photoshop Clone,我一直在努力并且它很好地进行。不是代码,解释,arange,rewwrite等等......只需要代码并完成流程。

答案 35 :(得分:1)

历史悠久。

根据我的经验,当我还是一个年轻的人(1970年代)时,汇编程序很重要,而不仅仅是代码的速度。

例如,如果使用高级语言的模块是1300字节的代码,但是该模块的汇编器版本是300字节,那么当您尝试将应用程序适配为16K或32K时,该1K字节非常重要。的内存。

当时的编译器还不是很好。

在老式的Fortran中

X = (Y - Z)
IF (X .LT. 0) THEN
 ... do something
ENDIF

当时的编译器在X上执行了SUBTRACT指令,然后执行了TEST指令。 在汇编器中,您只需在减法后检查条件代码(LT零,零,GT零)即可。

对于现代系统和编译器,这都不是问题。

我确实认为了解编译器的工作仍然很重要。 当您使用高级语言进行编码时,您应该了解允许或禁止编译器进行循环展开的原因。

在涉及条件的流水线和超前计算中,当编译器执行“ branch-likley”时

在执行高级语言不允许的操作时(例如读取或写入处理器特定的寄存器),仍然需要汇编程序。

但是,在很大程度上,除了对如何编译和执行代码有基本了解之外,通用程序员不再需要。

答案 36 :(得分:0)

如今,考虑到英特尔C ++等极其优化C代码的编译器,很难与编译器输出竞争。

答案 37 :(得分:0)

我会说,当你比给定指令集的编译器更好的时候。所以我认为没有通用答案

答案 38 :(得分:-1)

这个问题有点没有意义,因为无论如何c都会被编译为汇编程序。 但是,通过优化编译器产生的汇编器几乎已完全优化,因此,除非您在优化特定汇编方面获得了20个博士学位,否则您就无法击败编译器。

答案 39 :(得分:-2)

我曾经和那些说过“如果编译器笨到想弄清楚你要做什么而又无法优化它的人一起工作,那么你的编译器就会被打破,现在是时候了。”我确信在汇编会破坏你的C代码时会出现边缘情况,但如果你经常发现自己使用汇编程序来“胜过”你的编译器,你的编译器就会被破坏。

同样可以说是编写了“优化的”SQL,它试图强迫查询规划器执行操作。如果您发现自己正在重新安排查询以使计划员按照您的意愿行事,那么您的查询计划程序就会被破坏 - 获得一个新的查询计划程序。