在GCC向量化器中存在一个缺陷,最近的GCC版本似乎已经解决了这个问题。在我的测试案例中,GCC 4.7.2成功地对以下简单循环进行了向量化:
#pragma omp parallel for schedule(static)
for (int i = 0; i < N; i++)
a[i] = b[i] + c[i] * d;
同时,GCC 4.6.1版本并不支持,且会报告循环包含函数调用或无法分析的数据引用。向量化器中的错误由GCC实现parallel for
循环的方式触发。当处理和扩展OpenMP结构时,简单的循环代码会被转换为类似以下的形式:
struct omp_fn_0_s
{
int N;
double *a;
double *b;
double *c;
double d;
};
void omp_fn_0(struct omp_fn_0_s *data)
{
int start, end;
int nthreads = omp_get_num_threads();
int threadid = omp_get_thread_num();
start = (data->N * threadid) / nthreads;
end = (data->N * (threadid+1)) / nthreads;
for (int i = start; i < end; i++)
data->a[i] = data->b[i] + data->c[i] * data->d;
}
...
struct omp_fn_0_s omp_data_o;
omp_data_o.N = N;
omp_data_o.a = a;
omp_data_o.b = b;
omp_data_o.c = c;
omp_data_o.d = d;
GOMP_parallel_start(omp_fn_0, &omp_data_o, 0);
omp_fn_0(&omp_data_o);
GOMP_parallel_end();
N = omp_data_o.N;
a = omp_data_o.a;
b = omp_data_o.b;
c = omp_data_o.c;
d = omp_data_o.d;
在GCC 4.7之前的向量化器无法对该循环进行向量化处理。这不是OpenMP特定的问题。即使没有任何OpenMP代码,也可以轻松地复现此问题。为了确认这一点,我编写了以下简单测试:
struct fun_s
{
double *restrict a;
double *restrict b;
double *restrict c;
double d;
int n;
};
void fun1(double *restrict a,
double *restrict b,
double *restrict c,
double d,
int n)
{
int i;
for (i = 0; i < n; i++)
a[i] = b[i] + c[i] * d;
}
void fun2(struct fun_s *par)
{
int i;
for (i = 0; i < par->n; i++)
par->a[i] = par->b[i] + par->c[i] * par->d;
}
有人会期望两个代码(注意 - 这里没有使用OpenMP!)都能很好地向量化,因为使用了“restrict”关键字来指定不会发生别名问题。不幸的是,在GCC < 4.7中,情况并非如此 - 它成功地将
fun1
中的循环向量化,但无法将
fun2
中的循环向量化,原因与编译OpenMP代码时相同。
其原因在于向量化器无法证明
par->d
不在
par->a
、
par->b
和
par->c
指向的内存中。这在
fun1
中并非总是成立,有两种情况:
-
d
作为值参数在寄存器中传递;
-
d
作为值参数在堆栈上传递。
在x64系统中,System V ABI规定前几个浮点参数在XMM寄存器(AVX启用的CPU上为YMM)中传递。这就是
d
在这种情况下传递的方式,因此没有指针可以指向它 - 循环被向量化了。在x86系统中,ABI规定参数传递到堆栈上,因此
d
可能会被三个指针中的任何一个别名。事实上,如果使用
-m32
选项生成32位x86代码,GCC将拒绝向量化
fun1
中的循环。
GCC 4.7通过插入运行时检查来解决这个问题,确保
d
和
par->d
都不会别名。
消除
d
可以消除无法证明的非别名问题,随后的OpenMP代码可以被GCC 4.6.1向量化。
#pragma omp parallel for schedule(static)
for (int i = 0; i < N; i++)
a[i] = b[i] + c[i];