Is inline assembly language slower than native C++ code?
我尝试比较内联汇编语言和C++代码的性能,所以我写了一个函数,将两个大小为2000的数组加起来100000次。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | #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 }; } |
这是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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<<" "; cout<<"time ="<<end - start<<endl; return 0; } |
然后我运行程序五次,以获得处理器的周期,这可以看作是时间。每次我只调用上面提到的函数之一。
结果来了。
程序集版本的功能:1 2 3 4 5 6 7 8 | Debug Release --------------- 732 668 733 680 659 672 667 675 684 694 Average: 677 |
C++版本的功能:
1 2 3 4 5 6 7 8 | Debug Release ----------------- 1068 168 999 166 1072 231 1002 166 1114 183 Average: 182 |
发布模式中的C++代码比汇编代码快3.7倍。为什么?
我想我编写的汇编代码不如GCC生成的代码有效。对于像我这样的普通程序员来说,编写代码比由编译器生成的对手要快得多。这是否意味着我不应该相信我手写的汇编语言的性能,专注于C++而忘记汇编语言?
是的,大多数时候。
首先,你从错误的假设出发,一种低级语言(在这种情况下的程序集)总是会产生比高级语言(在这种情况下C++和C)更快的代码。这不是真的。C代码总是比Java代码快吗?不,因为还有另一个变量:程序员。您编写代码的方式和体系结构细节的知识极大地影响了性能(正如您在本例中看到的)。
你总是可以产生一个例子,手工汇编代码比编译代码好,但通常它是虚构的例子或单个例程,而不是一个500行C++代码的真实程序。我认为编译器将产生95%的更好的汇编代码,有时,只有很少的时候,您可能需要为很少的、短的、高度使用的、性能关键的例程编写汇编代码,或者当您必须访问您最喜欢的高级语言不公开的功能时。你想了解一下这种复杂性吗?在这里读这个很棒的答案。
为什么会这样?
首先,因为编译器可以进行我们甚至想象不到的优化(参见下面的简短列表),而且它们将在几秒钟内完成优化(当我们可能需要几天的时候)。
在程序集中编码时,必须使用定义良好的调用接口生成定义良好的函数。但是,它们可以考虑整个程序优化和过程间优化,例如作为寄存器分配、常数传播、公共子表达式消除、指令调度等复杂、不明显的优化(例如polytope模型)。在RISC体系结构方面,很多年前人们就不再担心这一点了(例如,指令调度很难手工调整),而现代的CISC CPU也有很长的管道。
对于一些复杂的微控制器,甚至系统库都是用C语言编写的,而不是用汇编语言编写的,因为它们的编译器可以生成更好(并且易于维护)的最终代码。
编译器有时可以自己自动使用一些mmx/simdx指令,如果不使用它们,则无法进行比较(其他答案已经很好地检查了汇编代码)。仅对于循环,这是一个简短的循环优化列表,列出了编译器通常检查的内容(您认为在为C程序确定计划时,您可以自己进行优化吗?)如果您在汇编中编写一些东西,我认为您至少需要考虑一些简单的优化。数组的教科书示例是展开循环(其大小在编译时已知)。再做一次测试。< BR>
如今,由于另一个原因,使用汇编语言也很少见:不同CPU的数量过多。你想支持他们吗?每个都有一个特定的微体系结构和一些特定的指令集。它们有不同数量的功能单元,并且应该安排组装说明,以使它们都很忙。如果您用C编写,您可以使用PGO,但是在汇编中,您将需要对特定的体系结构有很好的了解(并重新考虑和重做其他体系结构的所有内容)。对于小任务,编译器通常会做得更好,而对于复杂任务,工作通常不会得到回报(而且编译器也可能做得更好)。
如果你坐下来看一看你的代码,你可能会发现重新设计你的算法比翻译到程序集(阅读这篇伟大的文章在这里,所以),有高级优化(和提示编译器),你可以有效地应用之前,你需要诉诸汇编语言。可能值得一提的是,经常使用内部函数可以获得所需的性能,编译器仍然能够执行大部分优化。
所有这些都意味着,即使你能以5到10倍的速度生成汇编代码,你也应该问你的客户是愿意花一周的时间还是愿意花50美元买一个更快的CPU。我们大多数人根本不需要极端优化(尤其是在LOB应用程序中)。
您的装配代码异常差略次优,可改善:
- 您正在内部循环中推送和弹出寄存器(EDX)。这应该从循环中移出。
- 在循环的每个迭代中重新加载数组指针。这应该从循环中移出。
- 您使用
loop 指令,这在大多数现代CPU上都是非常慢的(可能是由于使用了一本古老的汇编书*) - 你没有利用手动循环展开。
- 您不使用可用的SIMD指令。
因此,除非您大幅提高了汇编程序方面的技能集,否则编写汇编程序代码以提高性能是没有意义的。
*当然,我不知道你是否真的从一本古老的汇编书中得到了
甚至在delving会议中,有transformations code,美元的高层次的存在。 P / < >
1 2 3 4 5 6 7 8 9 | 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]; } } } |
可以通过transformed成环rotation: P / < >
1 2 3 4 5 6 7 8 9 | 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]; } } } |
这是美好的记忆locality尽去。 P / < >
这可能是optimizes进一步,做
1 2 3 4 5 6 7 | 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]; } } |
然而,似乎它是我最喜欢的optimizer(llvm)并没有执行这个变换。 P / < >
[编辑]我的发现,也performed变换,如果我们让"
无论如何,这是,我认为,optimized C版本。它已经太多simpler。基于这一点,这里是我的裂纹在ASM(我让它generate哗,我是无用的在它): P / < >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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 |
我害怕,我不明白所有这些指令从哪里来,然而你不能总是有乐趣和尝试和看到它比………………但我仍然使用D * optimized C版本的(而不是一个,在代码,多更多的便携式。 P / < >
简短回答:是的。
长话短说:是的,除非你真的知道自己在做什么,并且有理由这么做。
我已经修复了我的ASM代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | __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 }; |
发布版本的结果:
1 2 | Function of assembly version: 81 Function of C++ version: 161 |
释放模式下的汇编代码比C++快2倍。
Does that mean I should not trust the performance of assembly language written by my hands
是的,这也意味着它到底是什么,和它是真实的为每一个语言。如果你不知道如何去写一个高效代码语言在X,然后你不应该信任你的能力到高效代码写在X。所以,如果你想要高效的代码,你应该用另一种语言。 P / < >
(也particularly sensitive到这,因为,嗯,你看到的是什么,你得到的。你写的特定指令,你想execute到CPU。与高层次的languages,有一compiler在betweeen,这可以transform你的代码和remove多inefficiencies。与会议,你会对自己的。 P / < >
现在使用汇编语言的唯一原因是使用一些语言无法访问的特性。
这适用于:
- 需要访问某些硬件功能(如MMU)的内核编程
- 使用编译器不支持的非常特定的矢量或多媒体指令的高性能编程。
但是当前的编译器非常聪明,它们甚至可以替换两个独立的语句,比如
的确,现代编译器在代码优化方面做得非常出色,但我仍然鼓励您继续学习汇编。
首先,你显然没有被它吓倒,这是一个伟大的,伟大的加上,下一步-你在正确的轨道上通过分析来验证或放弃你的速度假设,你要求有经验的人输入,你有人类已知的最伟大的优化工具:大脑。
随着您的体验的增加,您将了解何时何地使用它(通常是代码中最紧密、最内层的循环,在您在算法级别进行了深度优化之后)。
为了获得灵感,我建议你查阅Michael Abrash的文章(如果你没有收到他的消息,他是一位优化大师;他甚至与John Carmack合作优化Quake软件渲染器!)
"there ain't no such thing as the fastest code" - Michael Abrash
我已更改ASM代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | __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 }; |
发布版本的结果:
1 2 | Function of assembly version: 41 Function of C++ version: 161 |
释放模式下的汇编代码比C++快4倍。imho,汇编代码的速度取决于程序员
现在,高层次的languages compilers是很optimized和知道他们是这样做的。你可以尝试和dump的disassemble代码和比较它与你的本地的会议。我相信你会看到一些好的tricks你compiler也使用。 P / < >
只为例子,即使是我不确定它是正确的:任何更多): P / < >
这样做: P / < >
1 | mov eax,0 |
更多的成本比周期 P / < >
1 | xor eax,eax |
这一点不做同样的事情。 P / < >
"compiler知道所有这些tricks和他们的用途。 P / < >
这是一个非常有趣的话题!我已经用萨沙密码更改了SSE的MMX以下是我的结果:
1 2 3 4 | Function of C++ version: 315 Function of assembly(simply): 312 Function of assembly (MMX): 136 Function of assembly (SSE): 62 |
SSE的汇编代码比C++快5倍。
"compiler打败你。我会给它一个尝试,但我不会让任何guarantees。我将为此承担,"multiplication"由时报也意味着可以让它的身体,更多的相关性能,
1 2 3 4 5 6 7 8 9 10 | 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 |
就像我说的,我做不guarantees。但我会感到惊讶,如果它不能做得太多,更快的bottleneck人机记忆throughput即使我一切的L1的信仰。 P / < >
只是blindly实施精确的相同的算法,通过指令指令,在议会也保证要slower比什么compiler能做的。 P / < >
* * *因为即使smallest optimization compiler确实也比你的代码没有optimization刚性与所有的。 P / < >
当然,它也可能给打败的compiler,尤其是如果它是一个很小的一部分,localized code),即使让我做它自己得到一个approx速度。4包起来,但在这种情况下,我们需要很好的英语知识heavlly依赖在硬件和numerous,seemingly反- intuitive tricks。 P / < >
这正是它的意义所在。将微优化留给编译器。
我爱这个例子因为它的一个重要的demonstrates lesson关于低层次的代码。是的,你可以写一个会议,是个快速你的C代码。这tautologically真的,不过没有necessarily意味着什么。有人能明确,另有"assembler就不会知道的optimizations适当的情况下。 P / < >
likewise,同样的principle applies岁你去了"层次结构"的语言abstraction。是的,你可以写一个parser在C,20世纪年代快速的快速和肮脏的Perl脚本,和许多人做的。但这并不意味着,因为你用C代码,你将进入快速。在许多案例,更高层次的languages做optimizations,你可能永远不会有甚至被视为。 P / < >
作为一个编译器,我将用一个固定大小的循环替换很多执行任务。
1 2 3 4 | int a = 10; for (int i = 0; i < 3; i += 1) { a = a + i; } |
将生产
1 2 3 4 | int a = 10; a = a + 0; a = a + 1; a = a + 2; |
最终它会知道"a=a+0";是无用的,所以它会删除这一行。希望你头脑中的某些东西现在愿意附加一些优化选项作为评论。所有这些非常有效的优化都将使编译语言更快。
在许多情况下,执行某些任务的最佳方式可能取决于执行任务的上下文。如果一个程序是用汇编语言编写的,通常不可能根据上下文改变指令序列。作为一个简单的例子,考虑以下简单的方法:好的。
1 2 3 4 | inline void set_port_high(void) { (*((volatile unsigned char*)0x40001204) = 0xFF); } |
上面给出的32位ARM代码的编译器可能会将其呈现为:好的。
1 2 3 4 | ldr r0,=0x40001204 mov r1,#0 strb r1,[r0] [a fourth word somewhere holding the constant 0x40001204] |
或者也许好的。
1 2 3 4 | 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] |
这可以在手工汇编的代码中稍微优化,如下所示:好的。
1 2 3 | ldr r0,=0x400011FF strb r0,[r0+5] [a third word somewhere holding the constant 0x400011FF] |
或好的。
1 2 3 | 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版本优化为:好的。
1 | strb r7,[r5+0x204] |
比手工优化的汇编代码更短、更快。此外,假设set_port_high出现在上下文中:好的。
1 2 3 | int temp = function1(); set_port_high(); function2(temp); // Assume temp is not used after this |
在为嵌入式系统编码时一点也不令人难以置信。如果
请注意,在程序员知道精确的程序流的情况下,手工优化的汇编代码通常优于编译器,但编译器在一段代码在其上下文已知之前被写入,或者一段源代码可以从多个上下文调用的情况下(如果
总的来说,我建议汇编语言在每段代码都可以从非常有限的上下文访问的情况下,能够产生最大的性能改进,并且在从许多不同的上下文访问一段代码的情况下,很容易对性能产生不利影响。有趣的是(也很方便地)程序集对性能最有利的情况通常是代码最直接和最容易阅读的情况。汇编语言代码会变成一团糟的地方,通常是那些在汇编中编写可以提供最小性能好处的地方。好的。
[小提示:有些地方可以使用汇编代码来产生超优化的粘性混乱;例如,我为ARM编写的一段代码需要从RAM中提取一个单词,并根据值的上六位执行大约12个例程中的一个(许多值映射到同一个例程)。我想我把代码优化为:好的。
1 2 3 | 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毫秒间隔2毫秒。好的。好啊。
最近,我所做的所有速度优化都是用合理的代码替换大脑受损的慢代码。但是对于速度非常关键的事情,我投入了大量的精力来加快速度,结果总是一个迭代过程,每次迭代都能让我们对问题有更多的了解,找到用更少的操作来解决问题的方法。最终的速度总是取决于我对这个问题有多深入。如果在任何阶段我使用过优化的汇编代码或C代码,寻找更好的解决方案的过程都会受到影响,最终结果会变慢。
C++ is faster unless you are using assembly language with deeper
knowledge with the correct way.
当我在ASM中编码时,我手动重新组织指令,以便在逻辑上可能的情况下,CPU可以并行执行更多指令。我在ASM中编码时几乎不使用RAM,例如:ASM中可能有20000多行代码,我从来没有使用过push/pop。
您可能会跳到操作码的中间来自我修改代码和行为,而不会受到自我修改代码的可能惩罚。访问寄存器需要1个时钟周期(有时需要0.25个时钟周期),访问RAM可能需要数百个时钟周期。
在上一次ASM冒险中,我从未使用RAM存储变量(存储数千行ASM)。ASM可能比C++快得不可思议。但这取决于许多可变因素,如:
1 2 | 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,它的速度很慢。这是因为仅仅对GPU进行3D编程的概念使得向GPU发送命令的语言速度几乎不相关。
光金属装配工的速度是无可争辩的。C++内部的速度会更慢吗?-这可能是因为编写程序集代码时使用的编译器不使用汇编程序。
我个人的理事会是永远不会写汇编代码,如果你能避免它,即使我喜欢汇编。
这里的所有答案似乎都排除了一个方面:有时我们编写代码不是为了实现特定的目标,而是为了纯粹的乐趣。花时间这样做可能不经济,但可以说,没有比用手工滚动的asm替代方法在速度上击败最快的编译器优化代码片段更令人满意的了。
C++编译器将在组织级优化后,生成利用目标CPU内置函数的代码。HLL永远不会超过或超过执行汇编程序,原因有几个:1.)HLL将被编译和输出,带有访问器代码、边界检查和可能内置的垃圾收集(以前在OOP手册中的寻址范围),所有这些都需要周期(翻转和翻转)。HLL现在做的很好(包括较新的C++和其他类似GO),但是如果它们优于汇编程序(即你的代码),你需要查阅CPU文档——与草率代码的比较肯定是不确定的,并且汇编的Langs,如汇编程序,都归结为OP代码HLL摘要的细节,并没有消除。否则,如果主机操作系统识别出你的应用程序,它就不会运行。
大多数汇编程序代码(主要是对象)输出为"headless",用于包含到其他可执行格式中,所需处理量要小得多,因此它将更快,但更不安全;如果汇编程序输出可执行文件(nasm、yasm等),则它仍将运行得更快,直到它与功能中的hll代码完全匹配,然后重新结果可能会被精确称重。
以任何格式从HLL调用基于汇编程序的代码对象,除了内存空间调用外,还会固有地增加处理开销,使用全局分配的内存来处理变量/常量数据类型(这适用于LLL和HLL)。记住,最终输出最终使用CPU作为其API和ABI相对于硬件(操作码),汇编程序和"HLL编译器"本质上/根本上是相同的,唯一真正的例外是可读性(语法)。
使用fasm的汇编程序中的hello world控制台应用程序是1.5kb(在freebsd和linux中,这在windows中甚至更小),比GCC在最好的一天所能抛出的任何东西都好;原因包括隐式填充nops、访问验证和边界检查等等。真正的目标是干净的hll-libs和一个可优化的编译器,它以"硬核"的方式瞄准一个CPU,大多数情况下(最后)都是这样。GCC并不比Yasm好——这是开发人员的编码实践和理解问题所在,"优化"是在新手探索和临时培训和经验之后出现的。
编译器必须在与汇编程序相同的操作码中链接和汇编以供输出,因为这些代码是CPU将要执行的全部操作(cisc或risc[pic too])。Yasm在早期的NASM上进行了大量优化和清理,最终加速了汇编程序的所有输出,但即使如此,Yasm仍然像NASM一样,代表开发人员生成具有外部依赖性的面向OS库的可执行文件,因此里程可能有所不同。在结束时,C++在一个点上是难以置信的,而且比汇编语言要安全得多80个百分点,尤其是在商业领域…
如果编译器生成大量的OO支持代码,则汇编速度可能更快。
编辑:
给落选者:该报写道:"我应该……关注C++,忘记汇编语言吗?"我坚持我的回答。您总是需要关注OO生成的代码,特别是在使用方法时。不要忘记汇编语言意味着您将定期检查OO代码生成的汇编,我认为这是编写性能良好的软件所必需的。
实际上,这适用于所有可编译的代码,而不仅仅是OO。