我试图比较内联汇编语言和 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():
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++ 而忘记汇编语言?
是的,大多数时候。
首先,您从错误的假设开始,即低级语言(在这种情况下为汇编)总是会比高级语言(在这种情况下为 C++ 和 C)生成更快的代码。这不是真的。C 代码总是比 Java 代码快吗?不,因为还有另一个变量:程序员。您编写代码的方式和架构细节知识会极大地影响性能(正如您在本例中看到的那样)。
您 总是 可以制作一个示例,其中手工汇编代码比编译代码更好,但 通常 它是一个虚构的示例或单个例程,而不是500.000 多行 C++ 代码的 真实程序)。 我认为编译器会产生 95% 的更好的汇编代码, 有时,只有极少数情况下, 您可能需要为少数、简短、高度使用、性能关键的例程编写汇编代码,或者当您必须访问您最喜欢的高级语言的功能时不暴露。您想了解一下这种复杂性吗?在 SO 上阅读这个很棒的答案。
为什么这个?
首先,因为编译器可以进行我们甚至无法想象的优化(请参阅这个简短列表),并且它们会在 几秒钟内完成 。
当您在汇编中编码时,您必须使用定义良好的调用接口来制作定义良好的函数。然而,它们可以考虑整个程序优化和过程间优化,例如寄存器分配、常量传播、公共子表达式消除、指令调度和其他复杂的、不明显的优化(例如Polytope 模型)。很多年前,在RISC架构上,人们就不再担心这个问题了(例如,指令调度很难手动调整),现代CISC CPU 的流水线很长也。
对于一些复杂的微控制器,甚至 系统 库都是用 C 语言而不是汇编语言编写的,因为它们的编译器会生成更好(且易于维护)的最终代码。
编译器有时可以自己自动使用一些 MMX/SIMDx 指令,如果您不使用它们,您根本无法比较(其他答案已经很好地审查了您的汇编代码)。仅针对循环,这是编译器 通常* 检查的循环优化的简短列表(当您为 C# 程序确定了日程安排后,您认为您可以自己做吗?)如果您用汇编编写一些东西,我认为你至少要考虑一些简单的优化。数组的教科书示例是展开循环(其大小在编译时已知)。这样做并再次运行您的测试。 *
如今,由于另一个原因需要使用汇编语言也很少见:不同的 CPU 过多。你想支持他们吗?每个都有特定的微体系结构和一些特定的指令集。它们有不同数量的功能单元,应安排组装说明以使它们保持 忙碌 。如果你用 C 语言编写,你可能会使用PGO,但在汇编中,你将需要对该特定架构有深入的了解(并 重新思考和重做其他架构的所有内容 )。对于小任务,编译器 通常 会做得更好,而对于复杂的任务,工作 通常 不会得到回报(并且无论如何编译器 可能 会做得更好)。
如果你坐下来看看你的代码,你可能会发现重新设计你的算法比翻译成汇编获得更多(在 SO 上阅读这篇很棒的帖子),有高级优化(和编译器提示),您可以在需要求助于汇编语言之前有效地应用。可能值得一提的是,经常使用内在函数可以获得所需的性能提升,并且编译器仍然能够执行大部分优化。
综上所述,即使您可以生成快 5 到 10 倍的汇编代码,您也应该询问您的客户是否愿意 支付 您一周的 时间 或 购买速度快 50 美元的 CPU 。我们大多数人通常不需要极端优化(尤其是在 LOB 应用程序中)。