提高C代码性能

6
什么是提高C代码性能最不正统的方法?没有任何限制!包括将循环结构更改为goto,硬编码所有内容,以奇怪的方式使用case语句等。完全不用担心可维护性、可读性等问题。
附注:这确实是实用的...我非常清楚如何以合理的方式提高代码性能(改进算法,在优化之前进行分析等)。

1
没有证据表明违背语言和编译器的“优化”能够提高性能。 - Khaled Alshaya
3
改善算法、优化前进行分析等做法,自何时起就变得合理了?如果这是真的,我们就不必花费那么大力气说服人们采取这些做法了。 - jason
我投票支持重新开放。我本来想添加一个答案,即这个链接:https://dev59.com/mnNA5IYBdhLWcg3whuV_#927773 - Mike Dunlavey
15个回答

22

根据我的经验,最不正统的优化C代码的方法是对应用程序进行分析,识别性能较慢的结构或数据库操作,然后使用大O分析设计合理的解决方案。


这并不真正值得+6,因为它并不是非常不正统,而且与问题相悖......但它是合理的,所以我也不能给你点踩:\ - mpen
4
@马克:这是一个笑话,意思是通过“非正统”的方式来进行优化其实是完全合理的,因为很少有人会那样去做。 - nobody
3
更重要的是,如果你建议这个想法,人们往往会像看怪物一样看着你。 - JeffreyABecker
1
直截了当的解决方案 - 简单地统计奇怪 - Andy Dent

6

达夫设备是一个经典的例子。这段代码如此奇异,以至于Tom Duff承认:“这段代码在[case语句中的穿透问题的辩论]中形成了某种争议,但我不确定它是支持还是反对。


6

5

使用内联汇编?

说真的,如果仅通过更改C代码就能提高性能,那么很有可能可以干净地完成。

一些例外:

1)如果您经常依赖于不同类型指针的对齐语义,则通常可以在指针上执行块操作,这些操作从技术上讲会暴露您的应用程序以超出边界的条件,但实际上由于系统的对齐特征而不会。 因此,可以通过对齐初始字符,然后使用long *指针来执行内部块的内存复制。

2)如果您知道编译器分配本地变量的内存顺序,则可能可以巧妙地复制堆栈帧。 这可能允许您实现协程,而语言本身不支持。 协程通常是实现某些类型的循环控制的一种更简单,更快速的方法。

3)无论如何使用,联合体始终有点“hacky”。 这是一种使用相当宽松的类型检查来实现多态性的方法。

4)将C预处理器用作自动生成代码的方式通常非常难以调试和阅读。 因此,人们倾向于避免使用它。


4

对你的代码进行剖析,找出慢的地方,并使用内联汇编来优化它们。


2
当我在游戏公司工作时,我们也这样做。但最终,你会遇到收益递减的情况,必须考虑整体。我们经常发现重新排列数据结构的布局对整体性能有很大影响。 - Nosredna
1
你忘记了第四步:重新进行性能分析,确保你的内联汇编并没有实际上减慢代码。我曾经见过这种情况发生过。 - Dour High Arch
@Nosredna - 我真的很感兴趣您的评论...我正要就此提问。 - tom

4

1) 循环展开。如果不需要循环,每次迭代可以省略跳转、比较和增量操作,从而提高效率。
2) 避免双重间接寻址。通常情况下,执行算术运算比检索更快,因此a[y*height+x]相对于a[y][x]来说更快,并且大小为MxN的一维数组相对于尺寸为MxN的矩形矩阵可以节省M(或N)个指针字长。
3) 在可能的情况下使用荒谬的汇编优化。例如,在x86架构上,您可以使用BSWAP指令以一次操作交换字节,而不是正常的temp=a; a=b; b=temp;模式。

当然,别忘了:
4) 不要进行边界检查或错误处理。

话虽如此,实际应用中我建议避免使用这些技巧,除了(2)。


1
除非大部分是无用的,因为编译器会自动完成它。 - Zan Lynx
2
在大多数情况下,“非正统的优化”是毫无意义的 - 指出一个毫无意义的问题的答案本身就是毫无意义的,有点...无意义。 - Dathan
编译器不能自动完成1和2吗?而且不应该有一个充满汇编技巧的库来处理这些事情吗? - mpen
糟糕。我以为自己很聪明——你实际上说的是“没用的”。唉! - Dathan
我最近没有查看C编译器的机器代码输出,但是由于1和3在我的本科编译器课程中已经涵盖了,我认为它们被广泛实现。 - Dathan

3

您正在寻找一种非正统、无限制但通用的解决方案来优化C语言?

那就用汇编语言重写吧。


3

对于C代码的性能,已经没有什么非正统的方法可用了。所有有效的技术都已经“正统化”。

我发现最好的方法是使用具有CPU性能计数器访问权限的分析工具,并特别关注缓存和分支未命中。在任何地方添加缓存预取并尽可能删除不可预测的分支。

不要费心进行循环展开。如果分支是可预测的,则几乎是免费的。让编译器自己处理。

在某些非常并行的体系结构上,例如IA64,将循环展开到最后可能更快。一个例子是避免使用C字符串函数。使用memset将字符串数组清零,使用memcpy设置字符串,使用memcmp将整个数组与另一个类似的数组进行比较。这可以使用64位加载,永远不必检查零终止符,并且如果使用64或128的“小”数组大小,则可以优化为根本不需要循环或分支。 memxxx()函数通常是编译器内置的,并且非常优化。


3

在Dathan的答案中,对于第三点,还有一种交换变量的非常规方法,可以使用异或运算符。

int = 3, y = 4;
x = x ^ y; 
y = y ^ x; 
x = x ^ y; 

现在x和y已经被交换了! :)

另外,当你将某些东西除以2时,最好使用右移运算符。同样的道理也适用于乘以2时的左移运算符。

在旧的Borland C编译器中,有一个_stklen属性,可以分配它以减少堆栈大小和代码。由于编译器技术得到了提升,我现在没有看到类似的东西。

使用malloc时,最好使用calloc,因为它会将内存初始化为零。

使用三元运算符而不是if / else语句显然更快,我想编译器编写人员在机器代码生成方面变得更加聪明了。我无法提供这方面的证明,但在Borland C 3.01统治时期,这是真实的。

使用汇编程序嵌入代码。

我喜欢这个问题的主题,因为它让我想起了过去,当时内存很宝贵,必须将一品脱装入一个夸脱壶中,并使用x86代码的咒语。感谢您发布这个问题,Database先生。


1
我还应该提到,当处理数组时,如果你已经声明了char some_array[50],那么使用*(some_array + n)会更快...但是现在考虑到编译器技术,这可能已经不相关了... ;) - t0mm13b

2

您的编译器几乎肯定比您丑陋的尝试更擅长优化。大多数历史上使用的小技巧现在都毫无意义。忽略可读性和可维护性的人往往会写出效率较低的代码,因为真正的优化变得更加困难。

当代码已经经过所有可能的优化且仍需要提高性能时,将关键部分重写为ASM是唯一有希望产生任何效果的方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接