几年前,我参加了一个专家小组讨论相关高级嵌入式C程序员职位的候选人.
我问过的一个标准问题是优化技术.我很惊讶有些候选人没有答案.
因此,为了为后代制作一个列表,您在优化C程序时通常使用哪些技术和结构?
接受优化速度和大小的答案.
首先要做的事情 - 不要太早优化.花时间仔细优化一大块代码只是为了发现它不是你认为的瓶颈,这种情况并不少见.或者,换句话说"在你快速制作之前,让它发挥作用"
在优化代码之前,调查是否有任何优化算法的选项.通过优化差的算法比优化代码更容易找到性能的改进,然后在你改变算法的时候抛弃它.
并找出您首先需要优化的原因.你想要实现什么目标?例如,如果您有机会改变执行顺序以最小化时间关键区域,那么您可以尝试改善某些事件的响应时间.例如,当尝试改善对某些外部中断的响应时,您可以在事件之间的死区时间做任何准备吗?
一旦你决定需要优化代码,你会优化哪一点?使用分析器.将注意力(首先)集中在最常用的区域.
那么你可以对这些方面做些什么呢?
最小化条件检查.检查条件(例如,循环的终止条件)是不用于实际处理的时间.可以使用循环展开等技术最小化条件检查.
在某些情况下,也可以通过使用函数指针来消除条件检查.例如,如果您正在实现状态机,您可能会发现将各个状态的处理程序实现为小函数(使用统一原型)并通过存储下一个处理程序的函数指针来存储"下一个状态"比使用具有在各个case语句中实现的处理程序代码的大型switch语句.因人而异.
最小化函数调用.函数调用通常会带来上下文保存的负担(例如,将寄存器中包含的局部变量写入堆栈,保存堆栈指针),因此如果您不必进行调用,则会节省时间.一个选项(如果您要优化速度而不是空间)是使用内联函数.
如果函数调用不可避免,则最小化传递给函数的数据.例如,传递指针可能比传递结构更有效.
优化速度时,请选择适合您平台的本机大小的数据类型.例如,在32位处理器上,操作32位值比8位或16位值更有效.(旁注 - 值得检查编译器是否正在按照您的想法进行操作.我遇到过这样的情况:我发现我的编译器坚持在8位值上进行16位算术,并且所有转换都来自转换和他们一起去)
查找可以预先计算的数据,并在初始化期间计算或在编译时(更好)计算.例如,在实现CRC时,您可以动态计算CRC值(直接使用多项式),这对于大小很好(但对于性能而言可怕),或者您可以生成所有临时值的表 - 这是一个更快的实施,不利于大小.
本地化您的数据.如果您经常操作数据blob,您的处理器可能会通过将其全部存储在缓存中来加快速度.并且您的编译器可能能够使用适合更多本地化数据的更短指令(例如,使用8位偏移而不是32位的指令)
同样,本地化您的功能.出于同样的原因.
计算出您可以对正在执行的操作做出的假设,并找到利用它们的方法.例如,在8位平台上,如果您在32位值上执行的唯一操作是增量,您可能会发现通过内联(或创建宏)专门为此目的,您可以做得比编译器更好,而不是使用正常的算术运算.
避免昂贵的指示 - 划分是一个很好的例子.
"register"关键字可以是您的朋友(虽然希望您的编译器对您的寄存器使用情况有很好的了解).如果您要使用"注册",则可能需要首先声明要"注册"的局部变量.
与您的数据类型保持一致.如果您对混合的数据类型(例如,short和int,double和float)进行算术运算,那么编译器会为每个不匹配添加隐式类型转换.这是浪费的cpu周期,可能没有必要.
上面列出的大多数选项可以作为正常练习的一部分使用,没有任何不良影响.但是,如果您真的想要获得最佳性能: - 调查您可以(安全地)禁用错误检查的位置.不建议这样做,但它可以节省一些空间和周期. - 在汇编程序中手工编写代码的一部分.这当然意味着您的代码不再是可移植的,但如果这不是问题,您可以在这里找到节省.请注意,可能有时间将数据移入和移出您可以使用的寄存器(即,以满足编译器的寄存器使用).还要注意你的编译器本身应该做得很好.(当然,也有例外)
正如其他人所说:个人资料,个人资料.
至于实际技术,我还没有提到过:
热和冷数据分离:保持在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个字节,因此您可以在缓存中容纳更多条目.如果您可以将它应用于渲染引擎的内部循环等情况,那么这可能是一个巨大的胜利.
我最喜欢的技术是使用一个好的探查器.如果没有一个好的资料告诉你瓶颈在哪里,没有任何技巧和技巧可以帮助你.
我遇到的最常见的技术是:
循环展开
循环优化以获得更好的缓存预取(即在M个循环中进行N次操作而不是NxM奇异操作)
数据对齐
内联函数
手工制作的asm片段
至于一般性建议,其中大多数已经响起:
选择更好的算法
使用探查器
如果不能提高20-30%的性能,请不要进行优化
对于低级优化:
来自ffmpeg的START_TIMER/STOP_TIMER宏(用于测量任何代码的时钟级精度).
当然,Oprofile用于剖析.
大量的手工编码汇编(只需在x264的/ common/x86目录上执行wc -l,然后记住大部分代码都是模板化的).
一般的仔细编码; 更短的代码通常更好.
智能低级算法,比如我编写的64位比特流编写器,它只使用一个if和no.
显式写入组合.
考虑到处理器的重要奇怪方面,例如英特尔的高速缓存分裂问题.
找到一个可以无损或接近无损地提前终止的情况,其中提前终止检查的成本远低于从中获得的速度.
实际上为更适合x86 SIMD单元的任务内联汇编,例如中值计算(需要对MMX支持进行编译时检查).
首先,使用更好/更快的算法.没有必要优化设计缓慢的代码.
在优化速度时,交易内存以提高速度:查找预先计算的值表,二进制树,编写更快的系统调用自定义实现...
交换内存速度时:使用内存中压缩