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

use*_*121 174 c c++ performance assembly

我试图比较内联汇编语言和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
    };
}
Run Code Online (Sandbox Code Playgroud)

这是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;
}
Run Code Online (Sandbox Code Playgroud)

然后我运行程序五次以获得处理器的周期,这可以被视为时间.每次我只调用上面提到的功能之一.

这是结果.

装配版本的功能:

Debug   Release
---------------
732        668
733        680
659        672
667        675
684        694
Average:   677
Run Code Online (Sandbox Code Playgroud)

C++版本的功能:

Debug     Release
-----------------
1068      168
 999      166
1072      231
1002      166
1114      183
Average:  182
Run Code Online (Sandbox Code Playgroud)

发布模式下的C++代码几乎是汇编代码的3.7倍.为什么?

我想我写的汇编代码不如GCC生成的那样有效.像我这样的普通程序员很难比编译器生成的对手更快地编写代码.这意味着我不应该相信我手写的汇编语言的表现,专注于C++而忘记汇编语言?

Adr*_*tti 244

是的,大多数时候.

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

总是可以创建一个示例,其中手工汇编代码比编译代码更好,但通常它是一个虚构的示例或单个例程,而不是500.000+行C++代码的真正程序).我认为编译器会产生95%的更好的汇编代码,有时候,只有极少数情况下,您可能需要编写汇编代码,用于少数,简短,高度使用,性能关键的例程,或者当您必须访问您喜欢的高级语言的功能时没有曝光.你想要触及这种复杂性吗?在SO上阅读这个很棒的答案.

为什么这个?

首先,因为编译器可以进行我们甚至无法想象的优化(参见这个简短的列表),他们将在几秒钟内完成它们(当我们可能需要几天时).

在汇编代码中进行编码时,必须使用定义良好的调用接口来创建定义良好的函数.然而,它们可以考虑整个程序优化过程间优化,例如寄存器分配,常量传播,公共子表达式消除,指令调度和其他复杂的,不明显的优化(例如,Polytope模型).在RISC架构上,人们多年前就不再担心这种情况(例如,指令调度很难手动调整),现代CISC CPU也有很长的管道.

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

编译器有时可以自动使用一些MMX/SIMDx指令,如果你不使用它们就无法比较(其他答案已经很好地检查了汇编代码).只是for循环这是一个循环优化简短列表,通常由编译器检查(你认为你可以自己在为C#程序决定你的日程安排时自己做吗?)如果你在汇编中写一些东西,我认为你必须考虑至少一些简单的优化.数组的学校书籍示例是展开循环(其大小在编译时已知).做它并再次运行测试.

现在,出于另一个原因需要使用汇编语言也是非常罕见的:过多的不同CPU.你想支持他们吗?每个都有一个特定的微体系结构和一些特定的指令集.它们具有不同数量的功能单元,并且应该安排组装指令以使它们全部忙碌.如果你用C语言编写,你可能会使用PGO,但是在汇编中你需要对该特定体系结构有很好的了解(并重新考虑并重做另一个体系结构的所有内容).对于小任务,编译器通常会做得更好,对于复杂的任务,通常不会回报工作(并且编译器可能会做得更好).

如果你坐下来看看你的代码,你可能会发现重新设计你的算法会比转换到汇编获得更多(在SO上阅读这篇精彩文章),有高级优化(和提示编译器)您可以在需要使用汇编语言之前有效地应用.值得一提的是,经常使用内在函数,您将获得正在寻找的性能增益,编译器仍然可以执行大部分优化.

所有这些都说,即使你能够生成5到10倍快的汇编代码,你应该问你的客户他们是否愿意支付一周的时间购买50美元更快的CPU.我们大多数人根本不需要极端优化(特别是在LOB应用程序中).

  • @ ja72 - 不,写*代码并不是更好.它优于*优化*代码. (62认同)
  • 在你真正考虑它之前,这是违反直觉的.以同样的方式,基于VM的机器开始进行运行时优化,编译器根本没有信息. (14认同)
  • 当然不是.我认为99%的人在95%的人中表现更好.有时因为它只是昂贵(因为**复杂的**数学)或时间花费(然后再次昂贵).有时因为我们忘了优化...... (9认同)
  • @BillK:PGO为编译器做同样的事情. (9认同)
  • @ M28:编译器可以使用相同的指令.当然,他们用二进制大小来支付它(因为他们必须在不支持这些指令的情况下提供回退路径).此外,在大多数情况下,无论如何,将添加的"新指令"是SMID指令,其中VM和编译器在使用时非常可怕.VM为此功能付费,因为他们必须在启动时编译代码. (6认同)
  • @Bill不是真的.编译器拥有与VM一样多的信息.只是编译器通常(微小)仍然比VM更快,而不考虑这些事情.(如果支持配置文件引导优化,则VM可以提供给编译器的任何统计信息,如同大多数编译器现在所做的那样)然而,使编译器或VM考虑更多优化而不是实际操作是微不足道的.手. (5认同)
  • @BillK:我认为drhirsch只是在嘲笑你坚持使用传统的编译/优化技术来阻止虚拟机成为*性能杀手.关于"避免手动重新编译",对大多数真正对性能感兴趣的人并不感兴趣.无论是从一些二进制中间语言开始,还是从源代码开始生成新的优化二进制文件(无论是否是原生的)都与编译器的原理相同,对性能真正感兴趣的人会使用它,即使它产生了手动操作. (5认同)
  • @Billy VMs知道CPU架构及其支持的指令,编译器不能使用这些指令,因为它必须谨慎,不要使用它们来支持不支持它们的CPU. (4认同)
  • @BillK:当然VM可以和编译一样快; 毕竟,大多数VM*都是*JIT编译器.只是大多数虚拟机通常不愿意支付优化时间成本来实现目标.一些现代VM真的非常擅长放置优化赌注.我的观点仅仅是总是存在权衡,而且一般来说编译器和虚拟机会(通常会说与类似的编写代码相比.大多数"VM"语言的更大开销是垃圾收集而不是JIT编译.(虽然GC有也有优势;例如分配真的很快) (3认同)
  • 我并不特别试图隐藏它,但这是将世界分类为"Java人"和"非Java人"的一种非常奇怪的方式.你也可以称我为"没有拿着点燃炸药棒"的家伙 - 这更加准确,因为虽然我花了数年时间专业地开发C和C++(和其他各种语言),但我从来没有拿过任何比点燃鞭炮更大的东西.我的手. (2认同)
  • 这是#100.:) (2认同)

Gun*_*iez 189

您的汇编代码非常差, 略微不理想,可能会有所改进:

  • 您正在推动并弹出内循环中的寄存器(EDX).这应该移出循环.
  • 您在循环的每次迭代中重新加载数组指针.这应该移出循环.
  • 您使用的loop指令在大多数现代CPU上已知都很(可能是因为使用了古老的装配书*)
  • 您不会利用手动循环展开.
  • 您没有使用可用的SIMD说明.

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

*当然,我不知道你是否真的得到loop了古代装配书的指示.但是你几乎从来没有在现实世界的代码中看到它,因为那里的每个编译器都足够聪明,不会发出loop,你只能在恕我直言中看到它们过时和过时的书籍.


Mat*_* M. 59

甚至在深入研究装配之前,存在更高级别的代码转换.

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];
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

可以通过循环旋转转换为:

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];
      }
    }
}
Run Code Online (Sandbox Code Playgroud)

就内存位置而言,这要好得多.

这可以进一步优化,做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];
    }
}
Run Code Online (Sandbox Code Playgroud)

但似乎我最喜欢的优化器(LLVM)不执行此转换.

[编辑]我发现如果我们有restrict限定符x和,那就进行转换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
Run Code Online (Sandbox Code Playgroud)

我担心我不明白所有这些指令来自何处,但是你总是可以玩得开心并尝试看看它是如何比较的...但我仍然使用优化的C版本而不是汇编版本,在代码中,更便携.

  • @ user957121:当我们有更多信息时,我们可以更好地优化它.具体来说,阻碍编译器的是`x`和`y`之间可能的*别名*.也就是说,编译器无法确定对于`[0,length)中的所有`i,j`,我们有`x + i!= y + j`.如果存在重叠,则无法进行优化.C语言引入了`restrict`关键字来告诉编译器两个指针不能别名,但它不适用于数组,因为它们仍然可以重叠,即使它们不是完全别名. (2认同)

Oli*_*rth 41

简短回答:是的.

答案长:是的,除非你真的知道你在做什么,并且有理由这样做.

  • 然后,只有你运行了一个程序集级别的分析工具,如vtune for intel chips,看看你在哪里可以改进东西 (3认同)
  • 这从技术上回答了问题,但也完全没有用。我给的-1。 (2认同)
  • 很长的答案:“是的,除非您想在使用新的(er)CPU时更改整个代码。选择最佳算法,但让编译器进行优化” (2认同)

sas*_*sha 33

我修复了我的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
};
Run Code Online (Sandbox Code Playgroud)

发布版本的结果:

 Function of assembly version: 81
 Function of C++ version: 161
Run Code Online (Sandbox Code Playgroud)

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

  • 现在,如果你开始使用SSE而不是MMX(寄存器名称是`xmm0`而不是'mm0`),你将获得另一倍的加速;-) (17认同)
  • 我换了,装配版得了41.它快4倍:) (7认同)
  • 现在,如果你考虑它实际上花费的时间:装配,大约10个小时左右?C++,我猜几分钟?这里有一个明显的赢家,除非它是性能关键代码. (5认同)
  • 如果使用所有xmm寄存器,也可以多达5% (3认同)

jal*_*alf 24

这是否意味着我不应该相信我手中编写的汇编语言的表现

是的,这正是它的意思,对每种语言都是如此.如果您不知道如何在X语言中编写高效的代码,那么您就不应该相信自己在X中编写高效代码的能力.因此,如果您想要高效的代码,则应该使用其他语言.

装配对此特别敏感,因为,你所看到的就是你得到的.您编写了您希望CPU执行的特定指令.使用高级语言,有一个编译器,它可以转换您的代码并消除许多低效率.有了装配,你就可以自己动手了.

  • 即使在现代的x86 CPU上,C编译器代码*通常也可以更好.但你必须很好地理解CPU,这对于现代的x86 CPU来说更难.这是我的观点.如果您不了解所定位的硬件,那么您将无法对其进行优化.然后编译器可能会做得更好 (4认同)
  • 我认为这是为了编写特别是对于现代x86处理器而言,由于每个核心内部存在管道,多个执行单元和其他噱头,因此编写高效的汇编代码非常困难.编写平衡所有这些资源的使用以获得最高执行速度的代码通常会导致代码具有根据"传统"汇编智慧"不应该"快速的非直接逻辑.但对于不太复杂的CPU,我的经验是C编译器的代码生成可以显着改善. (2认同)

for*_*ran 21

现在使用汇编语言的唯一原因是使用语言无法访问的某些功能.

这适用于:

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

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

  • 除了这两个之外,ASM还有其他地方.也就是说,由于可以访问进位标志和乘法的上部等,因此bignum库在ASM中通常比C快得多.你也可以在便携式C中做这些事情,但它们非常慢. (10认同)

小智 19

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

首先,你显然没有被它吓倒,这是一个伟大的,伟大的优势,接下来 - 你正在通过剖析以验证或放弃你的速度假设,你要求有经验的人和你的输入拥有人类已知的最好的优化工具: 大脑.

随着您的体验增加,您将学习何时何地使用它(通常在您的算法级别进行深度优化后,代码中最紧密,最内层的循环).

为了寻找灵感,我会建议你查找迈克尔·亚伯拉什的文章(如果你还没有听到他的消息,他是一个优化大师,他甚至与约翰·卡马克在地震软件渲染器的优化合作!)

"没有最快的代码" - 迈克尔·阿布拉什

  • 我相信Michael Abrash的一本书就是图形编程黑皮书.但他不是唯一一个使用集会的人,克里斯索耶自己编写了前两个过山车大亨游戏. (2认同)

sas*_*sha 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
};
Run Code Online (Sandbox Code Playgroud)

发布版本的结果:

 Function of assembly version: 41
 Function of C++ version: 161
Run Code Online (Sandbox Code Playgroud)

发布模式下的汇编代码几乎是C++的4倍.IMHo,汇编代码的速度取决于程序员

  • 它的速度提高了四倍,因为你只做了四分之一的工作:-)`shr ecx,2`是多余的,因为数组长度已经在`int`中给出而不是在byte中.所以你基本上达到了同样的速度.你可以从harold的答案中尝试`paddd`,这真的会更快. (5认同)

Nun*_*147 12

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

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

这样做:

mov eax,0
Run Code Online (Sandbox Code Playgroud)

花费更多的周期

xor eax,eax
Run Code Online (Sandbox Code Playgroud)

它做同样的事情.

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

  • 仍然如此,请参阅http://stackoverflow.com/questions/1396527/any-reason-to-do-a-xor-eax-eax/1396552#1396552.不是因为使用的周期,而是因为内存占用减少. (4认同)

小智 12

这是非常有趣的话题!
我在Sasha的代码中用SSE更改了MMX
这是我的结果:

Function of C++ version:      315
Function of assembly(simply): 312
Function of assembly  (MMX):  136
Function of assembly  (SSE):  62
Run Code Online (Sandbox Code Playgroud)

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


har*_*old 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
Run Code Online (Sandbox Code Playgroud)

就像我说的,我不保证.但是如果它可以更快地完成我会感到惊讶 - 这里的瓶颈就是内存吞吐量,即使一切都是L1命中.


vsz*_*vsz 6

只是盲目地实现完全相同的算法,逐个指令,在汇编中保证比编译器可以做的慢.

这是因为即使是编译器所做的最小优化也比刚刚完成优化的严格代码要好.

当然,有可能击败编译器,特别是如果它是代码的一个小的,本地化的部分,我甚至不得不亲自去做一个约.4倍加速,但在这种情况下,我们必须严重依赖对硬件的良好了解和众多看似反直觉的技巧.

  • 我认为这取决于语言和编译器.我可以想象一个非常低效的C编译器,其输出很容易被人类编写简单的程序集打败.海湾合作委员会,而不是. (3认同)

小智 5

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

int a = 10;
for (int i = 0; i < 3; i += 1) {
    a = a + i;
}
Run Code Online (Sandbox Code Playgroud)

将产生

int a = 10;
a = a + 0;
a = a + 1;
a = a + 2;
Run Code Online (Sandbox Code Playgroud)

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

  • 除非`a`是易失的,否则编译器很有可能从一开始就只做`int a = 13;`。 (4认同)