内联汇编语言比原生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++而忘记汇编语言?

71236 次浏览

这就是它的意思。把微优化留给编译器。

简短的回答:是的。

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

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

这适用于:

  • 需要访问某些硬件特性(如MMU)的内核编程
  • 使用编译器不支持的特定矢量或多媒体指令的高性能编程。
但是现在的编译器非常聪明,它们甚至可以替换两个单独的语句,比如 d = a / b; r = a % b;使用一个指令,在可用的情况下一次性计算除法和余数,即使C没有这样的操作符

你的汇编代码不是最优的,可以改进:

  • 您正在内循环中推入和弹出寄存器(EDX)。应该将其移出循环。
  • 在循环的每次迭代中重新加载数组指针。这个应该移出循环。
  • 你使用loop指令,它是在大多数现代cpu上都非常慢(可能是使用一个古老的汇编书*的结果)
  • 您无法利用手动循环展开。
  • 你没有使用可用的SIMD指令。

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

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

是的,大多数时候。

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

你可以总是生成一个例子,其中手工汇编代码比编译代码更好,但通常是一个虚构的例子或单个例程,而不是一个包含500.000多行c++代码的真正的程序)。我认为编译器可以95%的时间生成更好的汇编代码,你可能需要为一些简短的程序编写汇编代码,比如高度使用性能至关重要例程,或者当你必须访问你最喜欢的高级语言没有公开的特性时。你想感受一下这种复杂的感觉吗?在SO上阅读这个答案很棒

为什么呢?

首先,因为编译器可以进行我们甚至无法想象的优化(参见这个简短的列表),并且它们将在中进行优化(当我们可能需要几天时)。

当你用程序集编写代码时,你必须使用定义良好的调用接口来创建定义良好的函数。然而,他们可以考虑全程序优化inter-procedural优化这样的 例如寄存器分配持续的传播公共子表达式消除指令调度和其他复杂的,不明显的优化(例如多面体模型)。在RISC架构上,人们多年前就不再担心这个问题了(例如,指令调度很难手动调音),而现代CISC cpu也有非常长的管道

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

编译器有时可以自己自动使用一些MMX/SIMDx指令,如果你不使用它们,你根本无法比较(其他答案已经很好地审查了你的汇编代码)。 对于循环,这是编译器检查一般内容的循环优化的简短列表(你认为当你的日程安排已经确定为c#程序时,你可以自己做吗?)如果你用汇编写东西,我认为你必须至少考虑一些简单的优化。数组的教科书示例是展开循环(它的大小在编译时已知)。执行此操作并再次运行测试。
< / p >

如今,由于另一个原因,需要使用汇编语言也真的不常见了:过多的不同cpu。你想支持他们所有人吗?每个都有一个特定的微体系结构和一些特定指令集。他们有不同数量的功能单元和组装指令应该安排,以保持他们都。如果你用C语言编写,你可以使用PGO,但在汇编中,你将需要对特定的体系结构(和重新考虑和重做其他架构的所有内容)有很大的了解。对于较小的任务,编译器通常做得更好,而对于复杂的任务通常,工作是不偿还的(无论如何编译器可能做得更好)。

如果你坐下来看看你的代码,你可能会发现重新设计你的算法会比翻译成汇编语言获得更多(阅读这里是SO的好帖子),在你需要求助于汇编语言之前,你可以有效地应用高级优化(和编译器的提示)。值得一提的是,经常使用intrinsic可以获得所期望的性能提升,而且编译器仍然能够执行大部分优化。

尽管如此,即使你能写出5~10倍快的汇编代码,你也应该问问你的客户,他们是更喜欢支付一周的你的时间还是买一个50美元更快的CPU。大多数人并不需要极端优化(特别是在LOB应用程序中)。

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

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

做的事情:

mov eax,0

成本周期大于

xor eax,eax

它的作用是一样的。

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

甚至在深入研究程序集之前,在更高的级别上就存在代码转换。

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版本,而不是汇编版本,在代码中,可移植性要好得多。

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

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

是的,这就是它的意思,对于每一个语言也是如此。如果你不知道如何用X语言编写高效的代码,那么你就不应该相信自己有能力用X语言编写高效的代码。因此,如果你想要高效的代码,你应该使用另一种语言。

组装对这一点特别敏感,因为,你看到的就是你得到的。您编写希望CPU执行的特定指令。对于高级语言,在两者之间有一个编译器,它可以转换您的代码并消除许多低效率。对于组装,你只能靠自己。

只是盲目地实现完全相同的算法,一条一条指令,在汇编中保证比编译器能做的要慢。

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

当然,打败编译器是可能的,特别是如果它是一个小的,本地化的部分代码,我甚至不得不自己做,以获得一个大约。4倍的速度,但在这种情况下,我们必须在很大程度上依赖于良好的硬件知识和许多看似反直觉的技巧。

我喜欢这个例子,因为它展示了关于低级代码的重要一课。是的,你可以编写的程序集和你的C代码一样快。这是同义的,但不一定的意思是任何东西。显然有人可以,否则汇编器不会知道适当的优化。

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

如果你的编译器生成大量的面向对象支持代码,汇编可能会更快。

编辑:

致持反对态度的选民:OP写道“我应该……专注于c++而忘记汇编语言?”我坚持我的答案。您总是需要关注OO生成的代码,特别是在使用方法时。不要忘记汇编语言,这意味着您将定期检查OO代码生成的程序集,我认为这是编写性能良好的软件的必要条件。

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

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

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

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

首先,你显然是不被它吓倒,这是一个非常非常好的优点,其次-你在正确的轨道上通过分析,以验证或放弃您的速度假设,你要求来自有经验人士的意见,你有人类已知的最好的优化工具:一个大脑

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

为了获得灵感,我建议你查阅迈克尔个的文章(如果你没有听过他的文章,他是一位优化大师;他甚至与John Carmack合作优化了《雷神之锤》的软件渲染器!)

“没有最快的代码”——Michael Abrash

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

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

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代码,那么寻找更好解决方案的过程就会受到影响,最终结果也会变慢。

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

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——而不是“手工优化”的汇编代码。

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

一般来说,我认为在可以从非常有限的上下文中处理每段代码的情况下,汇编语言易于产生最大的性能改进,而在可以从许多不同上下文中处理一段代码的情况下,汇编语言易于对性能造成损害。有趣的是(也很方便),汇编对性能最有利的情况往往是那些代码最直接、最容易阅读的情况。汇编语言代码会变成一团乱麻的地方,往往是那些用汇编语言编写可以提供最小性能好处的地方。

[注意:在有些地方,汇编代码可以被用来产生一团超级优化的糊状;例如,我为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个循环的硬执行时间限制,因此9个周期的获取和分派对于实现这一目标非常有用。使用一个包含256个32位地址的表会快一个周期,但是会占用1KB非常宝贵的RAM (flash会增加多个等待状态)。使用64个32位地址将需要添加一个指令来屏蔽获取的单词的一些位,并且仍然会比我实际使用的表多占用192个字节。使用8位偏移量表产生了非常紧凑和快速的代码,但不是我期望编译器能够实现的;我也不希望编译器“全职”使用寄存器来保存表地址。

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

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

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++中会更慢吗?-这可能是因为你正在用编译器编写汇编代码,而不是一开始就使用汇编程序

我的个人建议是,如果可以避免,就不要写汇编代码,尽管我喜欢汇编。