历史上推荐使用的 x86 中哪些类型的 C 优化实践不再有效?

Posted

技术标签:

【中文标题】历史上推荐使用的 x86 中哪些类型的 C 优化实践不再有效?【英文标题】:What kinds of C optimization practices in x86 that were historically recommended to use are no longer effective? 【发布时间】:2014-06-05 20:24:19 【问题描述】:

由于 x86 C 编译器(即 GCC 和 Clang)的进步,许多被认为可以提高效率的编码实践已不再使用,因为编译器可以比人类更好地优化代码(例如 bit shift vs. multiplication) .

这些具体做法是什么?

【问题讨论】:

我不同意关闭的决定。有些做法显然是有益的,但现在显然适得其反。这些建议通常基于确凿的事实,而这些确凿的事实会发生变化,从而改变建议。而且这种变化也是一个不争的事实。 也许“基于意见”是错误的,但“过于宽泛”肯定不是。我的意思是,每个人都应该留下一个答案还是应该一个人发布每个答案?我们是在谈论 x86 还是应该考虑 ARM 和 PPC 处理器?哪个编译器以及在什么优化级别?一个更好的问题是“历来推荐优化 x [需要引用]。它是否仍然适用于 x86-64 程序中的 gcc 等现代编译器?” 我在等待有人说,“全部。完全没有理由优化。编译器会为你做所有事情。它甚至会为你做早餐。” 全部。完全没有理由优化。编译器将为您做所有事情。它甚至可以让你做早餐。 Duff's device, “当 Duff 设备的大量实例从 XFree86 服务器 4.0 版本中删除时,性能有所提高。” 【参考方案1】:

在通常推荐的优化中,考虑到现代编译器,有几个基本上不会有成效的优化包括:

数学变换

现代编译器了解数学,并会在适当的时候对数学表达式进行转换。

现代编译器已经执行了优化,例如将乘法转换为加法,或将常量乘法或除法转换为位移位,即使在低优化级别也是如此。这些优化的例子包括:

x * 2  ->  x + x
x * 2  ->  x << 1

请注意,某些特定情况可能会有所不同。比如x &gt;&gt; 1x / 2不一样;用一个代替另一个是不合适的!

此外,许多建议的优化实际上并不比它们替换的代码快。

愚蠢的代码技巧

我什至不知道该怎么称呼它,但是像 XOR 交换 (a ^= b; b ^= a; a ^= b;) 这样的技巧根本不是优化。它们只是派对技巧——它们比明显的方法更慢,更脆弱。不要使用它们。

register 关键字

许多现代编译器都忽略了这个关键字,因为它的预期含义(强制将变量存储在寄存器中)在当前寄存器分配算法中没有意义。

代码转换

编译器会在适当的时候自动执行各种代码转换。一些经常被推荐用于手动应用但在应用时很少有用的此类转换包括:

循环展开。(如果不加选择地应用,这通常实际上是有害的,因为它会增加代码大小。) 函数内联。(将函数标记为static,启用优化时通常会在适当的地方内联。)

【讨论】:

循环展开和强制内联在正确完成时仍然可以提供巨大的加速。你提到的其他一切现在几乎都是正确的。 @Mysticial:循环展开很少有用,除非循环内容非常微不足道,并且在大多数情况下,编译器可以正确展开它。但是,我发现手动展开仍然可以胜过编译器可以做的任何事情。我有一个疯狂的strlen 实现,它只基于一种特定形式的展开(以及渐近 100% 正确的分支预测的好处),它在大多数(如果不是全部)拱门上超过了你可以做的任何事情,而无需 >=64 位矢量化. @R.. 在我遇到的大多数情况下,循环体是 100+ 周期的高延迟浮点指令依赖链。编译器拒绝展开它,因为它太大了。并且主体太大而无法放入 CPU 重新排序缓冲区。因此,通过手动展开和交错迭代可以获得很多 ILP。 @Mysticial:你能用-funroll-all-loops编译这样的文件,或者在函数上使用等效的#pragma__attribute__吗?显然,编译器进行交错处理会很好,我认为只要避免别名问题(例如使用 restrict 关键字)就可以。 @R.. 除了小型 sn-ps 进行实验之外,我从未尝试过它。我发现的主要问题是,如果正文太大,编译器不会交错。原因是调度算法在 O(N^2) 中运行。因此,当您拥有(根据我的经验)超过几百个时,编译器会吓坏并退回到其他东西。交错对我来说是微不足道的,因为我对数据依赖性有全面的了解。但对于编译器来说,它只是一个有成百上千个节点和边的 DAG——这需要大量的工作来处理。【参考方案2】:

一种这样的做法是通过使用数组指针数组而不是真正的二维数组来避免乘法。


老做法:

int width = 1234, height = 5678;
int* buffer = malloc(width*height*sizeof(*buffer));
int** image = malloc(height*sizeof(*image));
for(int i = height; i--; ) image[i] = &buffer[i*width];

//Now do some heavy computations with image[y][x].

这曾经更快,因为乘法过去非常昂贵(大约 30 个 CPU 周期),而内存访问实际上是免费的(直到 1990 年代才添加缓存,因为内存无法跟上以全 CPU 速度运行)。


但是乘法变得很快,一些 CPU 能够在一个 CPU 周期内完成它们,而内存访问根本跟不上。所以,现在这段代码可能会更高效:

int width = 1234, height = 5678;
int (*image)[width] = malloc(height*sizeof(*image));

//Now do some heavy computations with image[y][x],
//which will invoke pointer arithmetic to calculate the offset as (y*width + x)*sizeof(int).

目前,仍有一些 CPU,其中第二个代码并不快,但我们不再需要大乘法惩罚了。

【讨论】:

技术性:在第二个例子中你不会乘以sizeof(int);编译器会为您处理缩放。 @JonathanLeffler 如果我没记错的话,这个乘法实际上是作为 X86 CPU 上加载指令的一部分完成的。如果它被编译成自己的指令,它通常会编译成位移位。在任何一种情况下,都必须在某处显式或隐式地计算以字节为单位的实际物理偏移量,这涉及乘以 int 的大小。 次要:不应该是例子image[i] = buffer[i*width] --> image[i] = &amp;buffer[i*width],缺少&amp; @chux 是的。谢谢。固定。【参考方案3】:

由于有多个平台,您最多只能针对给定平台(或 CPU 架构/模型)和编译器进行优化!!如果您的代码在许多平台上运行,那是在浪费时间。 (我说的是微优化,总是值得考虑更好的算法)

这表示针对给定平台进行优化,如果需要,DSP 是有意义的。那么最好的第一个助手是恕我直言,如果编译器/优化器很好地支持它,那么明智地使用 restrict 关键字。避免涉及条件和跳跃代码(breaks、goto、if、while、...)的算法这有利于流式传输并避免过多的错误分支预测。我同意这些提示现在是常识

一般来说,我会说:任何通过假设编译器如何优化来修改代码的操作,恕我直言,应该完全避免。

相反,然后切换到汇编(对于 DSP 中一些非常重要的算法的常见做法,其中编译器虽然非常出色,但仍然错过了最后几个 % 的 CPU/Mem 周期性能提升......)

【讨论】:

【参考方案4】:

真正不应该更多使用的优化是#define(稍微扩展了duskwuff的答案)。

C 预处理器是一个很棒的东西,它可以做一些惊人的代码转换,它可以让某些非常复杂的代码变得更简单——但是使用 #define 只是为了让一个小操作被内联不是 通常再合适不过了。大多数现代编译器都有一个真正的inline 关键字(或等效关键字,如@9​​87654324@),而且它们足够聪明,可以内联大多数static 函数,这意味着这样的代码:

#define sum(x, y) ((x) + (y))

写成等效函数真的更好:

static int sum(int x, int y)

    return x + y;

您避免了危险的多重评估问题和副作用,您获得了编译器类型检查,并且您最终也得到了更清晰的代码。如果值得内联,编译器会这样做。

一般来说,将预处理器保存在需要的情况下:快速发出大量复杂的变体代码或部分代码。使用预处理器内联小函数和定义常量现在主要是一种反模式。

【讨论】:

以上是关于历史上推荐使用的 x86 中哪些类型的 C 优化实践不再有效?的主要内容,如果未能解决你的问题,请参考以下文章

什么是形参和实参?参数传递的方式都有哪些?

基于 Flink 实现的商品实时推荐系统(附源码)

Hive 中推荐的优化技术都有哪些?

为新指令集扩展优化编译的代码的向后兼容性

为 x86 架构开发操作系统 [关闭]

如何在 x86 实模式下正确设置 SS、BP 和 SP?