>分析和提高C应用程序性能的步骤通常是什么?
>如果我正在为嵌入式系统开发,这些步骤会改变吗?
>哪些工具可以帮助我?
最近,我被赋予了在ARM11平台上提高产品性能的任务.我对这个嵌入式系统领域比较陌生,需要专家来帮助我.
只需更改编译器就可以多次提高相同源代码的C性能. GCC多年来的表现并不一定好,因为某些程序 gcc 3.x产生的代码比4.x更严格.当我访问这些工具时,ARMs编译器生成的代码明显比gcc好.速度提高3到4倍. LLVM已经赶上了GCC 4.x,我怀疑在交叉编译嵌入式代码的性能和总体使用方面会通过gcc.如果您使用gcc,请尝试不同版本的gcc,3.x和4.x. Metaware的编译器和武器adt绕gcc3.x运行,gcc3.x会给gcc4.xa运行它的钱用arm代码,对于拇指代码gcc4.x更好,对于thumb2(这不适用于你)gcc4.x也更好.请记住,我还没有说过改变单行代码(还).除了比gcc更多的调整旋钮之外,LLVM还能够进行全程序优化.尽管生成的代码(版本27)只是在我尝试的少数程序的性能方面赶上当前的gcc 4.x.而且我没有尝试实际数量的优化组合(在编译步骤上优化,每个文件的不同选项,或者组合两个文件或三个文件或所有文件并优化这些包,我的理论是对C到bc没有优化步骤,将所有bc链接在一起,然后对整个程序执行单个优化传递,允许在llc将其带到目标时进行默认优化).
同样,只需了解您的编译器和优化就可以大大提高代码的性能,而无需更改任何代码.你有一个ARM11,你为arm11或通用臂编译?通过告诉编译器特定的通用armv4(ARM7)通常被选为默认的架构/系列(例如armv6),可以获得几个到十几个百分点.如果你勇敢,知道使用-O2或-O3.
通常情况并非如此,但切换到拇指模式可以提高特定平台的性能.不适用于你,但gameboy advance是一个完美的例子,加载非零等待状态16位总线. Thumb有一些百分比的开销,因为它需要更多的指令来做同样的事情,但是通过增加获取时间,并利用gba thumb代码的一些顺序读取功能可以比arm代码快得多地运行相同的源代码.
有一个arm11你可能有一个L1和L2缓存,他们在吗?他们配置了吗?你有一个mmu,你的大量使用内存缓存?或者你正在运行零等待状态内存,不需要缓存,应该将其关闭?除了没有意识到你可以使用相同的源代码并通过更改编译器或选项使其运行速度提高许多倍之后,人们通常不会意识到当你使用缓存时只需在你的启动代码中添加一个最多几个nops(作为调整代码在内存中通过一,二,几个字进行调整的技巧,您可以将代码执行速度更改为10%到20%.在那些使用频繁的函数/循环中遇到的缓存行读取会产生很大的不同.甚至通过调整代码着陆的位置来保存一个高速缓存行读取(例如将其从3切换到2或2到1).
了解您的体系结构,处理器和内存环境都是调优的地方(如果有的话).大多数C库,如果你的高级别足以使用它(我经常不使用C库,因为我在没有操作系统和资源非常有限的情况下运行)在他们的C代码中,有时添加一些汇编程序来制作瓶颈例程,如memcpy,快多了.如果您的程序在对齐的32位甚至更好的64位地址上运行,并且您调整即使它意味着使用少量字节更多的内存,每个结构/数组/ memcpy是32位或64位的整数倍,您将看到显着的改进(如果您的代码使用结构或以其他方式复制数据).除了让你的结构(如果你使用它们,我当然不使用嵌入式代码)大小对齐,即使你浪费内存,使元素对齐,考虑为每个元素使用32位整数而不是字节或半字.根据你的记忆系统,这可能有所帮助(它可能会伤害太多btw).与上面的GBA示例一样,通过剖析或直觉来了解特定函数,您知道这些函数没有以利用您的处理器或平台或库的方式实现,您可能需要从头开始转向汇编程序或从C初始编译然后拆卸和手动调整. Memcpy是一个很好的例子,你可能知道你的系统内存性能,可能会选择专门为对齐数据创建自己的memcpy,每条指令复制64或128或更多位.
同样地,混合全局变量和局部变量可以产生明显的性能差异.传统上人们被告知永远不要使用全局,但在嵌入式中这不一定是真的,取决于你有多深入嵌入,调整和速度以及你感兴趣的其他因素.这是一个敏感的主题,我可能会因此受到抨击,所以我会留下它.
编译器必须刻录和逐出寄存器才能进行函数调用,如果使用局部变量,可能需要堆栈帧,因此函数调用很昂贵,但同时,取决于现在函数中的代码通过避免函数来增加大小,您可能会创建您试图避免的问题,逐出寄存器来重用它们.即使是一行C代码也可以使函数中的所有变量之间的差异适合寄存器,从而必须开始驱逐一堆寄存器.对于您知道需要一些性能增益进行编译和反汇编的函数或代码段(并查看寄存器使用情况,它获取内存或写入内存的频率).您可以并且将找到需要使用良好循环的地方并使其成为自己的函数,即使函数调用有一个惩罚因为这样做,编译器可以更好地优化循环而不是逐出/重用寄存器而你得到一个总净收益.即使循环中的一个额外指令大约数百次也是可测量的性能损失.
希望你已经知道绝对不能编译调试,关闭调试选项的所有编译.你可能已经知道为没有错误运行的调试编译代码并不意味着它被调试,编译用于调试和使用调试器隐藏错误,将它们作为时间炸弹留在代码中以便最终编译发布.学习总是编译以发布和测试发布版本的性能和查找代码中的错误.
大多数指令集没有除法功能.避免在代码中使用除法或模数,尽可能多地使用它们是性能杀手.当然,这不是两个权力的情况,保存编译器和精神上避免分裂和模数尝试使用shift和ands. Multplies在指令集中更容易且更常见,但仍然很昂贵.这是编写汇编程序以进行乘法而不是让C copiler执行此操作的好例子.臂乘法是32位* 32位= 32位所以为了做精确的数学运算而没有溢出,必须在乘法处附加额外的C代码,如果你已经知道你不会溢出,烧掉函数调用的寄存器并进行乘法运算汇编程序(用于手臂).
同样,大多数指令集都没有浮点单元,你可以使用它,即使如此,也可以避免浮动.如果你必须使用浮动,这是一整套其他潘多拉的性能问题.大多数人都没有看到代码的性能问题,如下所示:
float a,b; ... a = b * 7.0;
问题的其余部分是不了解浮点精度以及C库只是试图将常量变为浮点形式的好坏.浮动再次是关于性能问题的另一个长期讨论.
我是Michael Abrash的产品(我实际上有汇编语言的zen打印副本),底线是代码的时间.想出一个准确的代码计时方法,你可能会认为你知道瓶颈在哪里,你可能认为你知道你的架构,但即使你认为它们是错的也会尝试不同的东西,并且你可能会发现并最终必须对它们进行计时弄清楚你的想法中的错误.添加nops到start.S作为最后一个调整步骤就是一个很好的例子,你为性能所做的所有其他工作都可以通过与缓存没有很好的对齐来立即删除,这也意味着重新安排你的功能.源代码使它们落在二进制图像的不同位置.由于缓存行对齐,我已经看到10%到20%的速度增加和减少.