您使用哪些编码技术来优化C程序?

时间:2008-09-21 10:04:38

标签: c optimization

几年前,我参加了一个小组讨论,该小组正在面试一位相对高级的嵌入式C程序员职位的候选人。

我问过的一个标准问题是优化技术。我很惊讶有些候选人没有答案。

因此,为了为后代制作一个列表,您在优化C程序时通常会使用哪些技术和结构?

接受优化速度和尺寸的答案。

23 个答案:

答案 0 :(得分:37)

答案 1 :(得分:26)

正如其他人所说:个人资料,个人资料个人资料。

至于实际技术,我还没有提到过:

Hot&冷数据分离:保持在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];

通过这种方式,当您循环遍历“customers”数组时,每个条目只有12个字节,因此您可以在缓存中容纳更多条目。如果您可以将它应用于渲染引擎的内部循环等情况,那么这可能是一个巨大的胜利。

答案 2 :(得分:20)

我最喜欢的技巧是使用一个好的探查器。如果没有一个好的概况告诉你瓶颈在哪里,没有任何技巧和技巧可以帮助你。

答案 3 :(得分:15)

我遇到的最常见的技术是:

  • 循环展开
  • 循环优化以获得更好的缓存预取 (即在M个周期中进行N次操作而不是NxM单次操作)
  • 数据对齐
  • 内联函数
  • 手工制作的asm片段

至于一般性建议,其中大多数已经响起:

  • 选择更好的algos
  • 使用profiler
  • 如果不提供20-30%的性能提升,则不进行优化

答案 4 :(得分:8)

对于低级优化:

  1. 来自ffmpeg的START_TIMER / STOP_TIMER宏(用于测量任何代码的时钟级精度)。
  2. Oprofile,当然是用于剖析。
  3. 大量的手工编码程序集(只需在x264的/ common / x86目录上执行wc -l,然后记住大部分代码都是模板化的。)
  4. 一般的仔细编码;更短的代码通常更好。
  5. 智能低级算法,比如我编写的64位比特流编写器,它只使用一个if if和no else。
  6. Explicit write-combining
  7. 考虑处理器的重要奇怪方面,例如Intel's cacheline split issue
  8. 找出一个人可以毫无损失或接近无损地提前终止的情况,其中提前终止检查的成本远低于从中获得的速度。
  9. 实际上为更适合x86 SIMD单元的任务内联汇编,例如中值计算(需要对MMX支持进行编译时检查)。

答案 5 :(得分:5)

  • 首先,使用更好/更快的算法。没有必要优化设计缓慢的代码。
  • 优化速度时,交换内存以提高速度:查找预先计算的值表,二进制树,编写更快速的系统调用自定义实现...
  • 交换内存速度时:使用内存中压缩

答案 6 :(得分:4)

如果可能的话,比较0,而不是任意数字,特别是在循环中,因为与0的比较通常使用单独的,更快的汇编程序命令来实现。

例如,如果可能,写下

for (i=n; i!=0; --i) { ... }

而不是

for (i=0; i!=n; ++i) { ... }

答案 7 :(得分:4)

由于我的应用程序通常不需要太多的CPU时间,我会专注于磁盘和内存中二进制文件的大小。我所做的主要是寻找静态大小的数组,并用动态分配的内存替换它们,这值得额外的努力,以后释放内存。为了减少二进制文件的大小,我寻找在编译时初始化的大数组,并将初始化放入运行时。

char buf[1024] = { 0, };
/* becomes: */
char buf[1024];
memset(buf, 0, sizeof(buf));

这将从二进制文件.DATA部分中删除1024个零字节,而是在运行时在堆栈上创建缓冲区,并用零填充它。

编辑:哦,是的,我喜欢缓存一些东西。它不是C特定的,但取决于你的缓存,它可以给你一个巨大的性能提升。

PS:请在列表完成时告诉我们,我很好奇。 ;)

答案 8 :(得分:4)

预成熟优化是万恶之源! ;)

答案 9 :(得分:4)

避免使用堆。对相同大小的对象使用obstacks或pool-allocator。把寿命短的小东西放到堆栈上。 alloca仍然存在。

答案 10 :(得分:3)

另一件未提及的事情是:

  • 了解您的要求:不要针对不太可能或永远不会发生的情况进行优化,专注于最大的收益

答案 11 :(得分:3)

现在,优化中最重要的事情是:

  • 尊重缓存 - 尝试以简单模式访问内存,并且不要仅仅为了好玩而展开循环。使用数组而不是具有大量指针追踪的数据结构,对于少量数据,它可能会更快。并且不要做太大的事情。
  • 避免延迟 - 如果其他计算立即依赖它们,尽量避免分裂和缓慢的事情。依赖于其他内存访问的内存访问(即a [b [c]])是坏的。
  • 避免不可预测性 - 很多具有不可预测条件的if / elses,或引入更多延迟的条件,都会让你感到困惑。这里有很多无分支的数学技巧,但是它们会增加延迟,只有在你真正需要它们时才有用。否则,只需编写简单的代码,并且没有疯狂的循环条件。

不要为涉及复制和粘贴代码的优化(如循环展开)或手动重新排序循环而烦恼。编译器通常比你这样做做得更好,但是大多数都不够聪明,无法撤消它。

答案 12 :(得分:3)

基础/一般:

  • 当您没有问题时不要进行优化。
  • 了解您的平台/ CPU ......
  • ......彻底知道
  • 了解你的ABI
  • 让编译器进行优化,只需帮助完成工作。

一些实际上有帮助的事情:

选择尺寸/内存:

  • 使用位域存储bools
  • 通过与联合(小心)重叠来重用大型全局数组

选择速度(小心):

  • 尽可能使用预先计算的表格
  • 将关键功能/数据放入快速存储器中
  • 为常用的全局变量使用专用寄存器
  • 计数为零,零标志是免费的

答案 13 :(得分:3)

难以总结......

  • 数据结构:

    • 根据使用情况拆分数据结构非常重要。通常会看到一种结构,其中包含基于流控制访问的数据。这种情况可以显着降低缓存的使用率。
    • 要考虑缓存行大小和预取规则。
    • 重新排序结构成员以从代码中获取对它们的顺序访问
  • 算法:

    • 花些时间考虑问题并找到正确的算法。
    • 了解您选择的算法的局限性(对10个要排序的元素进行基数排序/快速排序可能不是最佳选择)。
  • 低级别:

    • 对于最新的处理器,建议不要展开体积较小的循环。处理器为此提供了自己的检测机制,并将管道的整个部分短路。
    • 信任HW预取器。当然,如果您的数据结构设计得很好;)
    • 关心您的L2缓存线未命中。
    • 尝试尽可能地减少应用程序的本地工作集,因为处理器倾向于每个核心更小的缓存(C2D每个核心最大容量为3MB,其中iCore7将为每个核心提供最大256KB + 8MB共享四核芯片的核心。)。

最重要的是:尽早测量,经常测量,永远不做假设,根据探查器检索的数据进行思考和优化(请使用PTU)。

另一个提示,性能是应用程序成功的关键,应该在设计时考虑,你应该有明确的性能目标。

这远非详尽无遗,但应该提供一个有趣的基础。

答案 14 :(得分:2)

如果有人对这个问题没有答案,可能是他们不太了解。

也可能是他们知道很多。我知道很多(恕我直言:-),如果我被问到这个问题,我会问你回答:为什么你认为这很重要?

问题在于,如果没有特定情况通知,任何关于绩效的先验概念都是根据定义猜测的。

我认为了解性能编码技术很重要,但我认为知道不使用它们更为重要,直到诊断显示存在问题及其原因。

现在我要反驳自己,并说,如果你这样做,你就会学会如何识别导致麻烦的设计方法,以便你可以避免它们,对新手来说,这听起来像是过早优化。

为您举一个具体的例子,this is a C application that was optimized

答案 15 :(得分:2)

有时你必须决定你所追求的是更多的空间还是更高的速度,这将导致几乎相反的优化。例如,为了充分利用您的空间,您需要pack个结构,例如#pragma pack(1)并在结构中使用bit fields。为了提高速度,您需要打包以与处理器首选项保持一致并避免使用位域。

另一个技巧是通过realloc为增长数组选择正确的重新调整大小的算法,或者更好地根据您的特定应用程序编写自己的堆管理器。不要认为编译器附带的那个是每个应用程序的最佳解决方案。

答案 16 :(得分:2)

在我工作的大部分嵌入式系统上都没有分析工具,所以很高兴说使用分析器但不太实用。

速度优化的第一条规则是 - 找到您的关键路径 通常你会发现这条路不是那么长而且不那么复杂。通用方式很难说如何优化它取决于你在做什么以及你有什么能力去做。例如,您通常希望在关键路径上避免使用memcpy,因此您需要使用DMA或优化,但如果您没有DMA,那该怎么办?检查memcpy实现是否是最好的,如果不重写它 在嵌入式设备中根本不使用动态分配,但如果由于某种原因不在关键路径中这样做 正确地组织你的线程优先级,正确的是真正的问题,它显然是系统特定的 我们使用非常简单的工具来分析瓶颈,存储时间戳和索引的简单宏。很少(2-3)在90%的情况下运行会找到你花费时间的地方 最后一个是代码审核非常重要的一个。在大多数情况下,我们在代码审查期间避免性能问题非常有效:):

答案 17 :(得分:2)

  1. 衡量表现。
  2. 使用现实和非平凡的基准。请记住"everything is fast for small N"
  3. 使用分析器查找热点。
  4. 减少动态内存分配,磁盘访问,数据库访问,网络访问和用户/内核转换的数量,因为这些通常往往是热点。
  5. 衡量表现。
  6. 此外,您应该衡量绩效。

答案 18 :(得分:2)

收集代码执行的配置文件可以获得50%的代码。另外50%涉及分析这些报告。

此外,如果您使用GCC或VisualC ++,您可以使用“配置文件引导优化”,其中编译器将从先前的执行中获取信息并重新安排指令以使CPU更快乐。

答案 19 :(得分:2)

内联函数!受到粉丝们的启发,我在这里描述了我的一个应用程序,并发现了一个小功能,可以在MP3帧上进行一些位移。它在我的applcation中占据了所有函数调用的大约90%,因此我将其设置为内联和瞧 - 该程序现在占用了之前的一半CPU时间。

答案 20 :(得分:1)

很棒的名单。我将在上面的列表中添加一个我没有看到的提示,在某些情况下可以以最小的成本产生巨大的优化。

  • 绕过链接器

    如果你有一些应用程序分为两个文件,比如main.c和lib.c,在很多情况下你可以在main.c中添加一个\#include "lib.c"那将完全绕过链接器并允许更多高效优化编译器。

可以实现优化文件之间依赖关系的相同效果,但更改的成本通常更高。

答案 21 :(得分:1)

有时Google是最好的算法优化工具。当我遇到一个复杂的问题时,一些搜索结果显示一些有博士学位的人已经找到了这个和一个众所周知的问题之间的映射,并且已经完成了大部分工作。

答案 22 :(得分:0)

我建议使用更高效的算法进行优化,而不是作为事后的想法,但从一开始就按照这种方式进行编码。让编译器弄清楚小事情的细节,因为它比你更了解目标处理器。

首先,我很少使用循环查找内容,我将项添加到哈希表中,然后使用哈希表查找结果。

例如,您有一个要查找的字符串,然后是50个可能的值。因此,不是执行50次strcmps,而是将所有50个字符串添加到哈希表中,并为每个字符串分配一个唯一的数字(您只需要执行一次)。然后你在哈希表中查找目标字符串,并有一个包含所有50个案例的大型开关(或者有函数指针)。

当使用常见的输入集(例如css规则)查找内容时,我使用快速代码来跟踪唯一可能的搜索,然后迭代思考那些以找到匹配项。一旦我有匹配,我将结果保存到哈希表(作为缓存),然后使用缓存结果,如果我稍后得到相同的输入集。

我提供更快代码的主要工具是:

哈希表 - 用于快速查找和缓存结果

qsort - 这是我使用的唯一一种

bsp - 用于根据区域(地图渲染等)查找内容