std::min和三目运算符在使用#pragma GCC optimize ("O3")进行自动向量化时的区别

10
Great, I'm happy to assist you with text translations. What language do you need me to translate from and to?
#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无法理解它只是一个最小值操作。这是标准引起的吗?还是实现的问题?或者有一些编译标志可以解决这个问题?


3
似乎添加优化级别为-O1或更高级别可以实现向量化。我怀疑这是函数内联的问题。 - Drew Dormann
是的,除非使用-O选项,否则这些优化选项都没有任何作用。特别是没有-O,编译器不会内联std::min,所以它可能修改全局变量a,b - Nate Eldredge
在编译器选项中加入我。 - Marek R
6
嗯,这是个好问题。快速阅读起来,#pragma GCC optimize ("O3") 相当于在每个函数上指定 __attribute__((optimize("O3")))。但我猜测这样做可能不允许像内联那样进行过程间优化,因此它可能不等同于在命令行上使用 -O3。总之,这表明实际上它们是等效的。 - Nate Eldredge
@NateEldredge 哇,真是尴尬,所以那个 pragma 不同于普通的 -O3。你知道的越多,我猜。如果你把它作为答案,我会接受的。感谢你的帮助。 - Maltysen
显示剩余4条评论
1个回答

9
总结:不要使用#pragma GCC optimize。相反,在命令行上使用-O3,你将得到你期望的行为。
GCC关于#pragma GCC optimize文档说:
每个在此之后定义的函数都被视为已声明了一个optimize(string)属性,其中包含每个字符串参数。 而且optimize属性有文档记录
优化属性用于指定一个函数要使用与命令行上指定的不同的优化选项进行编译。……优化属性仅适用于调试目的。它不适用于生产代码。 [强调添加,感谢Peter Cordes发现最后一部分。]
所以,不要使用它。
特别的,指定#pragma GCC optimize ("O3")在文件开头并不等同于在命令行使用-O3。事实证明前者没有导致std::min内联,因此编译器会认为它可能会修改全局内存,比如a,b数组。这自然会抑制矢量化。
仔细阅读__attribute__((optimize))文档让每个函数main()std::min()都像用-O3编译。但是这与同时使用-O3编译两者不同,只有后者才能使用类似内联的过程优化。

这里有一个非常简单的Godbolt示例。使用#pragma GCC optimize ("O3"),函数foo()please_inline_me()都被优化了,但please_inline_me()没有被内联。但是,在命令行上使用-O3,它会被内联。

猜测optimize属性,以及扩展#pragma GCC optimize,使编译器将该函数视为其定义在单独的源文件中,并使用指定选项进行编译。如果std::min()main()在单独的源文件中定义,您可以使用-O3编译每个文件,但不会进行内联。

可以说GCC手册应更明确地记录此内容,但我猜如果它仅用于调试,那么可能可以假设它是针对熟悉区别的专家而设计的。

如果你确实在命令行上使用-O3编译你的示例,你将获得相同(矢量化)的汇编代码,或者至少我是这样做的。(在修复反向比较之后:你的三元代码计算的是最大值而不是最小值。)

可能 -O0(显式或默认)是“特殊的”。即使您手动启用了所有 -O2 包含的优化选项(使用 -fverbose-asm 在汇编注释中报告),您仍然具有一些调试模式行为,我记得,例如不内联甚至在语句之间仍然将变量同步到内存中。也许通过 pragma 设置 O3 不能完全将其排除在调试模式之外。 - Peter Cordes
5
GCC的手册也说优化属性应仅用于调试目的。它不适用于生产代码。我认为这也适用于#pragma指令。 - Peter Cordes
@PeterCordes:手册中发现了好的地方。我已经将其添加到答案中。 - Nate Eldredge

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