编译器是否实际使用我的"omp declare simd"函数?

Does the compiler actually use my "omp declare simd" functions?

本文关键字:omp declare 函数 simd 我的 是否 编译器      更新时间:2023-10-16

看看我为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(…)。这是否调用这些函数之一?我必须做什么才能使用它们?

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

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

GCC 默认值是-O0- 针对调试进行反优化,因此代码当然完全是垃圾,实际上并没有自动矢量化(没有addpdmulpd指令)。

使用-O3启用优化。 GCC 在可以看到定义时将简单地内联调用。#pragma omp declare simd的事情允许编译器发出对函数的矢量化版本的调用,即使它看不到定义。 (或者对于它选择不内联的较大函数。


您可以在dot上使用__attribute__((noinline))来查看它如何工作,甚至对于您的小函数也是如此:

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

# 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

使用你的小#define SIMD 4main实际上根本不需要循环,只需两个 16 字节的向量就足够了。 具有编译时常量初始值设定项的数组得到优化;GCC 只是将常量具体化到寄存器中,0.0 为pxor-归零,并从静态常量数据加载 + 复制1.0.

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

IDK为什么GCC的OpenMP水平总和如此愚蠢。 显然,最好addpd xmm0,xmm0而不是存储两次,并且洗牌可以避免存储/重新加载。 使用addsd来做0.0 + x也是没有意义的;只需使用您存储的寄存器的低元素即可。


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


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

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

_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定义。