我试图比较内联汇编语言和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
Debug Release
-----------------
1068 168
999 166
1072 231
1002 166
1114 183
Average: 182
发布模式下的C ++代码几乎是汇编代码的3.7倍。为什么呢?
我想我写的汇编代码不如GCC生成的那样有效。像我这样的普通程序员很难比编译器生成的对手更快地编写代码。这是否意味着我不应该相信我手写的汇编语言的表现,专注于C ++而忘记汇编语言?
答案 0 :(得分:239)
是的,大多数时候。
首先,你从一个错误的假设开始,即低级语言(在这种情况下是汇编)总是会产生比高级语言更快的代码(在这种情况下是C ++和C)。这不是真的。 C代码总是比Java代码快吗?不,因为还有另一个变量:程序员。编写代码和体系结构细节知识的方式极大地影响了性能(正如您在本例中所见)。
您可以始终生成一个示例,其中手工汇编代码比编译代码更好但通常它是虚构的示例或单个例程而不是 true 程序500.000+行的C ++代码)。我认为编译器会产生95%的更好的汇编代码和有时,只有一些罕见的时间,您可能需要为少数,highly used,performance critical例程编写汇编代码或者当您必须访问功能时,您最喜欢的高级语言不会暴露。你想要触及这种复杂性吗?在此处阅读this awesome answer。
为什么会这样?
首先,因为编译器可以进行我们甚至无法想象的优化(参见this short list),他们将在秒(当we may need days时)执行这些优化。 / p>
当您在汇编代码中进行编码时,您必须使用定义良好的调用接口来创建定义良好的函数。但是,他们可以考虑whole-program optimization和inter-procedural optimization等帐户 作为register allocation,constant propagation,common subexpression elimination,instruction 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应用程序中)。
答案 1 :(得分:189)
您的汇编代码不是最理想的,可以改进:
loop
指令,known to be dead slow on most modern CPUs(可能是使用古老的装配书*的结果)因此,除非您大大提高了有关汇编程序的技能,否则编写汇编代码以提高性能是没有意义的。
*当然我不知道你是否真的得到了古代装配书的loop
指令。但是你几乎从来没有在真实世界的代码中看到它,因为那里的每个编译器都足够聪明,不能发出loop
,你只能在恕我直言中看到它们过时和过时的书籍。
答案 2 :(得分: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];
}
}
}
可以转换为via 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
限定符添加到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
我担心我不明白所有这些指令来自何处,但是你总是可以玩得开心并尝试看看它是如何比较的...但我仍然使用优化的C版本而不是汇编版本,在代码中,更便携。
答案 3 :(得分:41)
简答:是的。
答案很长:是的,除非你真的知道自己在做什么,并且有理由这样做。
答案 4 :(得分:32)
我修复了我的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倍。
答案 5 :(得分:24)
这是否意味着我不应该相信我手写的汇编语言的表现
是的,这正是它的含义,对于每种语言都是如此。如果你不知道如何在X语言中编写高效的代码,那么你就不应该相信你在X中编写高效代码的能力。所以,如果你想要有效的代码,你应该使用另一种语言。
汇编对此特别敏感,因为,你所看到的就是你得到的东西。您编写了您希望CPU执行的特定指令。使用高级语言,有一个编译器,它可以转换您的代码并消除许多低效率。通过装配,您可以自己动手。
答案 6 :(得分:21)
现在使用汇编语言的唯一原因是使用语言无法访问的某些功能。
这适用于:
但是目前的编译器非常聪明,甚至可以替换两个单独的语句
d = a / b; r = a % b;
只有一条指令可以计算除法和余数(如果可用),即使C没有这样的运算符。
答案 7 :(得分:18)
现代编译器确实在代码优化方面做了出色的工作,但我仍然鼓励你继续学习汇编。
首先,你显然没有被它吓倒,这是一个伟大的,伟大的优势,接下来 - 你通过分析在正确的轨道上以便验证或放弃您的速度假设,您要求经验丰富的人输入,并且您拥有人类已知的最佳优化工具:大脑。
随着您的体验增加,您将了解何时何地使用它(通常是您在算法级别深度优化后,代码中最紧密,最内层的循环)。
为了获得灵感,我建议您查找 Michael Abrash 的文章(如果您没有收到他的消息,他是一名优化专家;他甚至与John Carmack合作优化Quake软件渲染器!)
&#34;没有最快的代码和#34; - 迈克尔·阿布拉什
答案 8 :(得分:13)
我更改了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,汇编代码的速度取决于程序员
答案 9 :(得分:12)
大多数高级语言编译器都经过了优化,并且知道自己在做什么。您可以尝试转储反汇编代码并将其与本机程序集进行比较。我相信你会看到你的编译器正在使用的一些不错的技巧。
就是说,即使我不确定它是否正确:):
这样做的:
mov eax,0
比
花费更多的周期xor eax,eax
做同样的事情。
编译器知道所有这些技巧并使用它们。
答案 10 :(得分:10)
编译器打败了你。我试试看,但我不会保证。我将假设TIMES的“乘法”意味着使其成为更相关的性能测试,y
和x
是16对齐的,length
是非零的无论如何,这可能都是真的。
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命中。
答案 11 :(得分:10)
这是非常有趣的话题!
我在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倍
答案 12 :(得分:6)
只是盲目地实现完全相同的算法,逐个指令,在汇编中保证比编译器可以做的慢。
这是因为即使是编译器所做的最小优化也比没有优化的严格代码更好。
当然,有可能击败编译器,特别是如果它是代码的一个小的,本地化的部分,我甚至不得不亲自去做一个约。 4倍加速,但在这种情况下,我们必须严重依赖对硬件的良好了解和众多看似反直觉的技巧。
答案 13 :(得分: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;”没用,所以它会删除这一行。 希望你脑子里现在愿意附上一些优化选项作为评论。所有这些非常有效的优化将使编译语言更快。
答案 14 :(得分:4)
我喜欢这个例子,因为它展示了关于低级代码的重要教训。是的,您可以编写与C代码一样快的程序集。这在同义反复中是正确的,但不一定意味着任何东西。显然某人可以,否则汇编程序不会知道适当的优化。
同样,当你进入语言抽象的层次结构时,同样的原则也适用。是的,你可以在C中编写一个解析器,它与快速而肮脏的perl脚本一样快,很多人都这样做。但这并不意味着因为你使用了C,你的代码会很快。在许多情况下,更高级别的语言会进行您可能从未考虑过的优化。
答案 15 :(得分:4)
这正是它的含义。将微优化保留给编译器。
答案 16 :(得分:3)
在许多情况下,执行某项任务的最佳方式可能取决于执行任务的上下文。如果例程是用汇编语言编写的,则通常不可能根据上下文改变指令序列。举个简单的例子,考虑以下简单方法:
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个;后者将取代&#34;负载&#34;使用&#34;添加&#34;,这将在ARM7-TDMI上执行两个周期更快。如果代码将在r0不知道/不关心的环境中执行,那么汇编语言版本将比编译版本稍好一些。另一方面,假设编译器知道某些寄存器[例如, r5]将保持一个在所需地址0x40001204的2047字节内的值[例如0x40001000],并进一步知道其他一些寄存器[例如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中的第一个参数,所以&#34;优化&#34;汇编代码需要五条指令。即使编译器不知道任何保存地址或存储值的寄存器,它的四指令版本(它可以适应使用任何可用的寄存器 - 不一定是r0和r1)都会击败&# 34;优化&#34;汇编语言版本。如果编译器如前所述在r5和r7中具有必要的地址和数据,function1
将不会改变这些寄存器,因此它可以用set_port_high
指令替换strb
- < em>四个指令更小更快比&#34;手工优化&#34;汇编代码。
请注意,在程序员知道精确的程序流程的情况下,手动优化的汇编代码通常可以胜过编译器,但编译器会在代码片段在其上下文已知之前编写,或者在一个源代码的情况下发光可以从多个上下文中调用代码[如果在代码中的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位偏移表产生了非常紧凑和快速的代码,但是我不期望编译器会提出这样的代码;我也不希望编译器专门用一个寄存器&#34;全职时间&#34;拿着桌子地址。
上述代码旨在作为一个独立的系统运行;它可以定期调用C代码,但仅限于与其通信的硬件可以安全地放入&#34;空闲&#34;每16ms以两个大约一毫秒的间隔进行状态。
答案 17 :(得分:2)
最近,我所做的所有速度优化都是用合理的代码替换脑损坏的慢速代码。但是对于事情而言,速度确实很关键,我努力做出快速的事情,结果始终是一个迭代过程,每次迭代都能更深入地了解问题,找到如何用更少的操作解决问题的方法。最终的速度总是取决于我对问题的深入了解。如果在任何阶段我使用汇编代码或过度优化的C代码,那么找到更好的解决方案的过程将会受到影响,最终结果会变慢。
答案 18 :(得分:1)
除非您使用更深层次的汇编语言,否则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 ++中它可能更慢吗? - 这可能是因为您正在编写汇编代码,编译器不使用汇编程序来开始。
我的个人理事会是永远不会编写汇编代码,如果你能避免它,即使我喜欢汇编。
答案 19 :(得分:0)
这里的所有答案似乎都排除了一个方面:有时我们不会编写代码来实现特定的目标,而是为了纯粹的有趣。投入时间这样做可能不经济,但可以说没有比用手动滚动的asm替代方案在速度上击败速度最快的编译器优化代码片段更令人满意。
答案 20 :(得分:-2)
在组织级别优化之后,c ++编译器将生成将利用目标cpu的内置函数的代码。由于多种原因,HLL永远不会超过或超过汇编程序; 1.)HLL将被编译并输出Accessor代码,边界检查以及可能内置于垃圾收集(以前在OOP习惯中的范围),所有这些都需要循环(翻转和翻转)。 HLL目前表现非常出色(包括较新的C ++和其他类似GO),但如果它们的表现优于汇编程序(即你的代码),你需要查阅CPU文档 - 与草率代码进行比较肯定是不确定的,编译后的语言就像汇编程序一样解决转到操作代码HLL抽象细节并不消除它们,如果它甚至被主机操作系统识别,你的应用程序也不会运行。
大多数汇编代码(主要是对象)输出为&#34;无头&#34;包含在其他可执行格式中,所需的处理要少得多,因此它会更快,但更不安全;如果汇编程序输出可执行文件(NAsm,YAsm;等),它仍然会运行得更快,直到它与功能中的HLL代码完全匹配,然后可以准确地权衡结果。
除了使用全局分配的内存用于变量/常量数据类型的内存空间调用之外,以任何格式从HLL调用基于汇编程序的代码对象本身也会增加处理开销(这适用于LLL和HLL)。请记住,最终输出最终使用CPU作为其api和abi相对于硬件(操作码)和两者,汇编程序和&#34; HLL编译器&#34;基本上/基本上与唯一真正的例外是可读性(语法)相同。
使用FAsm的汇编程序中的Hello世界控制台应用程序是1.5 KB(这在Windows中甚至在FreeBSD和Linux中更小)并且优于GCC在其最佳日期可以抛弃的任何内容;原因是使用nops进行隐式填充,访问验证和边界检查等等。真正的目标是干净的HLL库和一个可优化的编译器,它针对&#34;硬核&#34;这些天(最后),这种做法和做法最多。海湾合作委员会并不比YAsm更好 - 这是对开发人员的编码实践和理解,这些都是有问题的,并且&#34;优化&#34;在新手探索和临时培训之后来到经验。
编译器必须在与汇编程序相同的操作码中链接和汇编输出,因为这些代码都是CPU除外(CISC或RISC [PIC也])。 YAsm在早期NAsm上进行了优化和清理,最终加快了汇编程序的所有输出,但即便如此,YAsm仍然像NAsm一样,代表开发人员生成具有外部依赖关系的可执行文件,因此里程数可能会有所不同。最后,C ++处于令人难以置信的程度,比80%以上的汇编程序安全得多,特别是在商业领域......
答案 21 :(得分:-3)
如果编译器生成大量OO支持代码,汇编可能会更快。
修改强>
对于downvoters:OP写道“我应该......专注于C ++而忘记汇编语言?”我支持我的回答。您始终需要密切关注OO生成的代码,尤其是在使用方法时。不要忘记汇编语言意味着您将定期查看您的OO代码生成的程序集,我认为这是编写性能良好的软件所必需的。
实际上,这适用于所有可编译代码,而不仅仅是OO。