鲲鹏gcc -fsimdmath 矢量化数学库选项详解
本文将详细分析矢量化数学库的使用,以及鲲鹏gcc在这方面做得一些工作。
背景
矢量化的概念自提出以来被广泛应用在各种计算场景中,其应用的边界也在不断拓展。在很多hpc应用中都存在一种场景,就是在循环中调用数学函数,这种情况下一般矢量化进程会被数学函数调用打断,从而只能生成标量代码,不能生成矢量化代码。
我们举个例子,假设有如下简单代码 test.c
void test (float a[], float b[], int len)
{
for (int i = 0; i <len; i++) {
a[i] = a[i] + b[i];
}
}
使用gcc 9.3.0 gcc -O3 test.c -S -o test.s
编译之后可以得到一个矢量化版本的汇编代码。打开生成的test.s汇编文件可以看到生成了这段汇编
.L4:
ldr q0, [x0, x3]
ldr q1, [x1, x3]
fadd v0.4s, v0.4s, v1.4s
str q0, [x0, x3]
add x3, x3, 16
cmp x3, x4
bne .L4
可见已经生成了fadd v0.4s, v0.4s, v1.4s
这样的矢量化指令,每个迭代可以计算四个浮点数据。
那么我们看一下如果在循环中加入一个数学函数的话会怎样
test-expf.c:
#include <math.h>
void test (float a[], float b[], int len)
{
for (int i = 0; i <len; i++) {
a[i] = a[i] + expf(b[i]);
}
}
使用 gcc test-expf.c -O3 -S -o test-expf.s
编译之后汇编如下
.L3:
ldr s8, [x20, x19]
ldr s0, [x22, x19]
bl expf
fadd s8, s8, s0
str s8, [x20, x19]
add x19, x19, 4
cmp x21, x19
bne .L3
我们可以发现现在循环已经无法矢量化了,一个迭代只能进行一组浮点值的计算。
如果使用gcc的优化dump选项-fdump-tree-vect-details
查看矢量化的具体原因可以在生成的test-expf.c.160t.vect文件中看到矢量化失败的原因是
test-expf.c:5:23: missed: statement clobbers memory: _7 = expf (_6);
test-expf.c:4:5: missed: not vectorized: loop contains function calls or data references that cannot be analyzed
test-expf.c:4:5: note: ***** Analysis failed with vector mode VOID
test-expf.c:4:5: missed: couldn't vectorize loop
也就是expf的加入导致了矢量化分析的失败,编译器认为expf这个函数调用是无法矢量化的。
好,到目前,我们提出了问题,就是当循环里面有数学库函数调用时,无法矢量化,从而会导致性能的下降。
开源解决方案
那么有办法解决上面提到的问题吗?当然有。
目前业界常用的思路是对数学函数进行矢量化版本声明,这个声明本质上是告诉编译器,这个函数可以输入输出矢量化结构的数据,比如一次输入输出4组浮点数。这种声明的方法可以参考openmp的相关介绍。编译器获取这种声明之后可以根据声明生成GNU ABI 约定的接口,同时根据声明的矢量化版本宽度自动寻求矢量化机会。
这里回到我们的例子,为了使能矢量化,我们需要对expf
这个函数添加矢量化声明
#include <math.h>
#pragma omp declare simd simdlen(4) notinbranch
float expf (float x);
void test (float a[], float b[], int len)
{
for (int i = 0; i <len; i++) {
a[i] = a[i] + expf(b[i]);
}
}
添加之后我们再次构建并生成dump文件,构建指令需要添加-fopenmp-simd -fno-math-errno
这两个选项。其中-fopenmp-simd
选项是告诉编译器,需要处理openmp simd相关的pragma。 -fno-math-errno
是告诉编译器不需要考虑数学函数产生exception的error number。 这里多讲一下,矢量化函数一般是无法产生error number的,所以编译器默认其无法产生error number, 如果是默认的-fmath-errno
模式,则其不会生成矢量化版本函数。
gcc test-expf.c -O3 -S -o test-expf.s -fdump-tree-vect-details -fopenmp-simd -fno-math-errno
构建完成可以看到矢量化的汇编循环已经生成了。
.L4:
ldr q16, [x20, x19]
ldr q0, [x21, x19]
bl _ZGVnN4v_expf
fadd v16.4s, v16.4s, v0.4s
str q16, [x20, x19]
add x19, x19, 16
cmp x19, x23
bne .L4
dump文件test-expf.c.160t.vect 中可以看到对expf这块代码的分析是找到其simdclone,所以分析继续了
test-expf.c:8:5: note: ==> examining statement: _7 = expf (_6);
test-expf.c:8:5: note: vect_is_simple_use: operand *_5, type of def: internal
test-expf.c:8:5: note: vect_is_simple_use: vectype vector(4) float
test-expf.c:8:5: missed: function is not vectorizable.
test-expf.c:8:5: note: vect_is_simple_use: operand *_5, type of def: internal
test-expf.c:8:5: note: vect_is_simple_use: vectype vector(4) float
test-expf.c:8:5: note: === vectorizable_simd_clone_call ===
编译阶段的问题就是这样解决了。有些同学可能注意到一个问题,就是编译接口是生成了,那这个矢量化的expf函数的实现在哪里呢?
这个问题的答案是最好使用业界已经成熟的矢量化函数,当然有兴趣也可以自己开发。目前性能最好的arm矢量化数学库是arm公司自己开发开源的mathlib,但是这个库的函数支持并不全面。另一个选择是sleef, 这个库的函数性能较差,但是支持很全面。
鲲鹏gcc -fsimdmath 选项
鲲鹏gcc在矢量化数学库的支持上面做了封装的工作。我们可以看到在开源的支持方式上面有三个问题。
- 接口声明很难写,需要非常熟练地掌握openmp相关知识和gnu abi相关知识。
- 选项添加很繁杂,不仅要添加使能pragma的选项,还要添加其他相关选项。
- 不同语言间的支持程度不同,使能需要的工作更加复杂。
鲲鹏gcc将以上这些工作都封装起来,使用一个-fsimdmath 选项去使能所有mathlib支持的矢量化数学库使能接口。同样是刚才的用例,我们可以使用
gcc test-expf.c -O3 -S -o test-expf.s -fsimdmath
编译指令生成矢量化数学库接口。此选项封装了所有使能所需的指令及相关openmp声明,并制定编译器去自动寻找相关声明。另外鲲鹏gcc内置了mathlib数学库,所以链接时指定-lmathlib
即可使用矢量化数学库。
不仅如此,鲲鹏gcc这个功能还尽量支持了常用语言,包括c\c++\fortran。当前支持矢量化的数学函数有
- cos
- cosf
- sin
- sinf
- exp
- expf
- log
- logf
- pow
- powf
- exp2f
矢量化数学库使能选项-fsimdmath 就介绍到这里,各位有问题敬请留言。
- 点赞
- 收藏
- 关注作者
评论(0)