首页 文章

什么时候装配比C快?

提问于
浏览
422

了解汇编程序的一个原因是,有时可以使用它来编写比在更高级语言中编写代码更高效的代码,特别是C.但是,我并不完全错误,汇编程序可以使用 actually 来生成更高性能代码的情况非常罕见,需要专业知识和汇编经验 .

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

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

30 回答

  • 10

    这是一个真实世界的例子:旧编译器的固定点倍增 .

    这些难以预测精确度损失) . 即在整个范围内均匀的绝对精度,而不是接近均匀的相对精度( 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编译器有机会理解's going on. This allows the code to be inlined, register allocated, common subexpression elimination and constant propagation can be done as well. You'l的方式来重写函数,从而获得比手写汇编程序代码更大的性能提升 .

    供参考: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版本的错误代码 . )


    Modern CPUs can do things C doesn't have operators for at all, like popcnt or bit-scan to find the first or last set bit . (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 <immintrin.h>的硬件 .

    或者在C中,分配给 std::bitset<32> 并使用 .count() . (这种语言已经找到了一种通过标准库可移植地展示popcount的优化实现的方法,其方式总是会编译成某种东西正确,并可以利用目标支持的任何东西 . )另见https://en.wikipedia.org/wiki/Hamming_weight#Language_support .

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


    内在函数或手写asm的另一个主要领域是使用SIMD指令的手动矢量化 . 编译器对于像 dst[i] += src[i] * 10.0; 这样的简单循环并不错,但是通常做得很糟糕或者不太可能得到像编译器从标量代码自动生成的How to implement atoi using SIMD?这样的东西 .

  • 5

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

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

    我刚刚有一个优化编译器,相同的代码在<5秒内旋转了图形 . 我查看了编译器生成的汇编代码,从我看到的那个决定那里,然后我编写汇编程序的日子结束了 .

  • 245

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

    它通过固定的 Map (硬件约束)发生 . 使用C,需要大约10微秒 . 当我把它翻译成Assembler时,考虑到这个 Map 的特定功能,特定的寄存器缓存,以及使用面向比特的操作;执行时间不到3.5微秒 .

  • 5

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

  • 4

    几乎在编译器看到浮点代码的任何时候,手写版本都会更快 . 主要原因是编译器无法执行任何强大的优化 . 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更快!) . 修复并更新了结果 .

  • 10

    在不提供任何具体示例或探查器证据的情况下,当您比编译器了解更多时,您可以编写比编译器更好的汇编程序 .

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

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

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

  • 4

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

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

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

    for (int y=0; y < imageHeight; y++) {
        for (int x=0; x < imageWidth; x++) {
           // do something
        }
    }
    

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

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

    仅在使用某些特殊用途指令集时,编译器才支持 .

    最大化计算具有多个流水线和预测性分支的现代CPU的强大功能,您需要以一种方式构建装配程序,使得a)人类几乎不可能编写b)更难以维护 .

    此外,更好的算法,数据结构和内存管理将比组装中的微优化提供至少一个数量级的性能 .

  • 12

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

    • 定点乘法:两个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标准并没有给您任何表达方式 .

    • 某些位移操作(旋转/进位):
    // 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,那么您将以表示多项式的位模式进行异或运算 .

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

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

  • 9

    简短的回答?有时 .

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

    C编程语言 - 将汇编语言的灵活性与汇编语言的强大功能相结合的语言 .

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

    值得注意的是,汇编语言只是在您编写它时运行 . 然而,在C和它生成的汇编语言之间有一个编译器,这非常重要,因为 how fast your C code is has an awful lot to do with how good your compiler is.

    当gcc出现在现场时,其中一个让它如此受欢迎的事情是,它通常比带有许多商业UNIX风格的C编译器好得多 . 不仅ANSI C(没有这个K&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

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

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

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

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

  • 5

    简单的答案......一个熟悉组装的人(也就是他身边的参考,并且正在利用每个小处理器缓存和管道功能等)保证能够产生比任何编译器快得多的代码 .

    然而,这些天的差异在典型的应用中并不重要 .

  • 10

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

  • 40

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

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

    我还想补充一下,有很多中间地带你可以在C编译中编码并检查生成的汇编,然后改变你的C代码或调整和维护为汇编 .

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

    • 删除现有功能

    • 减少部分或全部现有功能的大小可能会以性能为代价 .

    • 倡导转向更大的芯片,具有更高的成本,更高的功耗和更大的外形尺寸 .

    我想作为商业开发人员添加一个或多种语言,平台,应用程序类型的商业开发人员,我从来没有想过要深入编写程序集 . 我一直都很欣赏我所获得的知识 . 有时调试到它 .

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

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

    • 致力于低级操作系统功能

    • 使用编译器 .

    • 在极其有限的芯片,嵌入式系统等上工作

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

    大卫 .

  • 52

    我很惊讶没人说这个 . 如果用汇编编写, 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倍!

  • 13

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

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

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

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

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

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

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

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

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

  • 23

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

  • 15

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

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

    然后,处理器通常会有一些深奥的指令,这些指令太专门于编译器而烦恼,但有时汇编程序员可以很好地利用它们 . 以XLAT指令为例 . 如果您需要在循环中进行表查找并且表限制为256字节,那真的很棒!

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

  • 124

    我的经验中的一些例子:

    • 访问无法从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性能,那么's unfortunately often necessary to write the code directly. Of course you can use intrinsics instead of assembly but once you'在内在水平上你很好 - 尽管我不知道Altivec看起来更好,但是我不知道bitslicing AESSIMD error correction可以想象一个编译器可以分析算法并生成这样的代码,但我觉得这样一个智能编译器距离现有(至多)至少30年 .

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

  • 15

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

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

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

  • 4

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

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

  • 59

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

  • 38

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

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

    它们通常还涉及使用针对这些类型的操作调整的CPU特定芯片组扩展(MME / MMX / SSE /无论如何) .

  • 4

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

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

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

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

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

  • 43

    其中一个更着名的装配片段来自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设置所针对的实际体系结构 .

  • 4

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

  • 4

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

  • 7

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

  • 4

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

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

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

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

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

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

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

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

    • 内存很慢

    • 缓存很快

    • 尝试更好地使用缓存

    • 你多久会错过一次?你有延迟补偿策略吗?

    • 您可以针对单个缓存未命中执行10-100个ALU / FPU / SSE指令

    • 应用程序架构很重要..

    • ..但它在架构中确实是't help when the problem isn't

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

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

    这个话题有点死路一条;对于大多数人而言,这是无关紧要的,其余的,他们知道他们在做什么 .

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

  • 6

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

相关问题