编译器真的使用我的 "omp declare simd" 函数吗?
Does the compiler actually use my "omp declare simd" functions?
看看 this example 我构建的 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 版本:让编译器从 auto-vectorizing.
的循环中执行此操作
您没有启用优化,因此 GCC 不会 auto-vectorize 您的循环。因此它只调用函数的标量版本。
GCC 默认为 -O0
- anti-optimize 用于调试,所以当然代码完全是垃圾,实际上不是 auto-vectorized(没有 addpd
或 mulpd
说明)。
使用 -O3
启用优化。当 GCC 可以看到定义时,它将简单地内联调用。 #pragma omp declare simd
东西让编译器发出对函数矢量化版本的调用,即使它 不能 看到定义。 (或者对于它选择不内联的更大的函数。)
您可以在 dot
上使用 __attribute__((noinline))
来查看它是如何工作的,即使对于您的小功能也是如此:
On Godbolt with GCC9.1 -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
有了你的小 #define SIMD 4
,main
实际上根本不需要循环,只需要两个 16 字节的向量就足够了。带有 compile-time-constant 初始化器的数组被优化掉了; GCC 只是将常量具体化为寄存器,其中 pxor
为 0.0 归零并加载 + 从静态常量数据复制 1.0
.
所以无论如何,只有一次调用 dot()
的 SIMD 版本,仅此而已。我认为 GCC 知道同一个调用会给出相同的结果,这就是为什么它调用一次但存储结果两次的原因。
IDK 为什么 GCC 的 OpenMP 水平总和如此愚蠢。显然 addpd xmm0,xmm0
而不是存储两次会更好,并且洗牌可以避免 store/reload。同样使用 addsd
来做 0.0 + x
是没有意义的;只需使用您存储的寄存器的低位元素。
dot()
的标量版本具有函数的常用 C++ 名称修饰。其他版本有特殊的 name-mangling 约定,可能特定于 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 定义。
看看 this example 我构建的 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 版本:让编译器从 auto-vectorizing.
的循环中执行此操作您没有启用优化,因此 GCC 不会 auto-vectorize 您的循环。因此它只调用函数的标量版本。
GCC 默认为 -O0
- anti-optimize 用于调试,所以当然代码完全是垃圾,实际上不是 auto-vectorized(没有 addpd
或 mulpd
说明)。
使用 -O3
启用优化。当 GCC 可以看到定义时,它将简单地内联调用。 #pragma omp declare simd
东西让编译器发出对函数矢量化版本的调用,即使它 不能 看到定义。 (或者对于它选择不内联的更大的函数。)
您可以在 dot
上使用 __attribute__((noinline))
来查看它是如何工作的,即使对于您的小功能也是如此:
On Godbolt with GCC9.1 -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
有了你的小 #define SIMD 4
,main
实际上根本不需要循环,只需要两个 16 字节的向量就足够了。带有 compile-time-constant 初始化器的数组被优化掉了; GCC 只是将常量具体化为寄存器,其中 pxor
为 0.0 归零并加载 + 从静态常量数据复制 1.0
.
所以无论如何,只有一次调用 dot()
的 SIMD 版本,仅此而已。我认为 GCC 知道同一个调用会给出相同的结果,这就是为什么它调用一次但存储结果两次的原因。
IDK 为什么 GCC 的 OpenMP 水平总和如此愚蠢。显然 addpd xmm0,xmm0
而不是存储两次会更好,并且洗牌可以避免 store/reload。同样使用 addsd
来做 0.0 + x
是没有意义的;只需使用您存储的寄存器的低位元素。
dot()
的标量版本具有函数的常用 C++ 名称修饰。其他版本有特殊的 name-mangling 约定,可能特定于 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 定义。