【问题标题】:std::min vs ternary gcc auto vectorization with #pragma GCC optimize ("O3")使用 #pragma GCC 优化(“O3”)的 std::min 与三元 gcc 自动矢量化
【发布时间】:2021-08-10 21:26:04
【问题描述】:

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

我原以为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 理解它只是一个最小操作。这是由标准引起的吗?还是实施失败?或者是否有一些编译标志可以使这项工作?

【问题讨论】:

  • 似乎添加了-O1或更高achieves the vectorization的优化级别。我怀疑这是函数内联的问题。
  • @MarekR 这不是过早的优化,在我的计算机上它加速了 >2 倍。我认为您的编译器资源管理器链接没有矢量化标志,因为 asm 输出没有矢量化指令(例如vpaddd 等)
  • 是的,除非您使用-O,否则这些优化选项实际上都没有任何作用。特别是,没有-O,编译器将不会内联std::min,所以它知道它可能会修改全局变量a,b
  • 嗯,这是个好问题。快速阅读,#pragma GCC optimize ("O3") 相当于在每个函数上指定__attribute__((optimize("O3")))。但我可能猜测这不允许像内联这样的过程间优化,因此它可能不等同于在命令行上使用-O3。无论如何,这表明在实践中它们是等价的。

标签: c++ gcc optimization compiler-optimization auto-vectorization


【解决方案1】:

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

GCC 的 documentation #pragma GCC optimize 说:

在此之后定义的每个函数都被视为已为每个字符串参数声明了一个 optimize(string) 属性。

And the optimize attribute is documented as:

optimize 属性用于指定使用不同于命令行指定的优化选项来编译函数。 [...] optimize 属性只能用于调试目的。它不适用于生产代码。 [强调添加,感谢 Peter Cordes 发现最后一部分。]

所以,不要使用它。

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

仔细阅读__attribute__((optimize)) 的文档后,它看起来像每个 函数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 编译你的示例,你会得到两个版本的相同(矢量化)程序集,或者至少我做到了。 (修复向后比较后:您的三元代码正在计算最大值而不是最小值。)

【讨论】:

  • 可能-O0(显式或默认)是“特殊的”。即使您手动启用了-O2 包含的所有优化选项(在带有-fverbose-asm 的asm cmets 中报告),您仍然有一些调试模式行为,IIRC,例如不内联甚至仍然在语句之间将变量同步到内存。也许通过 pragma 设置 O3 并不能完全摆脱调试模式。
  • GCC 的手册also says 优化 属性 应该仅用于调试目的。它不适合生产代码。 - 我认为这也适用于 pragma。
  • @PeterCordes:手册中的好地方。我将此添加到答案中。
猜你喜欢
  • 2018-12-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-11-06
  • 2013-01-29
  • 1970-01-01
  • 2013-03-06
相关资源
最近更新 更多