首页 > 解决方案 > 编译器是否真的使用我的“omp declare simd”函数?

问题描述

看看我为 4D 点积构建的这个例子:

#pragma omp declare simd
double dot(double x0, double y0, double z0, double w0, double x1, double y1, double z1, double w1)
{
    return x0 * x1 + y0 * y1 + z0 * z1 + w0 * w1;
}

#define SIMD 4

int main(int argc, char **argv)
{
    double x[SIMD];
    double y[SIMD];
    double z[SIMD];
    double w[SIMD];

    double r[SIMD];

    for (int i = 0; i < SIMD; i++)
    {
        x[i] = y[i] = z[i] = 1;
        w[i] = 0;
    }

#pragma omp simd
    for (int i = 0; i < SIMD; i++)
    {
        r[i] = dot(x[i], y[i], z[i], w[i], x[i], y[i], z[i], w[i]);
    }

    double s = 0;
    for (int i = 0; i < SIMD; i++)
    {
        s += r[i];
    }
    return s;
}

在编译器输出中,您可以看到它生成了一些名为_XXXXXXvvvvvvvv_dot. 我假设这些是用于函数输入的不同长度的dot函数,或者至少它们应该是这样的。但是,这些函数似乎并没有被编译器实际使用。输出的第 94 行显示call dot(…). 这会调用这些功能之一吗?我必须做什么才能使用它们?

标签: c++openmpsimd

解决方案


不要尝试手动调用 SIMD 版本:让编译器从它自动矢量化的循环中执行此操作。

您没有启用优化,因此 GCC 不会自动矢量化您的循环。因此它只调用函数的标量版本。

GCC 默认是-O0- 反优化调试,所以当然代码完全是垃圾,实际上并不是自动矢量化的(没有addpdmulpd指令)。

启用优化-O3。当 GCC 可以看到定义时,它将简单地内联调用。这个#pragma omp declare simd东西让编译器即使看不到定义,也可以调用函数的向量化版本。(或者对于它选择不内联的较大函数。)


您可以使用__attribute__((noinline))ondot来查看它是如何工作的,即使对于您的小功能:

在带有 GCC9.1 的 Godbolt 上-O3 -fopenmp,进行了以下更改:

# gcc9.1 -O3 -fopenmp
main:
        sub     rsp, 40
        movapd  xmm0, XMMWORD PTR .LC0[rip]     # {1, 1}
        pxor    xmm7, xmm7                      # {0, 0}
        movapd  xmm3, xmm7
        movapd  xmm6, xmm0                      # duplicate the 1,1 vector for several args
        movapd  xmm5, xmm0
        movapd  xmm4, xmm0
        movapd  xmm2, xmm0
        movapd  xmm1, xmm0
        call    _ZGVbN2vvvvvvvv_dot(double, double, double, double, double, double, double, double)
        movaps  XMMWORD PTR [rsp], xmm0        # store to the stack
        movaps  XMMWORD PTR [rsp+16], xmm0     # twice
        pxor    xmm0, xmm0                     # 0.0
        addsd   xmm0, QWORD PTR [rsp]          # 0 + v[0]
        addsd   xmm0, QWORD PTR [rsp+8]        # ... += v[1]
        addsd   xmm0, QWORD PTR [rsp+16]
        addsd   xmm0, QWORD PTR [rsp+24]       # stupid inefficient horizontal sum
        add     rsp, 40
        cvttsd2si       eax, xmm0              # truncate to integer as main's return value
        ret

使用您的 tiny #define SIMD 4main实际上根本不需要循环,只需两个 16 字节向量就足够了。带有编译时常量初始化器的数组被优化掉了;GCC 只是将常量具体化到寄存器中,pxor对于 0.0 使用 -zeroing 并从静态常量数据中加载 + 复制对于1.0.

所以无论如何,只有一个调用 SIMD 版本的dot(),但就是这样。我认为 GCC 知道相同的调用会给出相同的结果,这就是为什么它调用一次但将结果存储两次的原因。

IDK 为什么 GCC 的 OpenMP 水平和如此愚蠢。显然,最好addpd xmm0,xmm0不要将其存储两次,并且随机播放可以避免存储/重新加载。同样使用addsdto do0.0 + x是没有意义的;只需使用您从中存储的寄存器的低元素。


的标量版本dot()具有通常的 C++ 名称修饰函数。其他版本具有特殊的名称修改约定,可能特定于 GCC 的 OpenMP、IDK。


有趣的是,gcc 制作了几个不同的版本dot,包括使用 YMM 寄存器的 AVX 版本。还有一些溢出到堆栈并在循环中使用标量数学;IDK 为什么这些存在。

所以我想这意味着即使你编译这个源文件没有,以这种方式-march=skylake-avx512编译的另一个循环仍然可以发出调用并获取 AVX512 定义:_ZGVeN8vvvvvvvv_dot

_ZGVeN8vvvvvvvv_dot(double, double, double, double, double, double, double, double):
        vmulpd  zmm1, zmm1, zmm5
        vfmadd132pd     zmm0, zmm1, zmm4
        vfmadd231pd     zmm0, zmm2, zmm6
        vfmadd231pd     zmm0, zmm3, zmm7

奇怪的是,我没有看到在 YMM regs 上使用 FMA 的 AVX+FMA 定义,只有使用 vmulpd / vaddpd 的 SSE2 和 AVX 定义。


推荐阅读