ChatGPT解决这个技术问题 Extra ChatGPT

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

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

差不多。手工编码的汇编在某些情况下是合适的,但必须注意确保汇编版本确实比使用更高级别语言可以实现的更快。
您可能会发现研究编译器生成的代码很有启发性,并尝试理解为什么它比您的汇编版本更快。
是的,看起来编译器比你更擅长写 asm。现代编译器确实非常好。
你看过 GCC 产生的程序集吗?它可能的 GCC 使用了 MMX 指令。您的函数非常并行 - 您可能会使用 N 个处理器以 1/N 次计算总和。尝试一个没有并行化希望的函数。
嗯,我本来希望一个好的编译器能快 100000 倍地做这件事……

C
Community

是的,大多数时候。

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

您可以总是制作一个示例,其中手工汇编代码比编译代码更好,但通常这是一个虚构的示例或单个例程而不是 真正的程序500.000+ 行 C++ 代码)。我认为编译器会在 95% 的时间内生成更好的汇编代码,并且有时,仅在极少数情况下,您可能需要为少数、简短、highly usedperformance critical 例程或当您有访问您最喜欢的高级语言未公开的功能。您想了解一下这种复杂性吗?请在此处阅读this awesome answer

为什么这个?

首先是因为编译器可以进行我们甚至无法想象的优化(参见 this short list),而且他们会在 内完成(当 we may need days 时)。

当您在汇编中编码时,您必须使用定义良好的调用接口来制作定义良好的函数。但是,它们可以考虑 whole-program optimizationinter-procedural optimization,例如 register allocationconstant propagationcommon subexpression eliminationinstruction scheduling 和其他复杂的、不明显的优化(例如 Polytope model)。在 RISC 架构上,人们多年前就不再担心这个问题了(例如,指令调度很难tune by hand),现代的 CISC CPU 也有很长的 pipelines

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

编译器有时可以自己automatically use some MMX/SIMDx instructions,如果您不使用它们,您根本无法比较(其他答案已经很好地审查了您的汇编代码)。仅对于循环,这是编译器通常检查的short list of loop optimizations(当您为 C# 程序确定了日程安排后,您认为您可以自己完成吗?)如果您编写汇编中的某些东西,我认为您至少必须考虑一些simple optimizations。数组的教科书示例是 unroll the cycle(它的大小在编译时已知)。这样做并再次运行您的测试。

如今,由于另一个原因需要使用汇编语言也很少见:plethora of different CPUs。你想支持他们吗?每个都有一个特定的 microarchitecture 和一些 specific instruction sets。它们具有不同数量的功能单元,应安排组装说明以使它们都忙碌。如果您用 C 编写,您可以使用 PGO,但在汇编中,您将需要对该特定架构有深入的了解(并且重新考虑并重做其他架构的所有内容)。对于小型任务,编译器通常会做得更好,而对于复杂的任务通常,工作不会得到回报(无论如何compiler may do better)。

如果您坐下来查看您的代码,您可能会发现重新设计算法比转换为汇编(阅读此great post here on SO)获得的收益更多,这里有高级优化(和提示编译器)您可以在需要求助于汇编语言之前有效地应用。可能值得一提的是,经常使用内在函数可以获得所需的性能提升,并且编译器仍然能够执行大部分优化。

综上所述,即使您可以生成快 5 到 10 倍的汇编代码,您也应该询问您的客户是否愿意支付您一周的时间或购买速度快 50 美元的 CPU。我们大多数人通常不需要极端优化(尤其是在 LOB 应用程序中)。


当然不是。我认为 99% 的时间里 95% 的人会更好。有时是因为它很昂贵(因为复杂的数学)或时间花费(然后又很昂贵)。有时是因为我们确实忘记了优化......
@ja72 - 不,编写代码并不好。它更擅长优化代码。
在您真正考虑之前,这是违反直觉的。同样,基于 VM 的机器开始进行运行时优化,而编译器根本没有这些信息可以进行。
@M28:编译器可以使用相同的指令。当然,他们按二进制大小付费(因为如果不支持这些指令,他们必须提供后备路径)。此外,在大多数情况下,将添加的“新指令”无论如何都是 SMID 指令,VM 和编译器在使用这些指令时都非常糟糕。虚拟机为此功能付费,因为它们必须在启动时编译代码。
@BillK:PGO 为编译器做同样的事情。
J
Jean-François Fabre

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

您正在内部循环中推送和弹出寄存器(EDX)。这应该移出循环。

在循环的每次迭代中重新加载数组指针。这应该移出循环。

您使用循环指令,众所周知,在大多数现代 CPU 上该指令非常慢(可能是使用古代汇编书的结果*)

您没有利用手动循环展开。

您不使用可用的 SIMD 指令。

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

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


如果您针对大小进行优化,编译器可能仍会发出 loop (以及许多“已弃用”的指令)
@phuclv 是的,但是最初的问题完全是关于速度,而不是大小。
M
Matthieu M.

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

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 += 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)似乎没有执行这种转换。

[edit] 我发现如果我们将 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 版本而不是汇编版本,更便携。


感谢您的回答。嗯,当我学习名为“编译器原理”的课程时,我了解到编译器会通过多种方式优化我们的代码,这有点令人困惑。这是否意味着我们需要手动优化我们的代码?我们能比编译器做得更好吗?这是一直让我困惑的问题。
@user957121:当我们有更多信息时,我们可以更好地优化它。具体来说,阻碍编译器的是 xy 之间可能的aliasing。也就是说,编译器不能确定对于 [0, length) 中的所有 i,j 我们都有 x + i != y + j。如果有重叠,那么优化是不可能的。 C 语言引入了 restrict 关键字来告诉编译器两个指针不能别名,但它不适用于数组,因为即使它们不完全别名,它们仍然可以重叠。
当前的 GCC 和 Clang 自动矢量化(如果省略 __restrict,则在检查非重叠之后)。 SSE2 是 x86-64 的基线,通过改组 SSE2 可以一次进行 2 次 32 位乘法(生成 64 位产品,因此改组将结果重新组合在一起)。 godbolt.org/z/r7F_uo。 (pmulld 需要 SSE4.1:压缩 32x32 => 32 位乘法)。 GCC 有一个巧妙的技巧,可以将常量整数乘法器转换为移位/加法(和/或减法),这对于设置少量位的乘法器很有用。 Clang 的 shuffle-heavy 代码将成为 Intel CPU 上 shuffle 吞吐量的瓶颈。
O
Oliver Charlesworth

简短的回答:是的。

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


然后只有当你运行像 vtune 这样的汇编级分析工具来查看你可以改进的地方
这在技术上回答了这个问题,但也完全没用。我的一个-1。
很长的答案:“是的,除非您想在使用新的(er)CPU 时更改整个代码。选择最好的算法,但让编译器进行优化”
s
sasha

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


现在,如果您开始使用 SSE 而不是 MMX(寄存器名称是 xmm0 而不是 mm0),您将获得另一个两倍的加速;-)
我改变了,得到了 41 的汇编版本。它快了 4 倍 :)
如果使用所有 xmm 寄存器,也可以多获得 5%
现在,如果您考虑一下实际花费的时间:组装,大约需要 10 个小时左右? C++,我猜几分钟?这里有一个明显的赢家,除非它是性能关键代码。
一个好的编译器已经使用 paddd xmm 自动矢量化(在检查 xy 之间的重叠之后,因为您没有使用 int *__restrict x)。例如 gcc 这样做:godbolt.org/z/c2JG0-。或者在内联到 main 之后,它不需要检查重叠,因为它可以看到分配并证明它们不重叠。 (在某些 x86-64 实现中,它也会假设 16 字节对齐,而独立定义的情况并非如此。)如果您使用 gcc -O3 -march=native 编译,您可以获得 256 位或512 位矢量化。
j
jalf

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

是的,这正是它的意思,并且适用于每种语言。如果你不知道如何用 X 语言编写高效的代码,那么你不应该相信你用 X 编写高效代码的能力。因此,如果你想要高效的代码,你应该使用另一种语言。

装配对此特别敏感,因为,你所见即所得。您编写希望 CPU 执行的特定指令。对于高级语言,中间有一个编译器,它可以转换你的代码并消除许多低效率的地方。有了组装,你就靠自己了。


我认为这是为了编写,特别是对于现代 x86 处理器,由于每个内核中存在管道、多个执行单元和其他噱头,因此编写高效的汇编代码异常困难。编写平衡所有这些资源的使用以获得最高执行速度的代码通常会导致代码具有不直截了当的逻辑,根据“传统”汇编智慧“不应该”快速。但是对于不太复杂的 CPU,根据我的经验,C 编译器的代码生成可以显着提高。
即使在现代 x86 CPU 上,通常也可以改进 C 编译器代码。但是你必须很好地了解 CPU,这对于现代 x86 CPU 来说更难做到。这就是我的观点。如果您不了解您的目标硬件,那么您将无法针对它进行优化。然后编译器可能会做得更好
如果你真的想把编译器吹走,你必须要有创造力,并以编译器无法做到的方式进行优化。这是时间/回报的权衡,这就是为什么 C 是某些脚本语言和其他高级语言的中间代码的原因。不过对我来说,组装更有趣:)。很像grc.com/smgassembly.htm
f
fortran

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

这适用于:

需要访问某些硬件功能(例如 MMU)的内核编程

使用编译器不支持的非常特定的向量或多媒体指令的高性能编程。

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


除了这两个之外,ASM 还有其他地方。也就是说,由于可以访问进位标志和乘法的上半部分等,ASM 中的 bignum 库通常比 C 快得多。你也可以在可移植的 C 中做这些事情,但是它们非常慢。
@MooingDuck这可能被认为是访问该语言中不直接可用的硬件硬件功能......但是只要您只是将高级代码手动转换为汇编,编译器就会击败您。
就是这样,但它不是内核编程,也不是特定于供应商的。尽管有轻微的工作变化,但它很容易落入任一类别。当您想要没有 C 映射的处理器指令的性能时,我会猜测 ASM。
@fortran你基本上只是说如果你不优化你的代码,它就不会像编译器优化的代码那么快。优化是人们首先编写程序集的原因。如果你的意思是翻译然后优化,编译器没有理由打败你,除非你不擅长优化汇编。因此,要击败编译器,您必须以编译器无法做到的方式进行优化。这很不言自明。编写程序集的唯一原因是您是否比编译器/解释器更好。这一直是编写汇编的实际原因。
只是说:Clang 可以通过内置函数访问进位标志、128 位乘法等。它可以将所有这些集成到其正常的优化算法中。
佚名

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

首先,您显然不会被它吓倒,这是一个非常棒的加分项,接下来 - 您通过分析走在正确的轨道上以验证或放弃您的速度假设,您正在向有经验的人征求意见,并且您拥有人类已知的最强大的优化工具:大脑。

随着经验的增加,您将了解何时何地使用它(通常是代码中最紧凑、最内层的循环,在您在算法级别进行深度优化之后)。

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

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


我相信 Michael Abrash 的书之一是图形编程黑皮书。但他并不是唯一一个使用汇编的人,克里斯·索耶(Chris Sawyer)在汇编中编写了前两个过山车大亨游戏。
s
sasha

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


是的,我的代码确实需要优化。祝你工作顺利,谢谢!
它快四倍,因为您只完成了四分之一的工作 :-) shr ecx,2 是多余的,因为数组长度已经在 int 中给出,而不是在字节中。所以你基本上达到了同样的速度。您可以尝试 harolds 答案中的 paddd,这确实会更快。
s
salaoshi

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


P
Peter Mortensen

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

举个例子,即使我不确定它是否正确:):

正在做:

mov eax,0

花费更多的周期比

xor eax,eax

它做同样的事情。

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


仍然正确,请参见stackoverflow.com/questions/1396527/…。不是因为使用的周期,而是因为减少了内存占用。
h
harold

编译器打败了你。我会试一试,但我不会做任何保证。我将假设 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 命中。


我认为复杂的寻址会减慢您的代码速度,如果您将代码更改为 mov ecx, length, lea ecx,[ecx*4], mov eax,16... add ecx,eax 然后在任何地方使用 [esi+ecx] 您将避免每条指令出现 1 个周期停顿,从而加快循环批次。 (如果您拥有最新的 Skylake,则不适用)。 add reg,reg 只是使循环更紧密,这可能有帮助,也可能没有帮助。
@Johan 这不应该是一个停顿,只是一个额外的周期延迟,但肯定没有它不会有什么坏处。我为没有这个问题的 Core2 编写了这段代码。顺便说一句,r+r 不是也“复杂”吗?
v
vsz

只是盲目地在汇编中逐条执行完全相同的算法,保证比编译器可以做的要慢。

这是因为即使是编译器所做的最小优化也比完全没有优化的僵化代码要好。

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


我认为这取决于语言和编译器。我可以想象一个效率极低的 C 编译器,其输出很容易被人类编写简单的汇编程序所击败。海湾合作委员会,不是那么多。
由于 C/++ 编译器是一项如此艰巨的任务,而且只有 3 个主要的编译器,因此它们往往非常擅长自己的工作。在某些情况下,手写汇编仍然(非常)可能会更快;许多数学库都使用 asm 以更好地处理多个/宽值。因此,虽然保证有点太强了,但很有可能。
@peachykeen:我并不是说程序集通常会比 C++ 慢。我的意思是在你有一个 C++ 代码并盲目地将它逐行翻译成汇编的情况下的“保证”。也阅读我答案的最后一段:)
M
Miah

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

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


除非 a 是易变的,否则编译器很有可能从一开始就执行 int a = 13;
L
Luchian Grigore

这正是它的意思。将微优化留给编译器。


t
tylerl

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

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


s
supercat

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

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),因此“优化”的汇编代码需要 5 条指令。即使编译器不知道任何寄存器保存地址或要存储的值,它的四指令版本(它可以适应使用任何可用的寄存器——不一定是 r0 和 r1)也会击败“优化”程序集- 语言版本。如果编译器在 r5 和 r7 中有必要的地址和数据,如前所述,function1 不会更改这些寄存器,因此它可以用单个 strb 指令替换 set_port_high -四个指令更小并且比“手动优化”的汇编代码更快

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

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

[小提示:在某些地方可以使用汇编代码来产生超优化的糊状混乱;例如,我为 ARM 编写的一段代码需要从 RAM 中获取一个字,并根据值的高六位执行大约十二个例程之一(许多值映射到同一个例程)。我想我将该代码优化为:

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 个周期,因此 9 个周期的获取和分派对于实现该目标非常有帮助。使用包含 256 个 32 位地址的表会快一个周期,但会占用 1KB 非常宝贵的 RAM [闪存会添加多个等待状态]。使用 64 个 32 位地址需要添加一条指令来屏蔽提取的字中的一些位,并且仍然会比我实际使用的表多吞噬 192 个字节。使用 8 位偏移量表产生了非常紧凑和快速的代码,但我不希望编译器会想出这样的东西;我也不希望编译器专门用一个寄存器“全职”来保存表地址。

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


g
gnasher729

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


m
madoki

这里的所有答案似乎都排除了一个方面:有时我们编写代码不是为了实现特定目标,而是为了纯粹的乐趣。花时间这样做可能不经济,但可以说,没有比手动滚动 asm 替代方案在速度上击败最快的编译器优化代码片段更令人满意的了。


当您只想击败编译器时,通常更容易将其 asm 输出用于您的函数并将其转换为您调整的独立 asm 函数。使用 inline asm 需要大量额外的工作才能使 C++ 和 asm 之间的接口正确并检查它是否正在编译为最佳代码。 (但至少当它只是为了好玩时,当函数内联到其他东西时,您不必担心它会破坏诸如常量传播之类的优化。gcc.gnu.org/wiki/DontUseInlineAsm)。
另请参阅 the Collatz-conjecture C++ vs. hand-written asm Q&A,了解更多关于击败编译器的乐趣 :) 以及有关如何使用所学知识修改 C++ 以帮助编译器生成更好代码的建议。
@PeterCordes所以你说的是你同意。
是的,asm 很有趣,除了内联 asm 通常是错误的选择,即使是在玩耍时也是如此。这在技术上是一个 inline-asm 问题,所以至少在你的答案中解决这一点会很好。此外,这实际上更像是一个评论而不是一个答案。
好的同意了。我曾经是一个只有 asm 的人,但那是 80 年代。
佚名

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

当我在 ASM 中编码时,我会手动重新组织指令,以便 CPU 可以在逻辑可能的情况下并行执行更多指令。例如,当我在 ASM 中编码时,我几乎不使用 RAM:ASM 中可能有 20000 多行代码,而我从未使用过 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++ 中它会更慢吗? - 这可能是因为您正在使用编译器编写汇编代码,而不是使用汇编程序开始。

我个人的想法是,如果可以避免的话,永远不要编写汇编代码,即使我喜欢汇编。


O
Olof Forshell

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

编辑:

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

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


-1:我没有看到正在使用任何 OO 功能。您的论点与“如果您的编译器添加一百万个 NOP,汇编也可能更快”相同。
我不清楚,这实际上是一个 C 问题。如果您为 C++ 编译器编写 C 代码,那么您就不是在编写 C++ 代码,也不会得到任何面向对象的东西。一旦你开始用真正的 C++ 编写,使用 OO 的东西,你必须非常了解让编译器不生成 OO 支持代码。
所以你的答案不是关于这个问题? (此外,答案中包含澄清,而不是评论。评论可以随时删除,无需通知、通知或历史记录。
不确定OO“支持代码”到底是什么意思。当然,如果您使用大量 RTTI 之类的东西,编译器将不得不创建大量额外指令来支持这些功能——但是任何足以批准使用 RTTI 的高级问题都太复杂而无法在汇编中编写.当然,您可以做的就是只将抽象的外部接口编写为 OO,在关键的地方分派到性能优化的纯过程代码。但是,根据应用程序,C、Fortran、CUDA 或没有虚拟继承的简单 C++ 可能比这里的汇编更好。
不,至少不太可能。 C++ 中有一个叫做零开销规则的东西,这在大多数情况下都适用。了解有关 OO 的更多信息 - 您最终会发现它提高了代码的可读性,提高了代码质量,提高了编码速度,提高了健壮性。也适用于嵌入式 - 但使用 C++,因为它为您提供更多控制,嵌入式 + OO Java 方式将花费您。