繁体   English   中英

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

[英]Is inline assembly language slower than native C++ code?

我试图比较内联汇编语言和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 ++而忘记汇编语言?

是的,大多数时候。

首先,你从一个错误的假设开始,即低级语言(在这种情况下是汇编)总是会产生比高级语言更快的代码(在这种情况下是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应用程序中)。

您的汇编代码不是最理想的,可以改进:

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

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

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

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

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];
    }
  }
}

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

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 += b X次相当于做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)不执行此转换。

[编辑]我发现如果我们将restrict限定符设置为xy则执行转换。 实际上没有这个限制, 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版本而不是汇编版本,在代码中,更便携。

简短回答:是的。

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

我修复了我的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倍。

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

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

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

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

这适用于:

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

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

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

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

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

为了获得灵感,我建议你查找Michael Abrash的文章(如果你没有收到他的消息,他是一名优化大师;他甚至与John Carmack合作优化了Quake软件渲染器!)

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

我更改了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倍。 IMHo,汇编代码的速度取决于程序员

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

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

这样做:

mov eax,0

花费更多的周期

xor eax,eax

它做同样的事情。

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

这是非常有趣的话题!
我在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倍

编译器打败了你。 我试试看,但我不会保证。 我将假设TIMES的“乘法”意味着使它成为一个更相关的性能测试, yx是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命中。

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

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

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

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

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;” 没用,所以它会删除这一行。 希望你脑子里现在愿意附上一些优化选项作为评论。 所有这些非常有效的优化将使编译语言更快。

这正是它的含义。 将微优化保留给编译器。

我喜欢这个例子,因为它展示了关于低级代码的重要教训。 是的,您可以编写与C代码一样快的程序集。 这是同义反复的,但并不一定意味着什么。 显然有人可以,否则汇编程序不会知道适当的优化。

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

在许多情况下,执行某项任务的最佳方式可能取决于执行任务的上下文。 如果例程是用汇编语言编写的,则通常不可能根据上下文改变指令序列。 举个简单的例子,考虑以下简单方法:

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个; 后者将用“添加”替换“加载”,这将在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是用汇编代码编写的,编译器必须在调用汇编代码之前将r0(它保存来自function1的返回值)移动到其他地方,然后将该值移回r0(因为function2将期望它的第一个参数) r0),因此“优化的”汇编代码需要五条指令。 即使编译器不知道任何保存地址或存储值的寄存器,它的四指令版本(它可以适应使用任何可用的寄存器 - 不一定是r0和r1)将击败“优化”汇编 - 语言版本。 如果编译器在前面描述的r5和r7中有必要的地址和数据,则function1不会改变那些寄存器,因此它可以用单个strb指令替换set_port_high 四个指令比“手工优化”的程序集更小更快码。

请注意,在程序员知道精确的程序流程的情况下,手动优化的汇编代码通常可以胜过编译器,但编译器在代码片段在其上下文已知之前编写,或者某个源代码可能是从多个上下文调用[如果在代码中的50个不同位置使用set_port_high ,则编译器可以独立地决定每个内容如何最好地扩展它]。

总的来说,我建议汇编语言在那些可以从非常有限的上下文中处理每一段代码的情况下容易产生最大的性能改进,并且在一块片段的地方容易对性能产生不利影响。可以从许多不同的上下文中接近代码。 有趣的是(并且方便地)汇编对性能最有利的情况通常是代码最简单易读的情况。 汇编语言代码变成粘糊糊的地方通常是那些写入汇编会提供最小性能优势的地方。

[次要注意:有些地方可以使用汇编代码来产生超级优化的粘糊糊的混乱; 例如,我为ARM做的一段代码需要从RAM中获取一个字,并根据该值的高六位执行大约12个例程中的一个(许多值映射到同一例程)。 我想我将代码优化为:

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两个大约一毫秒的间隔进入“空闲”状态。

最近,我所做的所有速度优化都是用合理的代码替换脑损坏的慢速代码。 但是对于事情而言,速度确实很关键,我努力做出快速的事情,结果始终是一个迭代过程,每次迭代都能更深入地了解问题,找到如何用更少的操作解决问题的方法。 最终的速度总是取决于我对问题的深入了解。 如果在任何阶段我使用汇编代码或过度优化的C代码,那么找到更好的解决方案的过程将会受到影响,最终结果会变慢。

除非您使用具有更深入知识的汇编语言并使用正确的方法,否则C ++会更快。

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

您可能会跳转到操作码的中间,以自我修改代码和行为,而不会受到自修改代码的惩罚。 访问寄存器需要1个滴答(有时需要0.25个滴答)的CPU。访问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 ++中它可能更慢吗? - 可能是因为您正在编写汇编代码,而编译器不使用汇编程序来开始。

我的个人理事会是永远不会编写汇编代码,如果你可以避免它,即使我喜欢汇编。

这里的所有答案似乎都排除了一个方面:有时我们不会编写代码来实现特定的目标,而是为了它的纯粹乐趣 投入时间这样做可能不经济,但可以说,与手动滚动的asm替代方案相比,速度最快的编译器优化代码片段没有更大的满足感。

在组织级别优化之后,c ++编译器将生成将利用目标cpu的内置函数的代码。 由于多种原因,HLL永远不会超过或超过汇编程序; 1.)HLL将被编译并输出Accessor代码,边界检查以及可能内置于垃圾收集(以前在OOP习惯中的范围),所有这些都需要循环(翻转和翻转)。 HLL目前表现非常出色(包括较新的C ++和其他类似GO),但如果它们的表现优于汇编程序(即你的代码),你需要查阅CPU文档 - 与草率代码进行比较肯定是不确定的,编译后的语言就像汇编程序一样解决转到操作码HLL抽象细节并不消除它们,否则如果主机操作系统甚至可以识别你的应用程序就不会运行。

大多数汇编代码(主要是对象)都被输出为“无头”,以便包含在其他可执行格式中,所需的处理要少得多,因此它会更快,但更不安全; 如果汇编程序输出可执行文件(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一样,代表开发人员生成具有外部依赖关系的可执行文件,因此里程数可能会有所不同。 最后,C ++处于令人难以置信的地步,比80%以上的汇编程序更安全,特别是在商业领域......

如果您的编译器生成大量OO支持代码,那么汇编可能会更快。

编辑:

对于downvoters:OP写道“我应该......专注于C ++并忘记汇编语言吗?” 我支持我的回答。 您始终需要密切关注OO生成的代码,尤其是在使用方法时。 不要忘记汇编语言意味着您将定期查看您的OO代码生成的程序集,我认为这是编写性能良好的软件所必需的。

实际上,这适用于所有可编译代码,而不仅仅是OO。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM