【问题标题】:pragma omp for simd does not generate vector instructions in GCC用于 simd 的 pragma omp 不会在 GCC 中生成向量指令
【发布时间】:2020-07-24 00:20:48
【问题描述】:

简短pragma omp for simd OpenMP 指令是否生成使用 SIMD 寄存器的代码?

更长: 正如OpenMP documentation 中所述,“worksharing-loop SIMD 构造指定一个或多个相关循环的迭代将分布在已经存在的线程中 [..] 使用 SIMD 指令”。从这个声明中,我希望以下代码 (simd.c) 在编译运行 gcc simd.c -o simd -fopenmp 时使用 XMMYMMZMM 寄存器,但事实并非如此。

#include <stdio.h>
#define N 100

int main() {
    int x[N];
    int y[N];
    int z[N];
    int i;
    int sum;

    for(i=0; i < N; i++) {
        x[i] = i;
        y[i] = i;
    }

    #pragma omp parallel
    {
        #pragma omp for simd
        for(i=0; i < N; i++) {
            z[i] = x[i] + y[i];
        }
        #pragma omp for simd reduction(+:sum)
        for(i=0; i < N; i++) {
            sum += x[i];
        }
    }
    printf("%d %d\n",z[N/2], sum);

    return 0;
}

在检查运行gcc simd.c -S -fopenmp 生成的汇编程序时,没有使用SIMD 寄存器。

我可以使用 -O3 选项在没有 OpenMP 的情况下使用 SIMD 寄存器,因为根据 GCC documentation 它包括-ftree-vectorize 标志。

  • XMM 寄存器:gcc simd.c -o simd -O3
  • YMM 寄存器:gcc simd.c -o simd -O3 -march=skylake-avx512
  • ZMM 寄存器:gcc simd.c -o simd -O3 -march=skylake-avx512 -mprefer-vector-width=512

但是,将标志 -march=skylake-avx512 -mprefer-vector-width=512-fopenmp 结合使用不会生成 SIMD 指令。

因此,我可以使用 -O3 轻松地对我的代码进行矢量化,而无需使用 pragma omp for simd,但反之则不行。

此时,我的目的不是生成 SIMD 指令,而是了解 OpenMP SIMD 指令如何在 GCC 中工作,以及如何仅使用 OpenMP(没有-O3)生成 SIMD 指令。

【问题讨论】:

  • 添加 simd 子句不会改变流行编译器的成本算法。在像您这样的简单缩减循环中,循环长度 100 几乎不足以从 simd (甚至 avx512)中受益,并且可能不足以从任一循环中的 omp parallel 中受益。用于 simd 的 omp 需要生成类似于嵌套循环的东西。除非编译器可以专门针对特定的循环计数,即 simd 长度乘以线程数的倍数,否则内部和外部循环都需要运行时余数和可能的对齐代码。
  • 在 gnu 编译器中使用 simd 子句的一般效果只是推翻对可能导致 simd 矢量化无效的可能别名的检测。
  • @tim18: 100 个元素足以让 128 位 SIMD 在现代 x86 上物有所值,尤其是在大小(向量宽度的倍数)和对齐方式(16 倍)已知良好的情况下。最后对向量进行水平求和的时间非常短。与某些微架构(例如某些 ARM)中向量单元只是松散耦合并且获得标量向量结果存在很大延迟不同,x86 对于movd eax, xmm0 只有几个周期延迟。

标签: c gcc openmp vectorization simd


【解决方案1】:

至少启用 -O2 以使 -fopenmp 正常工作并提高性能

gcc simd.c -S -fopenmp

GCC 的默认值为 -O0,针对一致的调试进行了反优化。它永远不会使用-O0 进行自动矢量化,因为当C 源代码中的每个i 值都必须存在于内存中时,它是没有意义的,依此类推。 Why does clang produce inefficient asm with -O0 (for this simple floating point sum)?

当您必须能够一次单步执行一个源代码行,甚至在运行时使用调试器修改 i 或内存内容,并让程序像您期望的 C 一样继续运行时,这也是不可能的抽象机器会。

没有任何优化的构建对于性能来说是完全垃圾;考虑到是否足够关心性能以使用 OpenMP 是很疯狂的。(当然,实际调试除外。)通常,从反优化到优化标量的加速比向量化该标量所能获得的要多代码,但两者都可能是很大的因素,因此您肯定需要自动矢量化之外的优化。


我可以使用-O3 选项在没有 OpenMP 的情况下使用 SIMD 寄存器,因为根据 GCC 文档,它包含 -ftree-vectorize 标志。

好吧,那就这样吧。 -O3 -march=native -flto 通常是在编译主机上运行的代码的最佳选择。此外,-fno-trapping-math -fno-math-errno 应该对一切都是安全的,并启用一些更好的 FP 函数内联,即使您不想要 -ffast-math。还优选地@ 987654336 ///////////////////////////////////////// @剖面引导优化(PGO),展开热环,并选择Branchy VS. Brantly等。

#pragma omp parallel-O3 -fopenmp 上仍然有效 - 默认情况下,GCC 不启用自动并行化。

另外,#pragma omp simd 有时会使用不同的矢量化样式。在您的情况下,似乎让 GCC 忘记了它知道数组是 16 字节对齐的,并使用 movdqu 加载(当 AVX 不适用于 paddd xmm0, [rax] 的未对齐内存源操作数时)。比较 https://godbolt.org/z/8q8Dqm - main 调用的 main._omp_fn.0: 辅助函数不假定对齐。 (尽管如果 GCC 不费心做向量大小的块,在除以线程数之后可能无法将数组拆分为范围?)


使用-O2 -fopenmp 得到你所期望的

OpenMP 将让 gcc 更容易或更有效地矢量化循环,其中您没有在指向函数的指针参数上使用 restrict 以让它知道数组不重叠,或者让浮点让它假装 FP 数学即使您没有使用 -ffast-math 也是关联的。

或者如果您启用了一些优化但没有完全优化(例如,-O2 不包括 -ftree-vectorize),那么#pragma omp 将按照您预期的方式工作。

请注意,x[i] = y[i] = i; init 循环不会在 -O2 处自动矢量化,但 #pragma 循环会。没有-fopenmp,纯标量。 Godbolt compiler explorer


对于这个小的N,串行-O3 代码将运行得更快,因为线程启动开销远不值得。但是对于大 N,如果单核不能使内存带宽饱和(例如在 Xeon 上,但大多数双核/四核台式机 CPU 几乎可以用一个核使内存带宽饱和),并行化可能会有所帮助。或者,如果您的阵列在不同内核的缓存中很热。

不幸的是(?)即使 GCC -O3 也无法通过您的整个代码进行持续传播,而只是打印结果。或者将z[i] = x[i]+y[i] 循环与sum(x[]) 循环融合。

【讨论】:

  • 谢谢。即使我同意你的观点,-O2 也不是 OpenMP API 规范的一部分,并且声明“pragma omp for simd”应该足以告诉编译器生成 SIMD 指令。一件事是 OpenMP 规范,另一件事是 -O{1,2,3} 执行的代码优化。有了你有用的答案,我看到 GCC 可能不遵循 OpenMP API,也许是因为他们也让你假设“这对性能来说完全是垃圾”。我知道在没有性能标志的情况下运行是没有意义的,但在这里我的意思不是试图跑得更快,而是要理解和学习。
  • @albertgumi:不同的编译器称其优化选项的名称不同,因此规范没有提及任何特定的编译器选项是有道理的。一些编译器默认启用优化(就像 ICC 过去那样)。假设您在关心生成的 asm 的任何情况下都启用优化。 #pragma omp simd 是对编译器的提示,它应该尝试对循环进行矢量化,但取决于源它可能无法做到。从某个 PoV 来看,-O0 / 调试模式即使是最琐碎的循环也太复杂了,对于脑死亡的编译器来说太复杂了。
  • @albertgumi:我有点理解你最初的困惑,即默认情况下没有启用优化,但我认为你关于 GCC 应该在-O0 向量化的论点没有任何意义。对我来说,能够在调试模式下单步执行循环的标量逻辑似乎更好,即使您使用了#pragma omp simdparallel。 OpenMP 使编译器能够根据需要编写更好的代码;它不会强制他们。 GCC 不在 -O0 处向量化,因为该模式将每个 C 语句编译为单独的 asm 块,而不是因为它故意变慢(这是一个副作用)
  • 感谢您公开的回答。我认为你是对的。
猜你喜欢
  • 2016-10-04
  • 2016-05-03
  • 1970-01-01
  • 2021-04-09
  • 2010-12-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-07
相关资源
最近更新 更多