提问



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


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


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

最佳参考


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


这些只是在没有浮点的设备上才会派上用场,它们在精确度上会发光,因为它们可以提供32位精度和可预测的误差(浮点数只有23位且预测精度损失更难)。即在整个范围内均匀绝对精度,而不是接近均匀的相对精度(float)。





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



  • 获得64位整数乘法的高位:使用uint64_t进行32x32 => 64位乘法的便携式版本无法在64位CPU上进行优化,因此您需要内在函数或__int128在64位系统上实现高效代码。

  • Windows上的
  • _umul128 32位:MSVC在将32位整数乘以64时并不总是做得很好,所以内在函数帮了很多。






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位)的源+ asm和ARM。 (不幸的是,它没有足够的编译器来生成简单的纯C版本的错误代码。)[32]





现代CPU可以做的事情C没有的操作符>,如popcnt或bit-scan来查找第一个或最后一个设置位。(POSIX)有一个ffs()函数,但它的语义不匹配x86 bsf/bsr。见https://en.wikipedia.org/wiki/Find_first_set).[33]


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


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


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





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

其它参考1


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


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


我刚刚获得了一个优化编译器,并且相同的代码在<中旋转了图形。>

其它参考2


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


#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更快!)。修复并更新了结果。

其它参考3


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


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


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


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

其它参考4


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



  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


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


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


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

其它参考6


虽然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位右移。


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



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


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

其它参考7


简短的回答?有时。


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



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



这很有趣,因为它是真的: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往往在那里占主导地位)也)等等。或者只是为了踢(有点自虐)。

其它参考8


第一点不是答案。

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


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


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


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



  1. 删除现有功能

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

  3. 倡导更高成本,更高功耗和更大外形尺寸的更大芯片。


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


我知道我已经回答了我为什么要亚博2018平台汇编程序这个问题,但是我认为这是一个更重要的问题,那么它何时会更快。


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


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

  • 使用编译器。

  • 使用极其有限的芯片,嵌入式系统等




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



大卫。

其它参考9


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


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


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


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

其它参考10


我很惊讶没人说这个。如果用汇编语言写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


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

其它参考12


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



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

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

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



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


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


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


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


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

其它参考13


我的经验中的一些例子:



  • 访问无法从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年。[39] [40]



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

其它参考14


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


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


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

其它参考15


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


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

其它参考16


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


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


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


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

其它参考17


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

其它参考18


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


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


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

其它参考19


LInux汇编howto,问这个问题并给出使用汇编的优缺点。[42]

其它参考20


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


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

其它参考21


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


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

其它参考22


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


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


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


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


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

其它参考23


其中一个比较着名的装配片段来自迈克尔·阿布拉什的纹理映射循环(这里详细解释):[43]


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设置所针对的实际体系结构。

其它参考24


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

其它参考25


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

其它参考26


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

其它参考27


Longpoke,只有一个限制:时间。当你没有足够的资源来优化代码的每一次更改并花时间分配寄存器,优化少量溢出而不是什么时,编译器将每次都获胜。你对代码进行修改,重新编译和测量。必要时重复。


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


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


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


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


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


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


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



  • 内存很慢

  • 缓存很快

  • 尝试使用缓存更好

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

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

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

  • ..但是当问题不在架构中时,它确实没有帮助



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


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


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


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

其它参考28


如何在运行时创建机器代码?


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


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


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

其它参考29


我已经阅读了所有的答案(超过30个),并没有找到一个简单的原因:如果你已经阅读并实践了英特尔?64和IA-32架构优化参考手册,那么汇编程序比C更快,那么原因装配速度可能较慢的原因是编写这种较慢装配的人没有阅读优化手册。[45]


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


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


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


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


大多数人根本不了解乱序执行,所以他们编写像80286这样的汇编程序,期望他们的指令在不考虑上下文的??情况下需要一段固定的时间来执行;而C编译器知道无序执行并正确生成代码。这就是为什么这些不知情的人的代码速度较慢,但??如果你意识到,你的代码会更快。