当前位置:  开发笔记 > 编程语言 > 正文

什么时候装配比C快?

如何解决《什么时候装配比C快?》经验,为你挑选了24个好方法。

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

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

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



1> Nils Pipenbr..:

这是一个真实世界的例子:旧编译器上的定点乘法.

这些不仅可以在没有浮点的设备上得心应用,它们在精度方面也会发光,因为它们可以提供32位精度并且具有可预测的误差(浮点数仅为23位,并且难以预测精度损失).即在整个范围内均匀的绝对精度,而不是接近均匀的相对精度(float).


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

获得64位整数乘法的高分:使用uint64_t32x32 => 64位乘法的可移植版本 无法在64位CPU上进行优化,因此您需要内部函数或__int12864位系统上的高效代码.

Windows 32位上的_umul128:当将32位整数乘以64时,MSVC并不总是做得很好,因此内在函数帮助很大.


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版本可以在没有内在函数的情况下执行此操作.)

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


现代的CPU可以做的事情C没有运营商一样,popcnt还是位扫描,找到第一个或最后一组位.(POSIX有一个ffs()函数,但它的语义与x86 bsf/ 不匹配bsr.请参阅https://en.wikipedia.org/wiki/Find_first_set).

有些编译器有时可以识别一个循环来计算整数中的设置位数并将其编译成一条popcnt指令(如果在编译时启用),但__builtin_popcnt在GNU C中使用它会更可靠,如果你只是在x86上则更可靠使用SSE4.2定位硬件:_mm_popcnt_u32来自.

或者在C++中,分配给a std::bitset<32>并使用.count().(这种情况下,语言已经找到了一种方法,可以通过标准库轻松暴露popcount的优化实现,以一种始终编译为正确的方式,并且可以利用目标支持的任何内容.)另请参阅https ://en.wikipedia.org/wiki/Hamming_weight#Language_support.

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


内在函数或手写asm的另一个主要领域是使用SIMD指令的手动矢量化.编译器对于简单的循环dst[i] += src[i] * 10.0;也不错,但是当事情变得更复杂时,编程通常会很糟糕或根本不会自动矢量化.例如,您不太可能得到任何类似如何使用SIMD实现atoi的内容?由编译器从标量代码自动生成.


嗨Slacker,我认为你从来没有必要在时间关键代码上工作......内联汇编可以产生巨大的差异.同样对于编译器而言,内在函数与C中的常规算法相同.这是内在函数中的要点.它们允许您使用架构功能而无需处理缺点.
@slacker实际上,这里的代码非常易读:内联代码执行一个独特的操作,读取方法签名时立即明白.当使用模糊指令时,代码在可读性方面仅缓慢丢失.这里重要的是我们有一种方法只能进行一次清晰可识别的操作,而这实际上是生成可读代码这些原子函数的最佳方法.顺便说一句,这不是一个不起眼的小评论,如/*(a*b)>> 16*/不能立即解释它.
怎么样{x = c%d; y = c/d;},编译器是否聪明到能够成为一个div还是idiv?
@slacker:"正确"的解决方案是让语言提供一种方式来请求你想做什么.编写代码,其天真的解释将非常低效,希望编译器执行特定的优化是切换编译器版本时烧毁的好方法.在人们知道编译器没有的操作数大小的情况下尤其如此(例如,在知道ulong_dividend小于2 ^ 32的情况下,编译器可以优化`uint_quotient = ulong_dividend/uint_divisor` uint_divisor?)
公平地说,至少在今天,这是一个可怜的例子。即使语言没有直接提供,C编译器也能够执行32x32-> 64乘法:他们认识到,当您将32位参数强制转换为64位然后乘以它们时,它不需要做一个完整的64位乘法,但是32x32-> 64会很好。我检查了一下,[最新版本的clang,gcc和MSVC都正确了](https://godbolt.org/g/B3tEMh)。这不是什么新鲜事-我记得十年前曾看过编译器输出并注意到这一点。
实际上,一个好的编译器会从第一个函数产生最佳代码.使用内在函数或内联汇编*来隐藏源代码,绝对没有任何好处*不是最好的事情.

2> lilburne..:

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

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

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


优化编译器是否编译了原始程序或您的版本?
他可能看过他不能写的代码:/
只是想知道:图像是否为每像素1位格式?
是的,这是一个单色系统,特别是它是Atari ST上的单色图像块.

3> Skizz..:

几乎在编译器看到浮点代码的任何时候,手写版本都会更快.主要原因是编译器无法执行任何强大的优化.有关该主题的讨论,请参阅MSDN中的这篇文章.这是一个示例,其中汇编版本的速度是C版本的两倍(使用VS2K5编译):

#include "stdafx.h"
#include 

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  (rand ()) / static_cast  (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更快!).修复并更新了结果.


或者在GCC中,你可以通过使用标志`-ffast-math`解开编译器的浮点优化问题(只要你保证不对无穷大或NaN做任何事情).它们有一个优化级别`-Ofast`,它当前等同于`-O3 -ffast-math`,但将来可能会包含更多的优化,这些优化会导致在极端情况下生成错误的代码(例如依赖于IEEE的代码) NaN的).
@Praxeolitic:FP add是可交换的(`a + b == b + a`),但不是关联的(操作的重新排序,因此中间体的舍入是不同的).re:这段代码:我不认为取消注释x87和`loop`指令是一个非常棒的快速asm演示.由于FP延迟,`loop`显然实际上不是瓶颈.我不确定他是否正在管理FP操作; x87很难让人类阅读.最后两个`fstp结果'insn显然不是最佳的.使用非商店可以更好地完成从堆栈中弹出额外的结果.就像`fstp st(0)`IIRC.
你尝试过SSE数学吗?性能是MS在x86_64中完全放弃x87和在x86中放弃80位长双倍的原因之一

4> Liedman..:

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

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

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

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



5> plinth..:

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

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

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

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

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

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



6> Nir..:

只有在使用某些专用指令集时,编译器才支持.

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

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


@Matt:手写的ASM通常在一些微型CPU上运行得更好*EE的工作具有糟糕的供应商编译器支持.
"只有在使用一些专用指令集时才会" 您可能以前从未编写过一些手动优化的asm代码.对您正在开发的体系结构有一定的了解,这使您有机会生成比编译器更好的代码(大小和速度).显然,正如@mghie评论的那样,你总是开始编写你可以带来的最好的算法问题.即使对于非常好的编译器,您也必须以一种将编译器引导到最佳编译代码的方式编写C代码.否则,生成的代码将是次优的.
+1,尽管最后一句话并不真正属于这个讨论 - 人们会认为汇编程序只有在实现了所有可能的算法改进之后才能发挥作用.
+1表示编译器(尤其是JIT)可以比人类做更好的*工作,*if*它们针对运行的硬件进行了优化.
@ysap - 在实际使用中的实际计算机(不是微不足道的嵌入式芯片)上,"最佳"代码不会更快,因为对于任何大型数据集,性能都会受到内存访问和页面错误的限制(如果你没有一个大的数据集,这将是快速的任何一种方式并且没有必要优化它 - 那些日子我主要在C#(甚至不是c)工作,并且压缩内存管理器的性能提升 - 加权垃圾收集,压缩和JIT编译的开销.

7> Jason S..:

虽然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-使用指针上的自动递增位右移.

    再举一个例子,有一些线性反馈移位寄存器(LFSR)在汇编中优雅地执行:取一块N位(8,16,32,64,128等),将整个事物右移1(见上文)算法),然后如果结果进位是1,那么你在表示多项式的位模式中进行异或运算.

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

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



8> cletus..:

简短的回答?有时.

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

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

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

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

当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往往在那里占主导地位)也)等等.或者只是为了踢(有点自虐).


"......因为C没有主观标准." 你的意思是*客观*.

9> Aaron Digull..:

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

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

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

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



10> David Waters..:

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

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

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

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

    删除现有功能

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

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

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

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

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

致力于低级操作系统功能

在编译器上工作.

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

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

大卫.


+1用于考虑微芯片上的嵌入式应用.这里太多的软件工程师要么不考虑嵌入式,要么认为这意味着智能手机(32位,MB RAM,MB闪存).

11> BlackBear..:

我很惊讶没人说这个.该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倍!


这与http://strchr.nfshost.com/optimized_strlen_function中的相比如何?

12> Mehrdad Afsh..:

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


不过,Mehrdad是对的.获得正确的SSE对于编译器来说是非常困难的,甚至在很明显的(对于人类而言)情况下,大多数编译器都不会使用它.
对于许多这种情况,您可以使用SSE内在而不是汇编.这将使您的代码更具可移植性(gcc visual c ++,64bit,32bit等),您无需进行寄存器分配.

13> Mike Dunlave..:

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

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

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

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

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

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

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

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

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


+1:"你可以偏离调用约定".C/C++编译器倾向于返回多个值.它们经常使用sret形式,其中调用者堆栈为结构分配一个连续的块并传递对它的引用以供被调用者填充它.在寄存器中返回多个值要快几倍.

14> Jack Lloyd..:

我的经验中的一些例子:

访问无法从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似乎更好.作为(当天)矢量化编译器无法弄清楚的事情的例子,请阅读有关比特序列AES或SIMD纠错的信息 - 可以想象一个编译器可以分析算法并生成这样的代码,但我觉得这样的智能编译器距现有(至多)至少30年.

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


@Dennis这就是为什么我写'当然你可以使用内在函数而不是汇编但是一旦你处于内在函数级别,你基本上就是编写汇编,只需使用编译器作为寄存器分配器和(名义上)指令调度器.

15> Dan Byström..:

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

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

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

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


配置文件定向优化为编译器提供有关循环使用频率的信息.

16> mfro..:

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

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

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



17> sharptooth..:

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

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


"所以没有必要用汇编语言来获得最快的代码"嗯,我没有看到编译器在任何情况下都做了最好的事情并不是微不足道的.几乎在所有情况下,经验丰富的人都可以比编译器做得更好.所以,写入汇编程序以获得"有史以来最快的代码"是绝对必要的.

18> Maxim Masiut..:

我已经阅读了所有答案(超过30个),却找不到简单的原因:如果您已经阅读并练习了《英特尔®64和IA-32架构优化参考手册》,那么汇编程序的运行速度将比C快。更慢的是写这样慢的汇编的人没有看过“优化手册”

在英特尔80286的美好时光中,每条指令均以固定的CPU周期计数执行,但是自1995年发布的奔腾Pro以来,英特尔处理器就利用了复杂流水线:乱序执行和寄存器重命名,成为了超标量。在此之前,在1993年生产的Pentium上有U和V管线:双管线可以在不相互依赖的情况下在一个时钟周期执行两条简单指令。但这与Pentium Pro中出现的乱序执行和寄存器重命名没有什么可比的,如今几乎保持不变。

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

如果时间允许,您可以阅读有关乱序执行和注册重命名的更多信息,Internet上有大量可用信息。

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

大多数人根本不了解乱序执行,因此他们像80286一样编写汇编程序,希望他们的指令将花费固定的时间来执行,而不管上下文如何。而C编译器知道乱序执行并正确生成代码。这就是为什么这种不了解的人的代码速度较慢的原因,但是如果您意识到这一点,您的代码就会更快。



19> Doug T...:

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



20> ReinstateMon..:

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

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

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



21> James Brooks..:

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



22> SurDin..:

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

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



23> 小智..:

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



24> L̲̳o̲̳̳n̲̳̳g..:

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

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

推荐阅读
勤奋的瞌睡猪_715
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有