std::min 对比使用#pragma GCC 优化的三元 gcc 自动矢量化 ("O3")

std::min vs ternary gcc auto vectorization with #pragma GCC optimize ("O3")

我知道“为什么我的编译器会这样做”不是最好的问题类型,但这个问题对我来说真的很奇怪,我很困惑。

我以为std::min()和手写的三元一样(可能有一些编译时模板的东西),正常使用时似乎编译成相同的操作。但是,当尝试进行“最小和总和”循环自动矢量化时,它们似乎并不相同,如果有人能帮我弄清楚原因,我会很高兴。这是产生问题的一个小示例代码:

#pragma GCC target ("avx2")
#pragma GCC optimize ("O3")

#include <cstdio>
#include <cstdlib>
#include <algorithm>

#define N (1<<20)
char a[N], b[N];

int main() {
    for (int i=0; i<N; ++i) {
        a[i] = rand()%100;
        b[i] = rand()%100;
    }

    int ans = 0;
    #pragma GCC ivdep
    for (int i=0; i<N; ++i) {
        //ans += std::min(a[i], b[i]);
        ans += a[i]>b[i] ? a[i] : b[i];
    }
    printf("%d\n", ans);
}

我在 gcc 9.3.0 上用编译命令 g++ -o test test.cpp -ftree-vectorize -fopt-info-vec-missed -fopt-info-vec-optimized -funsafe-math-optimizations.

编译了这个

并且上面的代码在编译期间调试为:

test.cpp:19:17: optimized: loop vectorized using 32 byte vectors

相比之下,如果我注释三元组并取消注释 std::min,我会得到:

test.cpp:19:17: missed: couldn't vectorize loop
test.cpp:20:35: missed: statement clobbers memory: _9 = std::min<char> (_8, _7);

所以 std::min() 似乎在做一些不寻常的事情,阻止 gcc 理解它只是一个最小操作。这是由标准引起的吗?还是实施失败?或者是否有一些编译标志可以使这项工作?

总结:不要使用#pragma GCC optimize。在命令行上使用 -O3,您将获得预期的行为。

GCC 在 #pragma GCC optimize 上的 documentation 说:

Each function that is defined after this point is treated as if it had been declared with one optimize(string) attribute for each string argument.

And the optimize attribute is documented as:

The optimize attribute is used to specify that a function is to be compiled with different optimization options than specified on the command line. [...] The optimize attribute should be used for debugging purposes only. It is not suitable in production code. [Emphasis added, thanks Peter Cordes for spotting the last part.]

所以,不要使用它。

特别是,在文件顶部指定 #pragma GCC optimize ("O3") 似乎并不等同于在命令行中使用 -O3。事实证明,前者不会导致 std::min 被内联,因此编译器实际上确实假设它可能会修改全局内存,例如您的 a,b 数组。这自然会抑制矢量化。

仔细阅读 __attribute__((optimize)) 的文档,看起来 each 函数 main()std::min() 将被编译为如果有 -O3。但这与将它们两个与 -O3 一起编译是不一样的,因为只有在后一种情况下,像内联这样的过程间优化才可用。

Here is a very simple example on godbolt。使用 #pragma GCC optimize ("O3"),函数 foo()please_inline_me() 都得到了优化,但 please_inline_me() 没有得到内联。但是在命令行上使用 -O3,它确实如此。

猜测是 optimize 属性以及扩展名 #pragma GCC optimize 导致编译器将函数视为函数,就好像它的定义在单独的源文件中一样,该源文件正在使用指定的选项。事实上,如果 std::min()main() 是在单独的源文件中定义的,您可以使用 -O3 编译每个源文件,但不会进行内联。

可以说 GCC 手册应该更明确地记录这一点,但我想如果它只是为了调试,那么假设它是为熟悉区别的专家准备的可能是公平的。

如果您真的在命令行上使用 -O3 编译您的示例,您将获得两个版本的相同(矢量化)程序集,或者至少我得到了。 (修复向后比较后:您的三元代码正在计算最大值而不是最小值。)