几年前,我参加了一个小组,该小组正在面试一个相对高级的嵌入式 C 程序员职位的候选人。
我问的标准问题之一是关于优化技术。我很惊讶有些候选人没有答案。
所以,为了给后代整理一份清单——在优化 C 程序时,您通常使用哪些技术和结构?
速度和尺寸优化的答案都被接受。
几年前,我参加了一个小组,该小组正在面试一个相对高级的嵌入式 C 程序员职位的候选人。
我问的标准问题之一是关于优化技术。我很惊讶有些候选人没有答案。
所以,为了给后代整理一份清单——在优化 C 程序时,您通常使用哪些技术和结构?
速度和尺寸优化的答案都被接受。
第一要务——不要过早优化。花时间仔细优化一段代码却发现它并不是您认为的瓶颈,这种情况并不少见。或者,换一种说法“在你让它快速之前,让它工作”
在优化代码之前调查是否有任何优化算法的选项。通过优化一个糟糕的算法比优化代码更容易找到性能的改进,然后当你改变算法时才把它扔掉。
并首先弄清楚为什么需要优化。你想达到什么目的?例如,如果您正在尝试改善对某些事件的响应时间,请确定是否有机会更改执行顺序以最小化时间关键区域。例如,当试图改善对某些外部中断的响应时,您可以在事件之间的死区时间做任何准备吗?
一旦你决定需要优化代码,你会优化哪一点?使用分析器。将注意力(首先)集中在最常用的区域上。
那么你能对这些领域做些什么呢?
上面列出的大多数选项都可以用作正常练习的一部分,而不会产生任何不良影响。但是,如果您真的想获得最佳性能: - 调查可以(安全地)禁用错误检查的位置。不建议这样做,但它会为您节省一些空间和周期。- 在汇编程序中手工制作部分代码。这当然意味着您的代码不再是可移植的,但如果这不是问题,您可能会在这里找到节省。请注意,将数据移入和移出您可以使用的寄存器可能会浪费时间(即满足编译器的寄存器使用)。另请注意,您的编译器本身应该做得很好。(当然,也有例外)
正如其他人所说:配置文件,配置文件配置文件。
至于实际技术,我认为尚未提及的一种技术:
冷热数据分离:保持在 CPU 的缓存中非常重要。帮助做到这一点的一种方法是将数据结构拆分为经常访问(“热”)和很少访问(“冷”)的部分。
一个例子:假设你有一个看起来像这样的客户结构:
struct Customer
{
int ID;
int AccountNumber;
char Name[128];
char Address[256];
};
Customer customers[1000];
现在,让我们假设您想访问很多 ID 和 AccountNumber,但不想访问姓名和地址。你要做的就是把它分成两部分:
struct CustomerAccount
{
int ID;
int AccountNumber;
CustomerData *pData;
};
struct CustomerData
{
char Name[128];
char Address[256];
};
CustomerAccount customers[1000];
这样,当您遍历“客户”数组时,每个条目只有 12 个字节,因此您可以在缓存中容纳更多条目。如果您可以将其应用于渲染引擎的内部循环等情况,这将是一个巨大的胜利。
我最喜欢的技术是使用好的分析器。如果没有一个好的配置文件告诉你瓶颈在哪里,没有任何技巧和技术可以帮助你。
我遇到的最常见的技术是:
至于一般性建议,大部分已经响起:
对于低级优化:
避免使用堆。对相同大小的对象使用 obstacks 或 pool-allocator。将生命周期短的小东西放入堆栈。alloca 仍然存在。
未成熟的优化是万恶之源!;)
由于我的应用程序在设计上通常不需要太多 CPU 时间,因此我专注于我的二进制文件在磁盘和内存中的大小。我主要做的是寻找静态大小的数组,并用动态分配的内存替换它们,这值得稍后释放内存的额外努力。为了减少二进制文件的大小,我寻找在编译时初始化的大数组并将初始化放到运行时。
char buf[1024] = { 0, };
/* becomes: */
char buf[1024];
memset(buf, 0, sizeof(buf));
这将从二进制文件 .DATA 部分中删除 1024 个零字节,并将在运行时在堆栈上创建缓冲区并用零填充它。
编辑:哦,是的,我喜欢缓存东西。它不是特定于 C 的,但取决于您缓存的内容,它可以极大地提高性能。
PS:请在您的清单完成后告诉我们,我很好奇。;)
如果可能,请与 0 进行比较,而不是与任意数字进行比较,尤其是在循环中,因为与 0 的比较通常使用单独的、更快的汇编程序命令来实现。
例如,如果可能,写
for (i=n; i!=0; --i) { ... }
代替
for (i=0; i!=n; ++i) { ... }
这些天来,优化中最重要的事情是:
不要为涉及复制和粘贴代码(如循环展开)或手动重新排序循环的优化而烦恼。编译器在这方面通常比你做得更好,但它们中的大多数都不够聪明,无法撤消它。
基础/一般:
一些实际有帮助的事情:
选择大小/内存:
选择速度(小心):
很难概括...
数据结构:
算法:
低级:
最重要的是:尽早测量,经常测量并且永远不会做出假设,将您的思考和优化基于分析器检索到的数据(请使用PTU)。
另一个提示,性能是应用程序成功的关键,应该在设计时考虑,并且应该有明确的性能目标。
这远非详尽无遗,但应该提供一个有趣的基础。
还有一点没有提到:
在我工作的大多数嵌入式系统上,没有分析工具,所以说使用分析器很好,但不是很实用。
速度优化的第一条规则是—— 找到你的关键路径。
通常你会发现这条路并没有那么长,也没有那么复杂。很难以通用的方式说如何优化它,这取决于你在做什么以及你有能力做什么。例如,您通常希望避免在关键路径上使用 memcpy,因此您需要使用 DMA 或优化,但是如果您的硬件没有 DMA 怎么办?如果不重写它,请检查 memcpy 实现是否是最好的。
根本不要在嵌入式中使用动态分配,但如果出于某种原因这样做,请不要在关键路径中使用。
正确组织您的线程优先级,正确的是真正的问题,并且显然是系统特定的。
我们使用非常简单的工具来分析瓶颈,存储时间戳和索引的简单宏。在 90% 的情况下,很少 (2-3) 次运行会找到您花费时间的地方。
最后一个是代码审查 ,非常重要。在大多数情况下,我们在代码审查期间避免性能问题是非常有效的方法:)
此外,您应该衡量性能。
收集代码执行的配置文件可以让你完成 50% 的工作。其他 50% 负责分析这些报告。
此外,如果您使用 GCC 或 VisualC++,您可以使用“配置文件引导优化”,编译器将从以前的执行中获取信息并重新安排指令以使 CPU 更快乐。
内联函数!受到分析爱好者的启发,我分析了我的一个应用程序,发现了一个小函数,可以对 MP3 帧进行一些位移。它在我的应用程序中进行了大约 90% 的函数调用,因此我将其设为内联,瞧——该程序现在使用的 CPU 时间是以前的一半。
如果有人对这个问题没有答案,那可能是他们知道的不多。
也可能是他们知道很多。我知道很多(恕我直言:-),如果我被问到这个问题,我会问你:你为什么认为这很重要?
问题是,任何关于性能的先验概念,如果它们没有受到特定情况的影响,那么根据定义,它们都是猜测。
我认为了解编码技术以提高性能很重要,但我认为知道不使用它们更重要,直到诊断表明存在问题以及问题是什么。
现在我要自相矛盾地说,如果你这样做,你将学习如何识别导致麻烦的设计方法,以便避免它们,对于新手来说,这听起来像是过早的优化。
举一个具体的例子,这是一个经过优化的 C 应用程序。
有时谷歌是最好的算法优化工具。当我遇到一个复杂的问题时,稍微搜索一下就会发现一些拥有博士学位的人已经找到了这个问题和一个众所周知的问题之间的映射,并且已经完成了大部分工作。
很棒的清单。我将添加一个我在上面列表中没有看到的提示,在某些情况下可以以最小的成本产生巨大的优化。
绕过链接器
如果您有一些应用程序分为两个文件,比如 main.c 和 lib.c,在许多情况下,您只需\#include "lib.c"
在 main.c 中添加一个,这将完全绕过链接器并允许对编译器进行更有效的优化。
优化文件之间的依赖关系可以达到同样的效果,但更改的成本通常更高。
I would recommend optimizing using more efficient algorithms and not do it as an afterthought but code it that way from the start. Let the compiler work out the details on the small things as it knows more about the target processor than you do.
For one, I rarely use loops to look things up, I add items to a hashtable and then use the hashtable to lookup the results.
For example you have a string to lookup and then 50 possible values. So instead of doing 50 strcmps, you add all 50 strings to a hashtable and give each a unique number ( you only have to do this once ). Then you lookup the target string in the hashtable and have one large switch with all 50 cases ( or have functions pointers ).
When looking up things with common sets of input ( like css rules ), I use fast code to keep track of the only possible solitions and then iterate thought those to find a match. Once I have a match I save the results into a hashtable ( as a cache ) and then use the cache results if I get that same input set later.
My main tools for faster code are:
hashtable - for quick lookups and for caching results
qsort - it's the only sort I use
bsp - for looking up things based on area ( map rendering etc )