• 沒有找到結果。

9.2.5.1  什么是向量化 

Intel 的编译器支持向量化(Vectorization) ,向量化是使用向量处理单元进行批量计算的方 法, 可以把循环计算部分使用 MMX、 SSE、 SSE2、 SSE3、 SSSE3、 AVX、 Knights Corner Instructions  等扩展指令集进行向量化,从而大大提高计算速度。

9

Chapter 

MIC 处理器支持 512 位宽的 Knights Corner 指令,支持 16*32bit 或 8*64bit 处理模式,即 向量化宽度为 8 或 16。512 位相当于 16 个单精度浮点型数据的长度,单精度浮点数据向量化 的操作过程如图 9­9 所示,例如向量加操作 C[0~15]=A[0~15]+B[0~15](A、B、C 均为 float  型数据),没有使用向量化时这个操作需要 16 次加运算,而向量化之后,把 A、B、C 放到向 量寄存器中,进行一次向量加操作即可完成原来的 16 次加操作,因此,向量化可以大大提高 计算速度。

图 9­9  单精度浮点数据向量化示意图 

9.2.5.2  MIC 向量化优化策略 

MIC 向量化优化主要有两种方式:自动向量化和 SIMD 指令优化,向量化优化步骤一般为:

(1)插入引语自动向量化:不改变原程序结构,只需要插入预编译指令(引语)即可自 动向量化

(2)调整程序循环结构并插入引语自动向量化:对原程序做一些结构调整,如嵌套循环 交换次序等,然后插入引语可以自动向量化。

(3)编写 SIMD 指令:SIMD 指令可以比自动向量化获得更好的性能,但针对不同的硬 件平台编写的 SIMD 指令也不同,并且 SIMD 指令易读性较差,所以 SIMD 指令可以选择性 的使用。 

9.2.5.3  自动向量化

自动向量化是英特尔编译器提供的一个可以自动地使用  SIMD  指令的功能。在处理数据 时,编译器自动选择 MMX、Intel Streaming SIMD 扩展(Intel SSE、SSE2、SSE3、SSE4、AVX  和 Knights Corner Instructions)等指令集,对数据进行并行的处理。使用编译器提供的自动向 量化功能是提高程序性能的一个非常有效的手段。自动向量化在 IA­32 和 x86­64 的平台上均 提供很好的支持。自动向量化能最大程度地解放程序员,既为程序员屏蔽底层 CPU/MIC 的细 节,又能通过底层 CPU/MIC SIMD 并行获得有效的性能提升,MIC 处理器拥有 512 位宽的向

9 Chapter

量化处理器,因此自动向量化对 MIC 程序优化意义更为重大。 

1.自动向量化的好处

(1)提高性能:向量化处理,实现了单指令周期同时处理多批数据

(2)编写单一版本的代码,减少使用汇编使编码工作简化:较少的汇编意味着会大大减 少为特定的系统编程的工作, 程序将很容易升级并使用于最新的主流系统而不必重新编写那些 汇编代码

。 

2.什么样的循环可以向量化

(1)对于一个循环,如果编译器认为循环内的每一个语句都没有依赖于另一个语句并且 没有循环的依赖关系, 那么这个循环就是可向量化的。 换句话说, 每一个语句必须能独立执行,

读写数据的操作必须中立于循环的每次迭代

例如: 

for (int i=0; i<1000; i++)  { 

a[i] = b[i] * T + d[i] ;  b[i] = (a[i] + b[i])/2; 

c = c + b[i]; 

等价于下面的操作: 

for (int i=0; i<1000; i++) a[i] = b[i] * T + d[i] ;  for (int i=0; i<1000; i++) b[i] = (a[i] + b[i])/2; 

for (int i=0; i<1000; i++) c = c + b[i]; 

因此,这个循环是可以被向量化的。

再看一个例子: 

for (int i=1; i<1000; i++)  { 

a[i] = a[i­1] * b[i]; 

无论如何,这个循环是不能被向量化的,因为 a[i]在每次迭代中都读取了前一次迭代中的  a[i­1]。我们称这是一个交叉迭代的数据依赖或者“flow dependence” ,这样的循环不能被编译 器向量化。

(2)向量化只能作用在最内层的循环:在一个嵌套的循环中,向量器只能尝试向量化最 内层的循环, 查看向量器的输出信息可以知道循环是否能被向量化以及原因, 如果影响性能的 关键循环没有向量化,你可能需要做一些更深层的打算,比如调整嵌套循环的顺序。

(3)向量化处理的数据类型尽量一致:需要向量化处理的语句,其包含的变量数据类型 尽可能一致。如尽量避免在同一表达式中同时出现单精度和双精度变量。

9

Chapter 

3.编译器自动向量化方法

(1)编译器向量化选项:对于 MIC 程序,默认向量化编译选项为­vec,即默认情况下向 量化是打开的,若关闭向量化可以在编译选项中添加­no­vec。向量化编译器可以生成自己的 向量化报告,通过­vec­report 开关开启这一功能,具体选项功能如表 9­1 所示。

表 9­1  向量化报告 

­vec­report[n]  含义 

n=0  不显示诊断信息 

(2)#pragma ivdep 和 restrict 的使用

为了向量化一个包含或可能包含依赖关系的循环, 加上#pragma ivdep (ivdep, ignore vector  dependencies) 。

例如: 

1  void foo(int k)  2  { 

#pragma ivdep 

4  for(int j=0; j<1000; j++) 

5  { 

6  a[j] = b[j+k] * c[j]; 

7  b[j] = (a[j] + b[j])/2; 

8  b[j] = b[j] * c[j]; 

9  } 

10  } 

当向量化这个循环时,编译器会认为数组 b 依赖了交叉迭代,原因就是使用了变量 k,如 果我们知道 k 不会造成数据依赖,加上#pragma  ivdep 编译指导语句忽略数据的依赖关系并尝 试进行向量化。程序员必须知道这个依赖是怎么产生的,并确信它们没有数据依赖性。

使用“#pragma  vector  always”编译指导语句,指定循环向量化的方式可以避免一些没有 内存对齐的操作没有被向量化。甚至可以使用“#pragma simd”语句,同时在编译选项中加入 

­simd,强制向量化最内层循环,如果这么做的话,需要程序员保证程序的正确性。最后一条 语句与前面两条不同的是,前面两条对编译器来说只是建议,最终是不是被向量化,是由编译 器决定的,但“#pragma simd”指令对编译器来说是强制的,如果编译器坚持认为这段代码无 法向量化,将会产生一个编译器错误。

9 Chapter

在 MIC 平台上,可以采用“#pragma vector aligned”进行向量化对齐,但必须保证内存分 配以 64B 对齐,即以 align(64)声明变量。

使用指针的循环可能造成依赖性,如果为了向量化这样的循环,可以使用 restrict 关键字。 

void foo(float*restrict a, float*restrict b, float*restrict c)  2  { 

3  for(int j=0; j<1000; j++) 

4  { 

5  a[j] = b[j] * c[j]; 

6  b[j] = (a[j] + b[j])/2; 

7  b[j] = b[j] * c[j]; 

8  } 

9  } 

注意,使用了 restrict 关键字需要使用­restrict 编译选项。如果不使用 restrict 关键字,编译 器会认为数组的引用可能有交叉迭代的依赖性。 这是因为指针在循环中用来访问数据, 编译器

1  for(i=0; i<N; i++)  2  { 

3  rand();

9

1  for(i=0; i<N; i++)  2  { 

3  rand(); 

4  } 

for(i=0; i<N; i++) //自动向量化  6  { 

7  … 

8  … 

9  }  示例二: 

1  float s; 

2  for(i=0; i<N; i++)  3  { 

4  … 

5  s=…; 

6  for(j=0; j<M; j++)//自动向量化 

7  { 

8  if(s>0) 

9  { 

10  … 

11  } 

12  } 

13  } 

假设上面的代码中两层循环均无数据依赖,除了内层循环 for(j=0; j<M; j++)比较耗时,对 于 s 的求解也很耗时,然而求解 s 的部分是无法自动向量化的。我们可以通过拆分外层的循环 做到更好的自动向量化效果,修改后的伪代码如下: 

1  float s[16]; 

2  for(i=0; i<N; i+=16)  3  { 

4  T=min(N­1,16); 

for(k=0; k<T; k++) //自动向量化 

6  { 

7  …

9 Chapter 

8  s[k]=…; 

9  } 

10  for(k=0; k<T; k++) 

11  { 

12  for(j=0; j<M; j++) //自动向量化 

13  { 

14  if(s[k]>0) 

15  { 

1  for(i=0;i<100;i++)  2  { 

3  for(j=0;j<1024;j++) 

4  { 

5  … 

6  } 

7  } 

假设上面的代码两层  for  均无数据依赖,  MIC  并行的最佳线程数为  200,上面的代码内 层 for 可以采用自动向量化的方法,外层 for 作为 MIC 并行时仅有 100 次循环,并行度较低,

不利于发挥 MIC  的最佳性能,对于这种情况,我们可以采用拆分内层  for  的方式让并行程序 既可以满足并行度也可以满足自动向量化,一种拆分方法如下: 

1  for(i1=0;i1<200;i1++)  2  { 

3  for(j1=0; j1<512; j1++) 

4  { 

5  i = i1/2; 

6  j = (i1%2)*512+j1; 

7  … 

8  } 

9  }

9

Chapter

上面的方法可以满足 MIC 的并行度,同时满足自动向量化,上面的拆分也可以把外层 for  循环次数变得更大,但要保证内层 for 循环次数>16 次以保证向量化的效果。

向量化性能提升明显,但在某些应用的情景下,会出现精度的损失。因为向量化使用的是 向量单元,比如以前一个浮点数计算的时候,处理器给它分配了 1 个校验位,现在 16 个浮点 数同时操作,但校验位仍然只有 1 个(硬件限制),导致精度会出现误差。当然,在绝大多数 情况下,这点误差都是在可以接受的范围内的。 

9.2.5.4  SIMD 指令优化 

SIMD(Single Instruction Multiple Data)指令可以在程序执行中复制多个操作数,并把它 们直接打包在向量寄存器中。 显而易见, SIMD 指令在性能上有较大的优势, 可以以同步方式,

在同一时间内对多个数据执行同一条指令。

向量化的层次如图 9­10 所示,越往上的级别,使用的语言越低级,编程越复杂,但可以 控制的部分也越多,理论上性能也越高。相反的,越往下的级别,编程越容易,但性能可能未 必如此理想。

图 9­10  向量化层次

第一代 Intel MIC 产品为 KNC(Knights Corner) ,Knights Corner Instructions 是 KNC 支持 的  SIMD  指令的总称。可以看作是类似于  SSE、AVX  等的指令集。通过使用  Knights  Corner  指令,可以细粒度地控制向量化运算。

9 Chapter 

Knights Corner Instructions 分类:

(1)Knights Corner 指令(Knights Corner Instruction)是指具体的 SIMD 指令,是汇编指 令集中关于 SIMD 操作的子集。

(2) 内建 Knights Corner (Intrinsics of Knights Corner) 是对 Knights Corner 指令的封装 (几 乎涉及到所有指令),可以认为这些函数和数据类型是 C/C++的内建类型。

(3)Knights Corner 类库(Knights Corner Class Libraries)是为了方便使用 Knights Corner  指令而做的封装,可以让程序员尽量简单地使用  SIMD  指令,介于引语方式和  SIMD  代码之 间。其支持整型和浮点型数据。

下面通过单精度浮点向量加的例子说明三者的区别,见表 9­2。

表 9­2  Knights Corner 实现向量加 

Knights Corner 指令 内建 Knights Corner  Knights Corner 类库 

__m512 a,b,c;  看成变量),进行向量化改造。而直接使用内建 Knights  Corner 则更接近常规的思维方式,将 两个数组通过向量化函数进行运算,当然,其代码要比使用类库方式复杂一些,但由于减少了 封装和调用,因此性能也会略有提高。而内联汇编则是最难阅读的,由于最贴近底层,因而执 行效率也最高,只是编程的成本也是最高的。在实际的  SIMD  指令编写中,我们一般采用内 建 Knights Corner 的方式。

下面我们通过一个向量加的示例说明 SIMD 指令的使用方法。 

1  #include <immintrin.h> 

2  void foo(float *A, float *B, float *C, int N)  3  { 

#ifdef __MIC__ 

5  __M512 _A,_B,_C; 

6  for(int i=0; i<N; i+=16) 

7  { 

8  _A =_mm512_loadunpacklo_ps (_A, (void*)(&A[i]) ); 

9  _A = _mm512_loadunpackhi_ps (_A, (void*)(&A[i +16]) ); 

10  _B = _mm512_loadunpacklo_ps (_B, (void*)(&B[i]) ); 

11  _B = _mm512_loadunpackhi_ps (_B, (void*)(&B[i +16]) ); 

12  _C = _mm512_add_ps(_A,_B); 

13  _mm512_packstorelo_ps ((void*)(&C[i]) , _C );

相關文件