内联汇编语言是否比本机C ++代码慢?


183

我试图比较内联汇编语言和C ++代码的性能,所以我写了一个函数,将两个大小为2000的数组相加100000次。这是代码:

#define TIMES 100000
void calcuC(int *x,int *y,int length)
{
    for(int i = 0; i < TIMES; i++)
    {
        for(int j = 0; j < length; j++)
            x[j] += y[j];
    }
}


void calcuAsm(int *x,int *y,int lengthOfArray)
{
    __asm
    {
        mov edi,TIMES
        start:
        mov esi,0
        mov ecx,lengthOfArray
        label:
        mov edx,x
        push edx
        mov eax,DWORD PTR [edx + esi*4]
        mov edx,y
        mov ebx,DWORD PTR [edx + esi*4]
        add eax,ebx
        pop edx
        mov [edx + esi*4],eax
        inc esi
        loop label
        dec edi
        cmp edi,0
        jnz start
    };
}

这里是main()

int main() {
    bool errorOccured = false;
    setbuf(stdout,NULL);
    int *xC,*xAsm,*yC,*yAsm;
    xC = new int[2000];
    xAsm = new int[2000];
    yC = new int[2000];
    yAsm = new int[2000];
    for(int i = 0; i < 2000; i++)
    {
        xC[i] = 0;
        xAsm[i] = 0;
        yC[i] = i;
        yAsm[i] = i;
    }
    time_t start = clock();
    calcuC(xC,yC,2000);

    //    calcuAsm(xAsm,yAsm,2000);
    //    for(int i = 0; i < 2000; i++)
    //    {
    //        if(xC[i] != xAsm[i])
    //        {
    //            cout<<"xC["<<i<<"]="<<xC[i]<<" "<<"xAsm["<<i<<"]="<<xAsm[i]<<endl;
    //            errorOccured = true;
    //            break;
    //        }
    //    }
    //    if(errorOccured)
    //        cout<<"Error occurs!"<<endl;
    //    else
    //        cout<<"Works fine!"<<endl;

    time_t end = clock();

    //    cout<<"time = "<<(float)(end - start) / CLOCKS_PER_SEC<<"\n";

    cout<<"time = "<<end - start<<endl;
    return 0;
}

然后我运行该程序五次以获取处理器的周期,这可以看作是时间。每次我仅调用上述功能之一。

结果到了。

装配版本功能:

Debug   Release
---------------
732        668
733        680
659        672
667        675
684        694
Average:   677

C ++版本的功能:

Debug     Release
-----------------
1068      168
 999      166
1072      231
1002      166
1114      183
Average:  182

发行模式下的C ++代码几乎比汇编代码快3.7倍。为什么?

我想我编写的汇编代码不如GCC生成的汇编代码有效。对于像我这样的普通程序员来说,编写代码的速度要比编译器生成的对手要难得多,这是否意味着我不应该相信自己动手编写的汇编语言的性能,专注于C ++并忘记汇编语言?


29
差不多了 手工编码的汇编在某些情况下是合适的,但是必须注意确保汇编版本确实比使用高级语言所能实现的版本要快。
Magnus Hoff'3

161
您可能会发现研究编译器生成的代码有启发性,并尝试理解为什么它比汇编版本要快。
Paul R

34
是的,看起来编译器比您更擅长于编写asm。现代编译器确实相当不错。
David Heffernan'3

20
您是否看过GCC生产的装配体?它可能的GCC使用了MMX指令。您的函数是非常并行的-您可能使用N个处理器以1 / N的时间计算总和。尝试一个没有并行化希望的函数。
克里斯

11
嗯,我本来希望一个好的编译器可以快100000倍...
PlasmaHH 2012年

Answers:


261

是的,大多数时候。

首先,您从错误的假设开始,即低级语言(在这种情况下为汇编)将始终比高级语言(在此情况下为C ++和C)生成更快的代码。这不是真的。C代码是否总是比Java代码快?否,因为还有另一个变量:程序员。编写代码和了解体系结构细节的方式会极大地影响性能(如本例所示)。

总是可以提供一个示例,其中手工汇编代码比编译代码要好,但是通常这只是一个虚构的示例,或者是一个例程,而不是包含500.000多行C ++代码的真实程序)。我认为编译器将产生95%的更好的汇编代码,有时,只有在极少数情况下,您可能需要为少数,简短,高度使用,对性能有严格要求的例程编写汇编代码,或者在您必须访问自己喜欢的高级语言的功能时不暴露。您是否想要这种复杂性?在SO上阅读这个很棒的答案

为什么这个?

首先,因为编译器可以执行我们甚至无法想象的优化(请参阅此简短列表),并且它们将在几秒钟内可能需要几天的时间)完成优化。

在汇编语言中编写代码时,必须使用定义明确的调用接口来创建定义明确的函数。但是,它们可以考虑整个程序优化过程间优化,例如寄存器分配常量传播公共子表达式消除指令调度以及其他复杂的,不明显的优化(例如,Polytope模型)。在RISC架构上,很多年前,人们不再担心这一点(例如,很难手动调整指令调度),并且现代CISC CPU的流水线非常长 太。

对于某些复杂的微控制器,甚至系统库也是用C而不是汇编语言编写的,因为它们的编译器会生成更好(且易于维护)的最终代码。

编译器有时可以自行自动使用一些MMX / SIMDx指令,如果不使用它们,则根本无法比较(其他答案已经很好地检查了汇编代码)。仅针对循环,这是循环优化简短列表,该循环优化通常由编译器检查(如果您已确定C#程序的时间表,您是否可以自己完成?)如果您在汇编中编写内容,认为您必须至少考虑一些简单的优化。数组的教科书示例是展开循环(在编译时已知其大小)。这样做并再次运行测试。

如今,由于其他原因而需要使用汇编语言也确实非常普遍:大量不同的CPU。您是否想全力支持他们?每个都有特定的微体系结构和一些特定的指令集。它们具有不同数量的功能单元,应安排组装说明以使它们都保持忙碌状态。如果您使用C编写,则可以使用PGO,但是在汇编中,您将需要对特定体系结构有充分的了解(并重新考虑和重做其他体系结构的所有内容)。对于小型任务,编译器通常会做得更好,而对于复杂任务,通常工作是不会付清的(并且无论如何,编译器可能会做得更好

如果您坐下来看看代码,您可能会发现重新设计算法比转换为汇编要多(请阅读SO上的这篇很棒的文章),这里有一些高级的优化(以及对编译器的提示),您可以在诉诸汇编语言之前有效地进行申请。可能值得一提的是,经常使用内在函数,您将获得所需的性能提升,并且编译器仍将能够执行其大多数优化。

所有这一切说,即使你能产生5〜10倍的速度汇编代码,你应该问你的客户,如果他们愿意支付一个星期的时间,或者买了50 $更快的CPU。我们大多数人根本不需要极端优化(尤其是在LOB应用程序中)。


9
当然不是。我认为95%的人在99%的时间内表现更好。有时是因为仅仅是出于成本(由于复杂的数学运算)或时间花费(然后又是成本高昂)。有时是因为我们只是忘记了优化
而已

62
@ ja72-不,编写代码不是更好。更好地优化代码。
Mike Baranczak 2012年

14
在您真正考虑之前,这是违反直觉的。以同样的方式,基于VM的机器开始进行运行时优化,而编译器根本就没有这些信息。
Bill K

6
@ M28:编译器可以使用相同的指令。当然,他们为二进制大小付费(因为如果不支持这些指令,则必须提供备用路径)。而且,在大多数情况下,无论如何,要添加的“新指令”都是SMID指令,VM和编译器在使用时都非常恐怖。虚拟机为此功能付费,因为它们必须在启动时编译代码。
Billy ONeal 2012年

9
@BillK:PGO对编译器执行相同的操作。
Billy ONeal 2012年

194

您的汇编代码不是很理想,可能会得到改进:

  • 您正在内部循环中推送并弹出一个寄存器(EDX)。这应该移出循环。
  • 您可以在循环的每次迭代中重新加载数组指针。这应该移出循环。
  • 您使用了该loop指令,该指令在大多数现代CPU上都非常缓慢(可能是因为使用了古老的汇编手册*)
  • 您没有利用手动循环展开的优势。
  • 您不使用可用的SIMD指令。

因此,除非您极大地提高了有关汇编程序的技能,否则编写汇编代码以提高性能没有意义。

*当然,我不知道您是否真的loop从一本古老的汇编书中得到了指导。但是您几乎在现实世界的代码中几乎看不到它,因为那里的每个编译器都足够聪明,不会发出信号loop,您只会在恕我直言的糟糕和过时的书中看到它。


loop如果您针对大小进行优化,则编译器可能仍会发出(以及许多“已弃用”的指令)
phuclv 2014年

1
@phuclv很好,但是最初的问题是速度,而不是大小。
IGR94

60

甚至在研究汇编之前,还存在较高级别的代码转换。

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
  for (int i = 0; i < TIMES; i++) {
    for (int j = 0; j < length; j++) {
      x[j] += y[j];
    }
  }
}

可以通过Loop Rotation转换为:

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      for (int i = 0; i < TIMES; ++i) {
        x[j] += y[j];
      }
    }
}

就内存局部性而言,这要好得多。

这可以进一步优化,执行a += bX次等效于这样做a += X * b,我们得到:

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      x[j] += TIMES * y[j];
    }
}

但是,似乎我最喜欢的优化器(LLVM)没有执行此转换。

[编辑]我发现,如果我们restrictx和拥有限定词,则将执行转换y。实际上,没有此限制,x[j]并且y[j]可能混淆到使该转换错误的同一位置。[结束编辑]

无论如何,我认为是优化的C版本。已经很简单了。基于此,这是我对ASM的破解(我让Clang生成了它,对此我毫无用处):

calcuAsm:                               # @calcuAsm
.Ltmp0:
    .cfi_startproc
# BB#0:
    testl   %edx, %edx
    jle .LBB0_2
    .align  16, 0x90
.LBB0_1:                                # %.lr.ph
                                        # =>This Inner Loop Header: Depth=1
    imull   $100000, (%rsi), %eax   # imm = 0x186A0
    addl    %eax, (%rdi)
    addq    $4, %rsi
    addq    $4, %rdi
    decl    %edx
    jne .LBB0_1
.LBB0_2:                                # %._crit_edge
    ret
.Ltmp1:
    .size   calcuAsm, .Ltmp1-calcuAsm
.Ltmp2:
    .cfi_endproc

恐怕我不明白所有这些指令的来源,但是您总是可以从中获得乐趣并尝试看看它们之间的比较...但是我仍然会在代码中使用优化的C版本而不是汇编版本,更便携。


谢谢您的回答。那么,当我选了一个名为“编译器原理”的类时,我感到有点困惑,我知道编译器将通过多种方式优化我们的代码。这是否意味着我们需要手动优化代码?我们能比编译器做得更好吗?这就是总是使我困惑的问题。
user957121 2012年

2
@ user957121:当我们了解更多信息时,我们可以对其进行更好的优化。特别是在这里,阻碍编译器的是和之间可能的别名。也就是说,编译器不能确保所有的,我们有。如果存在重叠,则无法进行优化。C语言引入了关键字来告诉编译器两个指针不能别名,但是它不适用于数组,因为即使它们不完全是别名,它们仍然可以重叠。xyi,j[0, length)x + i != y + jrestrict
Matthieu M. 2012年

当前的GCC和Clang自动矢量化(如果忽略则检查非重叠后__restrict)。SSE2是x86-64的基准,并且通过改组SSE2可以一次执行2x 32位乘法(产生64位乘积,因此改组可以将结果放在一起)。godbolt.org/z/r7F_uo。(对于pmulld:压缩32x32 => 32位乘法,需要SSE4.1 )。GCC有一个巧妙的技巧,可以将常数整数乘法器转换为移位/加法(和/或减法),这对设置了几位的乘法器非常有用。Clang的重排代码将成为英特尔CPU的重排吞吐量的瓶颈。
彼得·科德斯,

41

简短的回答:是的。

长答案:是的,除非您真的知道自己在做什么,并且有理由这样做。


3
然后只有当您运行了诸如vtune之类的汇编级概要分析工具来处理intel芯片以查看可能需要改进的地方时
Mark Mullin

1
从技术上讲,这可以回答问题,但也完全没有用。我的-1。
纳文

2
很长的答案:“是的,除非您想在使用新CPU时更改整个代码。选择最佳算法,但让编译器进行优化”
Tommylee2k,

35

我已经修复了我的asm代码:

  __asm
{   
    mov ebx,TIMES
 start:
    mov ecx,lengthOfArray
    mov esi,x
    shr ecx,1
    mov edi,y
label:
    movq mm0,QWORD PTR[esi]
    paddd mm0,QWORD PTR[edi]
    add edi,8
    movq QWORD PTR[esi],mm0
    add esi,8
    dec ecx 
    jnz label
    dec ebx
    jnz start
};

发布版本的结果:

 Function of assembly version: 81
 Function of C++ version: 161

发行模式下的汇编代码几乎比C ++快2倍。


18
现在,如果您开始使用SSE而不是MMX(寄存器名称xmm0代替mm0),您将获得另一个提速两倍;-)
Gunther Piez,2012年

8
我改变了,组装版本为41。它快了4倍:)
sasha 2012年

3
如果使用所有xmm寄存器,也最多可以提高5%
sasha 2012年

7
现在,如果您考虑一下实际花费的时间:组装,大约需要10个小时左右?C ++,我想几分钟?除非有关键性能的代码,否则这里肯定有赢家。
Calimo

1
一个好的编译器就已经自动向量化与paddd xmm(检查之间的重叠后xy,因为你没有使用int *__restrict x)。例如,gcc这样做:godbolt.org/z/c2JG0-。或在内联到后main,它不需要检查重叠,因为它可以看到分配并证明它们是不重叠的。(而且,在某些x86-64实现中也将假定16字节对齐,对于独立定义而言并非如此。)如果使用进行编译gcc -O3 -march=native,则可以获得256位或512位。向量化。
彼得·科德斯

24

这是否意味着我不应该相信自己手写的汇编语言的性能

是的,这就是它的意思,对每个人都是如此语言都适用。如果您不知道如何用X语言编写高效的代码,那么您就不应该信任使用X语言编写高效代码的能力。因此,如果您想要高效的代码,则应该使用另一种语言。

组装对此尤为敏感,因为您所看到的就是所得到的。您编写希望CPU执行的特定指令。在高级语言中,没有一个编译器,它可以转换您的代码并消除许多效率低下的问题。有了组装,您就可以独自一人了。


2
我认为为了编写,特别是对于现代x86处理器,由于每个内核内存在管道,多个执行单元和其他头,因此编写高效的汇编代码异常困难。编写平衡所有这些资源的使用量以获得最高执行速度的代码,通常会导致代码具有不切实际的逻辑,根据“常规”汇编的智慧,“不应”很快。但是对于不太复杂的CPU,根据我的经验,可以大大改善C编译器的代码生成。
Olof Forshell 2012年

4
C编译器的代码可以被通常可以做得更好,即使是现代的x86 CPU。但是您必须非常了解CPU,而现代x86 CPU很难做到这一点。这就是我的意思。如果您不了解要定位的硬件,则将无法对其进行优化。然后编译器很可能会做的更好
jalf

1
而且,如果您真的想让编译器不知所措,则必须发挥创造力并以编译器无法做到的方式进行优化。时间/奖励是一个折衷,这就是为什么C是某些语言的脚本语言,而C是其他语言的高级代码的原因。不过对我来说,组装更有趣:)。很像grc.com/smgassembly.htm
霍肯(Hawken)

22

如今使用汇编语言的唯一原因是使用了一些该语言无法访问的功能。

这适用于:

  • 需要访问某些硬件功能(例如MMU)的内核编程
  • 使用编译器不支持的非常特定的向量或多媒体指令的高性能编程。

但是当前的编译器非常聪明,它们甚至可以替换两个单独的语句,例如 d = a / b; r = a % b;用一条指令一条指令可用,它也可以一次性计算除法和余数,即使C没有这样的运算符。


10
除了这两个之外,ASM还有其他地方。也就是说,由于可以访问进位标志和乘法的上半部分,因此bignum库在ASM中通常比C显着更快。您也可以在可移植的C语言中完成这些操作,但是它们的速度非常慢。
Mooing Duck 2012年

@MooingDuck可能被认为是访问该语言不直接可用的硬件硬件功能...但是只要您只是 高级代码手动转换为汇编语言,编译器就会击败您。
fortran 2012年

1
就是这样,但这不是内核编程,也不是特定于供应商的。尽管稍作修改,它很容易落入任一类别。当您想要没有C映射的处理器指令的性能时,请猜测ASM。
Mooing Duck 2012年

1
@fortran您基本上只是说,如果不对代码进行优化,它的速度将不及编译器优化的代码。优化是首先要编写汇编的原因。如果您的意思是翻译然后优化,那么除非您不擅长优化汇编程序,否则编译器没有理由击败您。因此,要击败编译器,您必须以编译器无法实现的方式进行优化。这很自我解释。编写汇编的唯一原因是,如果您比编译器/解释器更好。这始终是编写汇编的实际原因。
霍肯,2012年

1
只是说:Clang可以通过内置函数访问进位标志,128位乘法等等。并且可以将所有这些集成到其常规优化算法中。
gnasher729 2014年

19

的确,现代编译器在代码优化方面做得非常出色,但我仍然鼓励您继续学习汇编。

首先,您显然不被它吓倒了,这是一个很大的好处,其次,您可以通过概要分析走上正确的轨道,以验证或放弃速度假设,您需要有经验的人的帮助,然后拥有人类已知的最大的优化工具: 大脑

随着经验的增加,您将学习何时何地使用它(通常是在算法水平上进行了深度优化之后,代码中最紧密,最内层的循环)。

为了获得启发,我建议您查找Michael Abrash的文章(如果您没有听到他的话,他是优化专家;他甚至与John Carmack合作进行了Quake软件渲染器的优化!)

“没有最快的代码这样的东西”-Michael Abrash


2
我相信Michael Abrash的书籍之一是图形编程黑皮书。克里斯·索耶(Chris Sawyer)亲自编写了前两部过山车大亨游戏,但他并不是唯一使用汇编的人。
霍肯2012年

14

我已经更改了asm代码:

 __asm
{ 
    mov ebx,TIMES
 start:
    mov ecx,lengthOfArray
    mov esi,x
    shr ecx,2
    mov edi,y
label:
    mov eax,DWORD PTR [esi]
    add eax,DWORD PTR [edi]
    add edi,4   
    dec ecx 
    mov DWORD PTR [esi],eax
    add esi,4
    test ecx,ecx
    jnz label
    dec ebx
    test ebx,ebx
    jnz start
};

发布版本的结果:

 Function of assembly version: 41
 Function of C++ version: 161

发行模式下的汇编代码几乎比C ++快4倍。恕我直言,汇编代码的速度取决于程序员


是的,确实需要优化我的代码。感谢您的出色工作!
user957121 2012年

5
它的速度快了四倍,因为您只完成了四分之一的工作:-)这样做shr ecx,2是多余的,因为数组长度已经输入int,而不是以字节为单位。因此,您基本上可以达到相同的速度。您可以尝试 padddfrom harolds的答案,这确实会更快。
冈瑟·皮埃兹

13

这是一个非常有趣的话题!
我已经用Sasha的代码通过SSE更改了MMX,
这是我的结果:

Function of C++ version:      315
Function of assembly(simply): 312
Function of assembly  (MMX):  136
Function of assembly  (SSE):  62

SSE的汇编代码比C ++快5倍


12

大多数高级语言编译器都非常优化,并且知道它们在做什么。您可以尝试转储反汇编代码,并将其与本机程序集进行比较。我相信您会看到编译器正在使用的一些不错的技巧。

仅举例来说,即使我不确定它是否正确:):

正在做:

mov eax,0

花费比

xor eax,eax

做同样的事情。

编译器知道所有这些技巧并使用它们。


4
仍然适用,请参见stackoverflow.com/questions/1396527/…。不是因为使用了周期,而是因为减少了内存占用。
Gunther Piez'3

10

编译器击败了您。我会尝试的,但是我不会做任何保证。我将假设TIMES的“乘法”旨在使其成为更相关的性能测试,y并且x是16对齐的,并且length是4的非零倍数。无论如何,这可能都是正确的。

  mov ecx,length
  lea esi,[y+4*ecx]
  lea edi,[x+4*ecx]
  neg ecx
loop:
  movdqa xmm0,[esi+4*ecx]
  paddd xmm0,[edi+4*ecx]
  movdqa [edi+4*ecx],xmm0
  add ecx,4
  jnz loop

就像我说的,我不做任何保证。但是,如果可以更快地完成它,我会感到惊讶-即使瓶颈不是L1,这里的瓶颈也是内存吞吐量。


我认为复杂的寻址方式会降低代码的速度,如果将代码更改为mov ecx, length, lea ecx,[ecx*4], mov eax,16... add ecx,eax,然后在任何地方都使用[esi + ecx],则每条指令将避免1个周期的停顿,从而加快了循环速度。(如果您拥有最新的Skylake,则此方法不适用)。add reg,reg只是使循环更紧密,这可能有帮助也可能没有帮助。
约翰,

@Johan不应拖延,只是一个额外的周期延迟,但是确保没有它不会造成伤害。.我为Core2编写了此代码,但没有出现此问题。r + r难道不是也“复杂”吗?
哈罗德

7

只是盲目地执行完全相同的算法,指令由指令,在组件保证比什么编译器可以做慢。

这是因为即使是最小的优化,编译器也比没有任何优化的刚性代码要好。

当然,有可能击败编译器,特别是如果它只是代码的一小部分本地化部分,我什至必须自己做才能得到一个近似值。4倍速提速,但是在这种情况下,我们必须严重依赖于对硬件的了解以及众多看似不合常理的技巧。


3
我认为这取决于语言和编译器。我可以想象一个效率极低的C编译器,它的输出很容易被人工编写的汇编程序击败。海湾合作委员会,没有那么多。
Casey Rodarmor 2012年

由于C / ++编译器就是这样的事业,而且只有3种主要的编译器,因此它们在做事上往往比较擅长。在某些情况下,手写汇编仍然(非常)可能会更快。许多数学库都使用asm来更好地处理多个/宽值。因此,尽管保证有点太强大了,但很有可能。
ssube 2012年

@peachykeen:我并不是说总的来说保证汇编比C ++慢。在您具有C ++代码并盲目地将其逐行转换为汇编代码的情况下,我的意思是“保证”。也请阅读我的答案的最后一段:)
vsz 2012年

5

作为编译器,我会将固定大小的循环替换为许多执行任务。

int a = 10;
for (int i = 0; i < 3; i += 1) {
    a = a + i;
}

将产生

int a = 10;
a = a + 0;
a = a + 1;
a = a + 2;

最终它将知道“ a = a + 0;” 是没有用的,所以它将删除此行。希望您现在脑子里有些东西愿意附加一些优化选项作为评论。所有这些非常有效的优化将使编译的语言更快。


4
并且,除非a是易失性的,否则编译器int a = 13;很有可能从一开始便会做。
vsz


4

我喜欢这个示例,因为它演示了有关低级代码的重要课程。是的,您可以编写与C代码一样快的程序集。从重言式来看,这是正确的,但不一定意味着任何事情。显然有人可以,否则汇编程序将不知道适当的优化。

同样,当您沿语言抽象层次结构前进时,同样的原则也适用。是的,您可以使用C语言编写一个解析器,该解析器的速度与快速和肮脏的perl脚本一样快,很多人都可以这样做。但这并不意味着因为您使用了C,所以您的代码将很快。在许多情况下,高级语言会进行您可能从未考虑过的优化。


3

在许多情况下,执行某些任务的最佳方式可能取决于执行任务的环境。如果例程是用汇编语言编写的,那么通常就不可能根据上下文改变指令的顺序。作为一个简单的示例,请考虑以下简单方法:

inline void set_port_high(void)
{
  (*((volatile unsigned char*)0x40001204) = 0xFF);
}

上面给出的用于32位ARM代码的编译器可能会将其呈现为:

ldr  r0,=0x40001204
mov  r1,#0
strb r1,[r0]
[a fourth word somewhere holding the constant 0x40001204]

也许

ldr  r0,=0x40001000  ; Some assemblers like to round pointer loads to multiples of 4096
mov  r1,#0
strb r1,[r0+0x204]
[a fourth word somewhere holding the constant 0x40001000]

可以在手工汇编的代码中稍作优化,因为:

ldr  r0,=0x400011FF
strb r0,[r0+5]
[a third word somewhere holding the constant 0x400011FF]

要么

mvn  r0,#0xC0       ; Load with 0x3FFFFFFF
add  r0,r0,#0x1200  ; Add 0x1200, yielding 0x400011FF
strb r0,[r0+5]

两种手工组装的方法都需要12个字节的代码空间,而不是16个字节。后者将用“ add”替换“ load”,这将在ARM7-TDMI上更快地执行两个周期。如果要在r0未知/无关的上下文中执行代码,则汇编语言版本将比编译版本好一些。另一方面,假设编译器知道某个寄存器[例如r5]将保存在所需地址0x40001204 [例如0x40001000]的2047个字节内的值,并且进一步知道其他一些寄存器[例如r7]将要使用保留一个低位为0xFF的值。在这种情况下,编译器可以优化代码的C版本,使其简单地:

strb r7,[r5+0x204]

比手工优化的汇编代码更短,更快。此外,假设set_port_high在上下文中发生:

int temp = function1();
set_port_high();
function2(temp); // Assume temp is not used after this

当为嵌入式系统编码时,这一点都不令人难以置信。如果set_port_high是用汇编代码编写的,则编译器将必须function1在调用汇编代码之前将r0(保存返回值的)移到其他地方,然后再将该值移回r0(因为function2期望其第一个参数在r0中),因此,“优化的”汇编代码将需要五个指令。即使编译器不知道任何保存该地址或要存储的值的寄存器,其四指令版本(它可以适应以使用任何可用的寄存器-不一定是r0和r1)都将击败“优化”程序集语言版本。如果编译器如前所述在r5和r7中具有必要的地址和数据,则function1不会更改这些寄存器,因此可以替换set_port_high与,它的单个strb指令四个指令比“手动优化”的汇编代码更小,更快

请注意,在程序员知道精确的程序流程的情况下,手工优化的汇编代码通常可以胜过编译器,但是在知道一段代码的上下文之前编写一段代码,或者可能需要一段源代码的情况下,编译器会大放异彩。从多个上下文调用[如果set_port_high在代码中的五十个不同位置使用,则编译器可以独立地为每个扩展对象决定最佳扩展方式]。

通常,我建议汇编语言在可以从非常有限的上下文中访问每一段代码的情况下,倾向于产生最大的性能改进,并且在某些情况下,可能会损害性能。代码可以从许多不同的上下文中获取。有趣(方便)的是,汇编对性能最有利的情况通常是那些代码最直接,最容易阅读的情况。汇编语言代码会变成糊涂的地方通常是那些用汇编语言提供的性能收益最小的地方。

[未成年人注:在某些地方可以使用汇编代码来产生超优化的粘糊糊;例如,我为ARM编写的一段代码需要从RAM中提取一个字并基于该值的高6位(许多值映射到同一例程)执行大约十二个例程之一。我认为我将该代码优化为:

ldrh  r0,[r1],#2! ; Fetch with post-increment
ldrb  r1,[r8,r0 asr #10]
sub   pc,r8,r1,asl #2

寄存器r8始终保存主调度表的地址(在循环中,代码花费了其98%的时间,没有将其用于任何其他目的);所有64个条目都引用了其前256个字节中的地址。由于主循环在大多数情况下具有大约60个周期的硬执行时间限制,因此九个周期的获取和分派对于实现该目标非常有用。使用256个32位地址的表将快一个周期,但会吞噬1KB的非常宝贵的RAM [闪存将添加多个等待状态]。使用64个32位地址将需要添加一条指令以掩盖提取的字中的某些位,并且仍然比我实际使用的表多吞噬了192个字节。使用8位偏移量表可以生成非常紧凑且快速的代码,但是我不希望编译器会出现这种情况。我也不希望编译器将寄存器“​​全时”专用于保存表地址。

上面的代码旨在作为独立系统运行;它可以定期调用C代码,但是仅在某些时候,与之通信的硬件可以安全地处于“空闲”状态,每16ms间隔两个大约一毫秒。


2

最近,我所做的所有速度优化都是用合理的代码代替受脑损伤的慢代码。但是对于速度来说,速度是非常关键的,我全力以赴地加快速度,结果始终是一个迭代过程,每次迭代都可以使您对问题有更多的了解,并找到减少操作的方法。最终速度始终取决于我对问题的了解程度。如果在任何阶段使用汇编代码或过度优化的C代码,寻找更好的解决方案的过程都会受到影响,最终结果将变慢。


2

除非您以正确的方式使用具有更深知识的汇编语言,否则C ++的速度会更快。

当我在ASM中编码时,我会手动重新组织指令,以便在逻辑上可行时CPU可以并行执行更多指令。例如,当我在ASM中进行编码时,我几乎不使用RAM:ASM中可能有2万多行代码,而且我从未使用过push / pop。

您可能会跳到操作码的中间来自我修改代码和行为,而不会受到自我修改代码的惩罚。访问寄存器需要占用CPU的1个滴答声(有时需要0.25个滴答声)。访问RAM可能需要数百个。

对于我的上一个ASM冒险,我从未使用过RAM存储变量(用于数千行ASM)。ASM可能比C ++快得多。但这取决于许多可变因素,例如:

1. I was writing my apps to run on the bare metal.
2. I was writing my own boot loader that was starting my programs in ASM so there was no OS management in the middle.

我现在正在学习C#和C ++,因为我意识到生产力很重要!!您可以尝试在空闲时间仅使用纯ASM来完成最快的程序。但是为了产生某些东西,请使用一些高级语言。

例如,我编写的最后一个程序是使用JS和GLSL,我从来没有注意到任何性能问题,即使说到JS速度很慢。这是因为仅针对3D编程GPU的概念就使得将命令发送到GPU的语言的速度几乎无关紧要。

单独在裸机上组装的速度是无可辩驳的。在C ++中会更慢吗?-可能是因为您正在使用编译器编写汇编代码,而不是使用汇编器开始。

即使我喜欢汇编,但我个人的原则是永远不要编写汇编代码(如果可以避免的话)。


1

这里所有的答案似乎都排除了一个方面:有时我们不是为了达到特定目的而编写代码,而是出于纯粹的乐趣。花时间进行这项工作可能并不经济,但是可以说,与手动卷制asm替代品在速度上击败最快的编译器优化代码段相比,没有比这更令人满意的了。


当您只想击败编译器时,通常更容易为函数获取其asm输出并将其转换为您要调整的独立asm函数。使用内联 asm需要大量工作,以使C ++和asm之间的接口正确,并检查其是否已编译为最佳代码。(但是至少当只是为了好玩而已时,您不必担心它会在函数内联到其他内容中时击败诸如常量传播之类的优化 。gcc.gnu.org/wiki/DontUseInlineAsm)。
彼得·科德斯

另请参阅Collat​​z-conjecture C ++与手写asm问答,以获取更多有关击败编译器的乐趣:),以及有关如何使用所学知识来修改C ++的建议,以帮助编译器编写更好的代码。
彼得·科德斯

@PeterCordes所以你的意思是你同意。
madoki '16

1
是的,asm很有趣,除了嵌入式 asm通常是错误的选择,即使是在玩耍时也是如此。从技术上讲,这是一个内联汇编问题,因此最好在您的回答中至少解决这一点。此外,这实际上是评论而不是答案。
彼得·科德斯

好的,同意。我曾经是一个只有asm的家伙,但是那是80年代。
madoki '16

-2

在组织级别进行优化之后,C ++编译器将生成可利用目标cpu的内置函数的代码。HLL绝不会出于以下几个原因而超越或超越汇编程序:1.)HLL将使用访问者代码进行编译和输出,进行边界检查,并可能内置于垃圾回收中(以前以OOP方式处理地址),所有这些都需要周期(触发器)。HLL现在做得很好(包括较新的C ++和其他类似GO的工具),但是如果它们的性能优于汇编程序(即您的代码),则需要查阅CPU文档-与草率代码的比较毫无疑问,编译器语言像汇编程序一样可以解决所有问题直到操作码HLL都会抽象细节,并且不会消除细节,否则即使主机OS能够识别您的应用,您的应用也将无法运行。

大多数汇编代码(主要是对象)以“无头”形式输出,以包含在其他可执行格式中,而所需处理量却少得多,因此它将更快,但更不安全。如果汇编器(NAsm,YAsm等)输出可执行文件,则它仍将运行得更快,直到它在功能上与HLL代码完全匹配为止,然后才可以精确地权衡结果。

以任何格式从HLL调用基于汇编程序的代码对象,都会固有地增加处理开销,此外,还会使用针对可变/常量数据类型使用全局分配的内存的内存空间调用(这适用于LLL和HLL)。请记住,最终输出最终将CPU作为其相对于硬件(操作码)的api和abi,并且汇编程序和“ HLL编译器”在本质上/根本上是相同的,唯一的真正例外是可读性(语法上)。

在使用FAsm的汇编器中,Hello world控制台应用程序的大小为1.5 KB(在Windows中,在FreeBSD和Linux中甚至更小),并且胜过了GCC在最佳状态下可以抛弃的任何东西。原因是使用nops进行隐式填充,访问验证和边界检查等。真正的目标是干净的HLL库和可优化的编译器,该编译器以“硬核”方式针对cpu,大多数情况下(最终)都这样做了。GCC并不比YAsm好-有问题的是编码实践和对开发人员的理解,而“优化”是经过新手探索和临时培训与经验之后得出的。

编译器必须在与汇编器相同的操作码中进行链接和汇编,以输出输出,因为这些代码是CPU所独有的(CISC或RISC [PIC]也除外)。YAsm在早期的NAsm上进行了优化和清理,最终加快了该汇编程序的所有输出,但是即使那样,YAsm仍然像NAsm一样,以开发人员的名义针对OS库生成具有外部依赖性的可执行文件,因此里程可能会有所不同。结束语C ++令人难以置信,并且比汇编器更安全80%以上,尤其是在商业领域。


1
除非您提出要求,否则C和C ++没有任何边界检查,除非您自己实现或使用库,否则没有垃圾回收。真正的问题是,编译器是否比人类做出更好的循环(和全局优化)。通常是的,除非人类真的知道自己在做什么,并花大量时间在上面
彼得·科德斯

1
您可以使用NASM或YASM(无需外部代码)制作静态可执行文件。它们都可以以平面二进制格式输出,因此,如果您真的不想运行它们,可以让它们自己组装ELF标头ld,但这没有什么区别,除非您尝试真正优化文件大小(而不仅仅是优化文件大小)。文本段)。请参阅有关为Linux创建真正的ELF可执行文件的旋风教程
彼得·科德斯

1
也许您正在考虑使用C#,或std::vector在调试模式下进行编译。C ++数组不是那样的。编译器可以在编译时检查内容,但是除非启用额外的强化选项,否则不会进行运行时检查。例如,参见增加int array[]arg 的前1024个元素的函数。asm输出没有运行时检查:godbolt.org/g/w1HF5t。它得到的只是一个指针rdi,没有大小信息。这是给程序员用比1024更小的数组从来没有称这是为了避免不确定的行为
彼得·科德斯

1
无论您在谈论什么,都不是简单的C ++数组(使用分配,使用new手动删除delete,不进行边界检查)。您可以使用C ++生成肿的asm /机器代码(就像大多数软件一样),但这是程序员的错,而不是C ++的错。您甚至可以使用alloca将堆栈空间分配为数组。
彼得·科德斯

1
gcc.godbolt.org上链接一个示例,该示例g++ -O3为纯数组生成边界检查代码,或者执行您正在谈论的其他任何事情。C ++使生成肿的二进制代码变得容易得多(实际上,如果您要提高性能,则必须小心不要),但这并不是字面上不可避免的。如果您了解C ++如何编译为asm,则可以获得的代码只比您可以手工编写的要差一些,但是内联和常量传播的规模比您可以手工管理的要大。
彼得·科德斯

-3

如果您的编译器生成许多OO支持代码,则汇编可能会更快。

编辑:

致低俗人士:OP写道:“我应该……专注于C ++,而忘记汇编语言吗?” 我坚持我的回答。您始终需要注意OO生成的代码,尤其是在使用方法时。不要忘记汇编语言,这意味着您将定期检查OO代码生成的汇编,我认为这是编写性能良好的软件所必需的。

实际上,这与所有可编译代码有关,而不仅仅是面向对象。


2
-1:我看不到使用任何OO功能。您的论点与“如果编译器添加一百万个NOP,则汇编也可能会更快”相同。
Sjoerd 2012年

我不清楚,这实际上是一个C问题。如果您为C ++编译器编写C代码,则不是在编写C ++代码,而且您将不会得到任何面向对象的东西。一旦开始用真实的C ++编写代码,就必须使用OO知识来使编译器不生成OO支持代码。
Olof Forshell

所以你的答案不是这个问题吗?(另外,澄清去的答案,而不是评论评论可以在没有通知的,通知或历史中删除任何时候。
鸣叫鸭

1
不知道OO“支持代码”到底是什么意思。当然,如果您使用大量RTTI等工具,则编译器将不得不创建大量额外的指令来支持这些功能-但是任何足以批准使用RTTI的问题都太复杂了,无法在汇编中编写。当然,您可以做的是只将抽象的外部接口编写为OO,并在至关重要的情况下将其分配给性能优化的纯过程代码。但是,根据应用程序的不同,在不使用虚拟继承的情况下,C,Fortran,CUDA或仅是C ++可能比这里的汇编要好。
左右左转

2
不。至少不太可能。C ++中有一件事叫做零开销规则,这在大多数情况下都适用。了解有关OO的更多信息-您最终会发现,它可以提高代码的可读性,提高代码质量,提高编码速度,增强健壮性。同样适用于嵌入式-但是使用C ++可以为您提供更多控制权,而Java +嵌入式+ OO将花费您大量时间。
Zane 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.