本文收集了关于向量化的各种技巧:
在向量循环中处理用户定义的函数调用
如果您希望对具有用户定义的函数调用的循环进行向量化处理,(可能要重建代码)请将函数调用作为向量基本函数。
在基本函数中指定单位步长访问
如果您的基本函数访问单位步长中的内存,您可以通过两种方式来编写:
· 线性整数对统一指针进行索引
· 线性指针
__declspec(vector(uniform(a),linear(i:1)))
float foo(float *a, int i){
return a[i]++;
}
__declspec(vector(linear(a:1)))
float foo1(float *a){
return (*a)++;
}
处理向量循环内部的内存消歧
针对简单的循环考虑向量化:
void vectorize(float *a, float *b, float *c, float *d, int n) {
int i;
for (i=0; i<n; i++) {
a[i] = c[i] * d[i];
b[i] = a[i] + c[i] - d[i];
}
}
· 此处,编译器不知道这 4 个指针指向何处。作为一名编程人员,你可能知道它们指向完全独立的位置。编译器不会这么认为。除非编程人员明确地告诉编译器它们指向独立的位置,否则编译器必须假定它们彼此使用 VERY BADLY 别名 --- 例如,c[1] 和 a[0] 可能位于相同的地址,因此根本不能对循环进行向量化处理。
· 当未知指针的数量非常少时,编译器可以生成一个运行时检查,以及循环的优化和非优化版本(编译时间、代码大小以及运行时测试中存在开销)。由于开销会快速增长,因此"VERY SMALL"数值必须非常小 ---- 例如 2 --- 即便如此,由于没有告诉编译器“指针是独立的”,您仍会受到一定的影响。
· 因此,更好的方法是告诉编译器“指针是独立的”。其中一个方法是使用 C99 "restrict pointer"关键字。即便你没有使用"C99 标准"进行编译,您可以使用 -restrict (Linux) 和 -Qrestrict (Windows) 标记,以便使英特尔编译器接受"restrict"关键字。
void vectorize(float *restrict a, float *restrict b, float *c, float *d, int n) {
int i;
for (i=0; i<n; i++) {
a[i] = c[i] * d[i];
b[i] = a[i] + c[i] - d[i];
}
}
· 此处,编程人员应告诉编译器"a"和"b"没有其它别名。
· 另外一种方法是使用IVDEP 编译指示。IVDEP 的语义与限制指针不同,但是它可以使编译器消除某些假定的依赖性,这足以使编译器认为向量化是安全的。
void vectorize(float *a, float *b, float *c, float *d, int n) {
int i;
#pragma ivdep
for (i=0; i<n; i++) {
a[i] = c[i] * d[i];
b[i] = a[i] + c[i] - d[i];
}
}
避免 64 位整数与浮点之间的转换
· 避免 64 位整数与浮点之间的转换。
· 请使用 32 位整数。
· 如果可能,请使用 32 位的带符号整数(最高效)
避免 64 位整数对收集/分散进行索引
· 64 位索引收集/分散没有高效的硬件支持
带有间接访问的循环的向量化
考虑带有间接内存加载/存储的循环的向量化,如下所示:
for (i = kstart; i < kend; ++i) {
istart = iend;
iend = mp_dofStart[i+1];
float w = xd[i];
for (j = istart; j < iend; ++j) {
index = SCS[j];
xd[index] -= lower[j]*w;
}
}
对于上面所示的代码来说,向量化的关键条件是xd 值是不同的(否则便会存在真正的依赖性,从而不能对循环进行向量化处理。如果是这种情况,唯一的方法便是以向量友好型的方式重新编写算法)。如果 xd 值保证(由用户)是不同的,那么便可以使用 ivdep/simd 编译指示(在内部 j-loop 之前)执行向量化操作。编译器仍将生成收集/分散向量化 —如果存在可以使用其单位步长的算法列示,则会达到更好的效果。