什么时候汇编比C快?

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

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

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

140747 次浏览

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

如果您没有查看编译器生成的内容的反汇编,您实际上无法知道编写良好的C代码是否真的很快。很多时候你会发现“写得好”是主观的。

因此,没有必要用汇编程序来获得最快的代码,但出于同样的原因,了解汇编程序当然是值得的。

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

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

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

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

这很难具体地回答,因为这个问题非常不具体:到底什么是“现代编译器”?

理论上,几乎任何手动的汇编器优化都可以由编译器完成——它是否真的完成不能笼统地说,只能说特定编译器的特定版本。许多可能需要花费大量的精力来确定它们是否可以在特定的上下文中应用而不产生副作用,以至于编译器编写者不会为它们烦恼。

不需要给出任何具体的示例或分析器证据,当您比编译器知道的更多时,您可以编写比编译器更好的汇编程序。

在一般情况下,现代C编译器知道更多关于如何优化有问题的代码:它知道处理器管道是如何工作的,它可以比人类更快地尝试重新排序指令,等等——这基本上就像一台计算机在棋盘游戏中与最好的人类玩家一样好,甚至更好,等等,仅仅因为它可以比大多数人更快地在问题空间内进行搜索。虽然理论上你可以在特定的情况下执行得和计算机一样好,但你肯定不能以相同的速度完成,这使得它在少数情况下是不可行的(即,如果你尝试在汇编程序中编写多个例程,编译器肯定会优于你)。

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

其他人提到了特殊用途指令,这就是我在上面一段中所说的——编译器可能对这些指令了解有限或根本不了解,这使得人类可以编写更快的代码。

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

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

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

  • 对于跳转表之类的东西,可以避免检查索引的边界。

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

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

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

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

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

简短的回答吗?有时。

从技术上讲,每一个抽象都有成本,而编程语言是CPU如何工作的抽象。然而C非常接近。几年前,我记得当我登录UNIX帐户并收到以下财富信息时(当时这种东西很流行),我笑出声来:

C程序设计语言——A 语言结合了 汇编语言的灵活性 汇编语言的力量。

这很有趣,因为这是真的:C就像可移植的汇编语言。

值得注意的是,汇编语言无论如何编写都可以运行。然而,在C语言和它生成的汇编语言之间有一个编译器,这是非常重要的,因为你的C代码有多快和你的编译器有多好有很大关系。

当gcc出现时,它如此受欢迎的原因之一是它通常比许多商业UNIX版本附带的C编译器要好得多。它不仅是ANSI C(没有任何K&R C的垃圾),更健壮,通常能产生更好(更快)的代码。不是总是,而是经常。

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

同样地,汇编程序也会根据你正在运行的处理器、你的系统规格、你正在使用的指令集等而有很大的不同。历史上有两个CPU体系结构家族:CISC和RISC。CISC中最大的玩家过去是,现在仍然是Intel x86架构(和指令集)。RISC主宰了UNIX世界(MIPS6000、Alpha、Sparc等等)。CISC赢得了民心之战。

不管怎样,当我还是一个年轻的开发人员时,流行的观点是,手写的x86通常比C快得多,因为架构的工作方式,它的复杂性受益于人类的操作。另一方面,RISC似乎是为编译器设计的,所以没有人(我知道)写Sparc汇编器。我相信这样的人确实存在,但毫无疑问,他们现在都疯了,被送进了精神病院。

指令集是一个重要的点,即使在同一家族的处理器。某些英特尔处理器具有SSE到SSE4等扩展。AMD有他们自己的SIMD指令。像C这样的编程语言的好处是,人们可以编写他们的库,以便对您运行的任何处理器进行优化。这在汇编程序中是一项艰苦的工作。

你仍然可以在汇编程序中做一些编译器无法做的优化,一个编写良好的汇编程序算法将会和它的C等效程序一样快或更快。更大的问题是:这样做值得吗?

最终,汇编程序是其时代的产物,在CPU周期昂贵的时候更受欢迎。如今,一个制造成本为5-10美元的CPU(英特尔Atom)几乎可以做任何人想做的任何事情。现在编写汇编程序的唯一真正原因是为了底层的事情,比如操作系统的某些部分(即使绝大多数Linux内核是用C编写的),设备驱动程序,可能是嵌入式设备(尽管C也倾向于在那里占主导地位)等等。或者只是为了好玩(这有点受虐)。

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

当你有少量的代码或大量的时间来回答这个问题。最适用于嵌入式芯片,其中低芯片复杂性和针对这些芯片的编译器的低竞争可以使平衡向有利于人类的方向倾斜。此外,对于受限的设备,你经常要权衡代码大小/内存大小/性能,而这很难指示编译器去做。例:我知道这个用户操作不经常被调用,所以我的代码很小,性能也很差,但是另一个看起来类似的函数每秒都会被使用,所以我的代码更大,性能更快。这是一个熟练的汇编程序员可以使用的一种权衡。

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

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

李< ol > < > 删除一个的现有特性
  • 减少部分或全部现有特性的大小,可能会以性能为代价。
  • 主张转向成本更高、功耗更高、外形更大的更大芯片

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

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

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

    • 工作在低级操作系统功能
    • 在编译器上工作。
    • 工作在一个极其有限的芯片,嵌入式系统等

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

    大卫。

  • 很多年前,我教别人用c语言编程。练习是将图形旋转90度。他得到了一个花了几分钟才能完成的解,主要是因为他使用了乘法和除法等。

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

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

    在Amiga上,CPU和图形/音频芯片会为了访问特定区域的RAM(具体来说是前2MB的RAM)而争斗。因此,当你只有2MB RAM(或更少)时,显示复杂的图形加上播放声音会杀死CPU的性能。

    在汇编程序中,你可以巧妙地交错你的代码,使CPU只在图形/音频芯片内部繁忙时(即当总线空闲时)才尝试访问RAM。因此,通过重新排序指令,巧妙地使用CPU缓存,总线定时,你可以实现一些使用任何高级语言都不可能实现的效果,因为你必须为每个命令定时,甚至在这里或那里插入nop,以使不同的芯片不受彼此的雷达影响。

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

    当然,这种技术取决于特定的硬件设置。这就是为什么许多Amiga游戏无法适应更快的cpu的主要原因:指令的计时错误。

    CP/M-86版本的PolyPascal (Turbo Pascal的兄弟)的一个可能性是用机器语言例程取代“使用生物将字符输出到屏幕上”的功能,本质上是给定x、y和字符串放在那里。

    这使得更新屏幕的速度比以前快得多!

    二进制文件中有足够的空间来嵌入机器代码(几百个字节),也有其他的东西,所以尽可能多地压缩是必要的。

    事实证明,由于屏幕是80x25,这两个坐标都可以容纳每个字节,所以都可以容纳两个字节的单词。这允许在更少的字节内完成所需的计算,因为单个添加可以同时操作两个值。

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

    几乎任何时候编译器看到浮点代码,如果你使用的是旧的糟糕的编译器,手写的版本会更快。(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运行默认版本build:

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

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

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

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

    尽管C语言“接近”于对8位、16位、32位和64位数据的低级操作,但仍有一些C语言不支持的数学操作通常可以在某些汇编指令集中优雅地执行:

    1. 定点乘法:两个16位数字的乘积是一个32位数字。但是C语言的规则是两个16位数字的乘积是一个16位数字,两个32位数字的乘积是一个32位数字——在这两种情况下都是下半部分。如果你想要一个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位右移。

      另一个例子,有线性反馈移位寄存器 (LFSR)在汇编中优雅地执行:取N位的块(8,16,32,64,128等),将整个东西右移1(参见上面的算法),然后如果结果进位为1,则以表示多项式的位模式异或。

    尽管如此,除非有严重的性能限制,否则我不会求助于这些技术。正如其他人所说,汇编代码比C代码更难记录/调试/测试/维护:性能的提高伴随着一些严重的代价。

    编辑: 3。溢出检测在汇编中是可能的(在C中不能真正做到),这使得一些算法更容易。

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

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


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

    • 求64位整数乘法的高部分:使用uint64_t for 32x32 => 64位乘法的便携版本无法在64位CPU上进行优化,因此需要intrinsic或__int128来在64位系统上实现高效的代码。
    • _umul128在Windows 32位: 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的情况下做到这一点。)

    参见source + asm for x86(32位)和ARM on Godbolt编译器浏览器。(不幸的是,它没有任何旧到足以从简单的纯C版本生成糟糕代码的编译器。)


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

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

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

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


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

    Walter Bright的《optimization Immutable and Purity》可能值得一看,它不是一个概要测试,但向你展示了手写和编译器生成ASM之间的区别的一个很好的例子。Walter Bright写优化编译器,所以值得一看他的其他博客文章。

    LInux程序汇编,问了这个问题,并给出了使用汇编的优点和缺点。

    紧密循环,就像处理图像时一样,因为一张图像可能需要数百万像素。坐下来研究一下如何最好地利用有限的处理器寄存器会有很大的不同。下面是一个真实的例子:

    < a href = " http://danbystrom。se / 2008/12/22 / optimizing-away-ii / noreferrer“rel = > http://danbystrom.se/2008/12/22/optimizing-away-ii/ < / >

    处理器通常有一些深奥的指令,这些指令对于编译器来说太专业了,但有时汇编程序员可以很好地利用它们。以XLAT指令为例。如果你需要在循环而且中进行表查找,这真的很棒,表被限制在256字节!

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

    一个比较著名的汇编代码片段来自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设置来利用您所针对的实际体系结构。

    在我的工作中,有三个原因让我了解和使用组装。按重要性排序:

    1. 调试——我经常得到有bug或文档不完整的库代码。我通过介入组装级别来弄清楚它在做什么。我每周都得做一次。我还使用它作为工具来调试我无法发现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能让你访问进位标志),等等。我可能一年或两年做一次。

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

    这完全取决于你的工作量。

    对于日常操作,C和c++已经很好了,但是有一些特定的工作负载(任何涉及视频的转换(压缩、解压缩、图像效果等))几乎需要组装才能达到性能。

    它们通常还涉及使用特定于CPU的芯片组扩展(MME/MMX/SSE/等等),这些扩展是为这些类型的操作而优化的。

    在处理器速度以MHz为单位,屏幕尺寸低于100万像素的时代,拥有更快显示速度的一个众所周知的技巧是展开循环:为屏幕的每个扫描行写操作。它避免了维护循环索引的开销!再加上检测屏幕刷新,它是相当有效的 这是C编译器不会做的事情……(虽然通常你可以在优化速度和大小之间进行选择,但我认为前者使用了一些类似的技巧。 我知道有些人喜欢用汇编语言编写Windows应用程序。他们声称他们更快(很难证明)和更小(确实如此!) 显然,虽然这样做很有趣,但可能会浪费时间(当然,学习目的除外!),特别是对于GUI操作…… 现在,也许有些操作,比如在文件中搜索字符串,可以通过精心编写的汇编代码进行优化

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

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

    GCC已经成为广泛使用的编译器。它的优化通常不是很好。比编写汇编程序的普通程序员好得多,但就实际性能而言,并没有那么好。有些编译器产生的代码简直令人难以置信。所以一般来说,有很多地方你可以进入编译器的输出,调整汇编器的性能,和/或简单地从头重写例程。

    我需要对192位或256位的每次中断进行移位操作,每50微秒发生一次。

    它通过一个固定的映射(硬件限制)实现。使用C语言,制作它只需要大约10微秒。当我把它翻译到Assembler时,考虑到这个映射的特定特性,特定的寄存器缓存,并使用面向位的操作;它只花了不到3.5微秒的时间。

    以下是我个人经历中的几个例子:

    • 例如,许多体系结构(如x86-64、IA-64、DEC Alpha和64位MIPS或PowerPC)支持64 * 64的乘法运算,产生128位的结果。GCC最近添加了一个扩展,提供对这些指令的访问,但在该程序集之前是必需的。在64位cpu上实现RSA之类的程序时,访问这条指令可以带来巨大的差异——有时性能可以提高4倍。

    • 访问特定于cpu的标志。咬了我很多的是进位旗;在做多精度加法时,如果你没有访问CPU进位,就必须比较结果,看看它是否溢出,这需要每个分支多3-5条指令;更糟糕的是,它在数据访问方面非常串行,这降低了现代超标量处理器的性能。当连续处理数千个这样的整数时,能够使用addc是一个巨大的胜利(进位争夺也存在超标量问题,但现代cpu处理得很好)。

    • SIMD。即使是自动向量化编译器也只能处理相对简单的情况,因此如果您想要良好的SIMD性能,不幸的是经常需要直接编写代码。当然,您可以使用intrinsic而不是assembly,但是一旦您到了intrinsic级别,您基本上就是在编写assembly,只是将编译器用作寄存器分配器和(名义上)指令调度器。(我倾向于为SIMD使用intrinsic,因为编译器可以为我生成函数序言和诸如此类的东西,这样我就可以在Linux、OS X和Windows上使用相同的代码,而不必处理像函数调用惯例这样的ABI问题,但除此之外,SSE intrinsic真的不太好——Altivec intrinsic似乎更好,尽管我对它们没有太多的经验)。作为事情的例子(当前)vectorizing编译器不知道,读过< a href = " http://eprint.iacr.org/2009/129 " rel = " noreferrer " > bitslicing AES < / >或< a href = " http://www.randombit.net/bitbashing/programming/forward_error_correction_using_simd.html " rel = " noreferrer " > SIMD纠错< / >——一个可以想象一个编译器,可以分析算法和生成这样的代码,但感觉我像一个聪明的编译器至少30年的时间从现有的(最好的)。

    另一方面,多核机器和分布式系统已经将许多最大的性能优势转向了另一个方向——在汇编中编写内部循环可以获得额外的20%的加速,在多核上运行可以获得300%的加速,在机器集群上运行可以获得10000%的加速。当然,在ML或Scala这样的高级语言中进行高级优化(比如未来、内存等)通常比在C或asm中容易得多,而且通常可以提供更大的性能优势。因此,一如既往,我们需要做出权衡。

    答案很简单……一个知道汇编好吧的人(也就是在他旁边有参考,并且正在利用每一个小处理器缓存和管道特性等)保证能够产生比任何编译器更快的代码。

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

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

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

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

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

    //优化版本 call_function(数据[我],…);

    . //没有那么优化

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

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

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

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

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

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

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

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

    我很惊讶居然没人这么说。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的高阶词,它将成为快8倍,以前的C例程!

    如今,考虑到像英特尔c++这样的编译器对C代码进行了极大的优化,它很难与编译器的输出竞争。

    这个问题有点误导人。答案就在你的帖子里。为特定问题编写程序集解决方案总是可能的,它的执行速度比编译器生成的任何程序集都快。问题是,你需要成为汇编方面的专家,才能克服编译器的限制。 一个有经验的汇编程序员可以用任何HLL编写程序,比一个没有经验的人编写的程序执行得更快。事实上,你编写的汇编程序总能比编译器生成的程序执行得更快

    C语言常常需要做一些从汇编编码员的角度看来不必要的事情,这只是因为C标准这么说。

    例如,整数提升。如果你想在C语言中移动一个char变量,人们通常会期望代码实际上只做一个比特的移动。

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

    实际上您可以构建大规模的项目在一个大模型模式segaments可能被限制在64 kb代码但你可以写很多segaments,人们给反对ASM,因为它是一个古老的语言和我们不需要保留内存了,如果是这样的话为什么我们会包装我们电脑的内存,我唯一能找到的缺陷与基于ASM是或多或少的处理器大多数为intel体系结构编写的程序很可能不会在AMD架构上运行。至于说C比ASM快,没有任何语言比ASM更快,ASM可以做很多C和其他HLL在处理器级不能做的事情。ASM是一种很难学的语言,但一旦你学会了它,没有人能比你翻译得更好。如果你只能看到HLL对你的代码所做的一些事情,并理解它在做什么,你会想为什么越来越多的人不使用ASM,为什么汇编程序不再更新(无论如何对于一般公共使用)。所以没有C比ASM快。即使是经验丰富的c++程序员仍然使用并在ASM中编写代码块,将其添加到c++代码中以提高速度。另外,一些人认为过时或可能不好的语言有时也是一个神话,例如Photoshop是用Pascal/ASM编写的,源代码的第一个版本已经提交给了技术历史博物馆,而paintshop pro仍然是用Python、TCL和ASM编写的……这些“快速和伟大的图像处理器的共同点是ASM,虽然photoshop可能已经升级到delphi现在它仍然是pascal。任何速度问题都来自PASCAL但这是因为我们喜欢程序的样子而不是现在的样子。我想在纯ASM中做一个ps克隆,我一直在工作,它的进展相当顺利。不是代码,解释,不等,rewwrite等等……只需编写代码并完成流程。

    在运行时创建机器代码?

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

    然而,随着时间的推移,这种方法被新的图形硬件淘汰,变得毫无用处。

    今天,我认为大数据(数百万条记录)上的一些操作,如数据透视表、钻孔、实时计算等,都可以用这种方法进行优化。问题是:这样的努力值得吗?

    我已经阅读了所有的答案(超过30个),没有找到一个简单的原因:汇编程序比C更快,如果你已经阅读和练习了Intel®64和IA-32架构优化参考手册所以汇编速度变慢的原因是编写这种慢汇编的人没有阅读优化手册

    在Intel 80286的好日子里,每条指令都以固定的CPU周期数执行。尽管如此,自从奔腾Pro在1995年发布以来,英特尔处理器变成了超标量,利用复杂流水线:乱序执行。寄存器重命名。在此之前,在1993年生产的奔腾号上,有U型和V型管道。因此,奔腾引入了双管道,如果它们彼此不依赖,就可以在一个时钟周期内执行两条简单的指令。然而,这与“乱序执行”相比根本不算什么。注册重命名出现在奔腾专业版。在奔腾Pro中引入的这种方法实际上与现在大多数最新的英特尔处理器相同。

    让我用几句话来解释一下乱序执行。最快的代码是指令不依赖于之前的结果,例如,你应该总是清除整个寄存器(通过movzx),以消除对你正在使用的寄存器之前值的依赖,因此它们可以由CPU内部重命名,以允许指令并行或以不同的顺序执行。或者,在某些处理器上,可能存在错误的依赖关系,这也可能会降低速度,比如对奔腾4 inc/dec的错误依赖,所以你可能希望使用add eax, 1代替或inc eax来删除对先前状态标志的依赖关系。

    你可以阅读更多的乱序执行&如果时间允许,注册重命名。因特网上有大量的信息。

    还有许多其他基本问题,如分支预测、加载和存储单元的数量、执行微操作的门的数量、内存缓存一致性协议等,但最重要的是要考虑乱序执行。 大多数人根本没有意识到乱序执行。因此,他们编写像80286这样的汇编程序,期望它们的指令将花费固定的时间执行,而不管上下文如何。同时,C编译器知道乱序执行并正确地生成代码。这就是为什么这些不知情的人写的代码比较慢,但如果你变得有知识,你的代码就会更快

    除了乱序执行之外,还有很多优化技巧和技巧。请阅读上面提到的优化手册:-)

    然而,当涉及到优化时,汇编语言有它自己的缺点。根据Peter Cordes的说法(见下面的评论),编译器所做的一些优化对于手工编写的汇编中的大型代码库是不可维护的。例如,假设您使用汇编语言编写。在这种情况下,当内联函数(程序集宏)内联到调用它的函数中并使用一些参数作为常量时,您需要完全更改它。与此同时,C编译器使它的工作变得简单得多——以不同的方式将相同的代码内联到不同的调用站点。使用汇编宏所能做的事情是有限的。因此,为了获得同样的好处,您必须在每个地方手动优化相同的逻辑,以匹配您拥有的常量和可用寄存器。

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

    在历史上插话。

    当我还年轻的时候(20世纪70年代),根据我的经验,汇编是很重要的,更重要的是代码的大小,而不是代码的速度。

    如果一个高级语言的模块是1300字节的代码,但该模块的汇编版本是300字节,那么当您试图将应用程序装入16K或32K的内存时,这1K字节就非常重要。

    那时候编译器还不是很好。

    在老式的Fortran中

    X = (Y - Z)
    IF (X .LT. 0) THEN
    ... do something
    ENDIF
    
    当时的编译器在X上执行了一个SUBTRACT指令,然后是一个TEST指令。 在汇编程序中,只需检查减法后的条件代码(LT零,零,GT零)。

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

    我认为理解编译器在做什么仍然很重要。 当你用高级语言编写代码时,你应该了解什么允许或阻止编译器执行循环展开

    当编译器执行“类似分支”的操作时,使用管道内衬和包含条件的前瞻计算。

    当执行高级语言不允许的事情时,仍然需要汇编程序,比如读取或写入处理器特定的寄存器。

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