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

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

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

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


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

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

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

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