用于优化的编译器提示和语义

时间:2016-11-15 03:34:40

标签: c gcc optimization

我花了最近几周优化数值算法。通过预计算,内存对齐,编译器提示和标志以及试错实验的组合,我将运行时间降低了一个数量级。我还没有使用内在函数或使用多线程进行显式矢量化。

在处理此类问题时,经常会有一个初始化例程,在此之后,许多参数变为常量。这些可能是过滤器长度,switch语句的表达式,循环长度或迭代增量。如果在编译时知道参数,编译器应该能够通过确切地知道如何展开循环,用指令中具有偏移量的指令替换索引计算来简化或消除表达式,从而更有效地进行优化。编译时,可能消除switch语句等。处理这个问题的最极端的方法是运行初始化例程(在运行时),然后在关键函数上运行编译器,使用某种插件进行优化允许迭代抽象语法树,用常量替换参数,最后动态链接到共享对象。如果例程很短,可以使用许多工具在二进制文件中动态编译。

更实际的是,我非常依赖于对齐,gcc __builtin_assume_aligned,restrict,手动循环展开和编译器标志,以便在编译时给定编译器执行我想要的参数未知值。我想知道还有哪些其他选项至少接近便携式。我只使用内在函数作为最后的手段,因为它不便携并且工作量很大。 具体来说,如何使用语言语义,编译器扩展或外部工具为编译器(gcc)提供有关循环变量的其他信息,以便它可以更好地为我做优化。类似地,有任何方法可以将变量限定为具有步幅,以便加载和存储始终对齐,从而更容易启用自动矢量化和循环展开过程。

这些问题经常出现,所以我希望有更优雅的解决方法。以下是我手工优化的问题的例子,但我相信编译器应该能够为我做。这些不是进一步的问题。

有时你有一个滤波器,其长度不是最长SIMD寄存器长度的倍数,也可能存在内存对齐问题。在这种情况下,我要么(A)通过向量寄存器的倍数展开循环并调用结果/序言的未优化代码或(B)用零填充过滤器的开始或结束。我最近学会了gcc和其他编译器能够剥离循环。从我能够找到的有限文档中,我相信使用编译器指令,你对剥离的最好的颗粒控制是在整个函数(而不是单个循环)上。此外,您可以提供一些参数,但它主要只是展开量或生成的指令数量的上限或下限。

为了真正了解展开/剥离或零填充的最佳方法,编译器需要了解循环的长度和/或增量的大小。例如,知道循环可能具有大于一百或小于100的长度将是非常有帮助的。知道循环将总是运行32或34次将是有帮助的。事实上,由于编译器比我更了解计算机体系结构,如果它根据我提供的有关循环变量的信息做出所有展开决策,会好得多。我有一种情况,我希望编译器展开循环。我特意给它了#pragma GCC optimize ("unroll-loops")指令。但是,它需要工作的还有语句N &= ~7,从而通知编译器循环长度是8的倍数。这不是语言的语义特征,它没有改变的效果N的值。严格地告知静态分析器编译器循环已经是AVX寄存器长度的倍数。在这种情况下,我很幸运,它很有效,因为gcc非常聪明。但在其他情况下,我的暗示似乎不起作用(或者他们这样做,但是没有编译器反馈让我知道附加信息没有价值)。在一种情况下,我必须明确地告诉编译器不要展开循环,因为外部循环非常短并且开销不值得。如果优化器处于最大设置状态,通常只有这样才能了解正在进行的操作是查看汇编列表,进行一些更改,然后重试。

在另一种情况下,我小心地展开了一个循环,所以编译器会使用AVX寄存器。手动展开可能是必要的,因为编译器没有足够的关于循环长度的信息或者长度是特定倍数。不幸的是,内部循环正在访问每组长度为4的浮点数的未对齐数组(16字节对齐)。编译器仅使用传统的128位XMM寄存器。在使用AVX内在函数进行向量化的弱尝试之后,我发现未对齐访问的额外开销使得性能不比gcc正在做的更好。所以我想,我可以在缓存行的开头对齐每组浮点数,并使用等于缓存长度的步幅(或一半,即AVX寄存器的长度)来消除对齐问题。但是,由于额外的内存带宽,这可能会变得无效。这对我来说肯定更有意义。它使代码更难理解。并且,至少,正确的步伐将取决于我需要提供的编译时间常数。我想知道是否有一些更简单的方法可以依靠编译器完成所有工作呢?如果只是改变一行或两行代码,我愿意尝试。如果我必须手动完成它(在这种情况下无论如何),这是不值得的。 (在我写这篇文章时考虑过这个问题,我或许可以使用带有48个字节填充的联合或结构以及一些额外的代码行。我不得不考虑一下......)

0 个答案:

没有答案