内存布局转换

面向英特尔® MIC 架构的编译器方法

内存布局转换

概述

本章节介绍了一种非常实用的用户代码转换从结构数组 (AoS) 的数据组织方式转换为数组结构 (SoA) 的组织方式。该转换有助于编译器更加高效地访问处理器上的数据。

主题

借助一种有助于提高效率的用户代码转换用户无须使用内存散集 (gather-scatter) 操作。这种非常规的内存操作会增加延迟和带宽使用,并限制编译器矢量化的范围。将以结构数组 (AoS) 表达式编写的数据结构转换为以数组结构 (SoA) 表达式编写的数据结构,某些应用可从中获益。这有助于防止在跨数组元素访问结构的一个字段时发生的聚集现象,也能够帮助编译器对迭代数组的循环进行矢量化处理。请注意,考虑到这些数据结构的所有使用位置,此类转换必须在编程级别(由用户)进行。在循环层面执行会使循环前后格式之间的转换成本攀升。

AOS SOA 转换可在矢量化代码中帮助防止散集操作的常见优化方法是将结构数组 (AoS) 数据结构转换为数组结构 (SoA) 表达式。在结构实例上执行矢量化操作时,为每个结构字段保留单独的数组会保持内存访问配置。AOS 结构需要散集操作,这会影响 SIMD 效率,以及使内存访问占用更多的带宽并发生延迟。硬件散集机制也同样需要此转换 与连续负载相比,散集访问通常需要更多的带宽和延迟

SOA 的代价是降低原始结构实例多个字段访问之间的局部化进而增加 TLB转换旁视缓冲器压力。根据初始源代码中的数据访问模式的不同(无论其是否涉及对循环中结构的多个字段的访问,也无论是否涉及所有结构实例或者是一个子集),考虑结构数组到数组结构 (AOSOA) 的转换可能是更合适的。其目的是在外部层面(outer-level获得局部化(locality)的优势,并在 innermost-level(核心层面)获得 unit-stride。这里,内部数组的长度可以是矢量长度的几倍,以便能够充分利用 unit-stride 全矢量。每个结构实例都可以这种方式具有多个字段(小型数组中)。在外部层面,您可以具有一个结构数组(现在更大)。在这样的布局中,字段访问仍足够的近,可以获得原始代码中附近结构实例访问的页面局部化优势。

示例展示了如何使用源代码中的数组符号实现 AOS 数据结构到 SOA 的转换。

下面的代码片段来自 LIBOR* 性能指标评测的 OpenMP* 此版本使用数组符号将 AOS 格式原始代码中转换为 SOA 格式修改

原始代码(LIBORAOS 格式):

 

01

long tid = (long)(tid_);

02

long long int chunk = npath/nthreads;

 

03

long long int beg = tid*chunk;

04

long long int end = min(npath, (tid+1)*chunk);

 

05

long long int chunk_inner = PATH_BLOCK_SIZE/nthreads // JSP

06

printf("chunk = %d, chunk_inner = %dn", chunk, chunk_inner);

 

07

 

08

 #pragma omp parallel

 

09

 for(long long int path = beg; path < end; path += chunk_inner){ 

10

 

 

11

      #pragma omp for nowait

12

      for (long long int path_=0; path_<chunk_inner; path_+=1) {

 

13

             int    i, j, k;

14

             __declspec(align(64)) FPPREC B[nmat], S[nmat], L[n];

 

15

             FPPREC sqez, lam, con1, v_scal, vrat;

16

             FPPREC b, s, swapval;

 

17

             FPPREC *zlocal=z+nmat*(tid*chunk_inner+path_);

18

 

 

19

             for(i=0;i<n;i++) {

20

                     L[i] = L0[i];

 

21

             }

22

             for(j=0; j<nmat; j++){

 

23

                     sqez = SQRT(delta)*zlocal[j];

24

                     v_scal = ZERO;

 

25

 

26

                     for (i=j+1; i<n; i++) {

 

27

                             lam  = lambda[i-j-1];

28

                             con1 = delta*lam;

 

29

                             v_scal   += con1*L[i]/(ONE+delta*L[i]);

30

                             vrat = EXP(con1*v_scal + lam*(sqez-HALF*con1));

 

31

                             L[i] = L[i]*vrat;

32

                     }

 

33

             }

34

             b = ONE;

 

35

             s = ZERO;

36

             for (j=nmat; j<n; j++) {

 

37

                     b = b/(ONE+delta*L[j]);

38

                     s = s + delta*b;

 

39

                     B[j-nmat] = b;

40

                     S[j-nmat] = s;

 

41

             }

42

 

 

43

             v_scal = ZERO;

44

             for (i=0; i<nopt; i++){

 

45

                     k = maturities[i] -1;

46

                     swapval = B[k] + swaprates[i]*S[k] - ONE;

 

47

                     if (swapval < ZERO)

48

                             v_scal += MHUND*swapval;

 

49

             }

50

 

 

51

             // apply discount //

52

             for (j=0; j<nmat; j++){

 

53

                     v_scal = v_scal/(ONE+delta*L[j]);

54

             }

 

55

             v[tid*chunk_inner+path_]+=v_scal;

56

      }

 

57

}

修改代码(LIBOR*SOA 格式):

 

01

int tid = (int)(tid_); 

02

long long int chunk = (npath%nthreads)?(1 + npath/nthreads):(npath/nthreads); 

 

03

long long int beg = tid*chunk; 

04

long long int end = min(npath, (tid+1)*chunk); 

 

05

long long int chunk_inner = ((PATH_BLOCK_SIZE%(nthreads*SIMDVLEN))?(1 + PATH_BLOCK_SIZE/(nthreads*SIMDVLEN)):(PATH_BLOCK_SIZE/(nthreads*SIMDVLEN)))*SIMDVLEN; 

06

 

 

07

 #pragma omp parallel 

08

 for(long long int path = beg; path < end; path += chunk_inner){  

 

09

 

10

      #pragma omp for nowait 

 

11

      for (long long int path_=0; path_<chunk_inner; path_+=SIMDVLEN) { 

12

             int    i, j, k; 

 

13

              __declspec(align(64)) FPPREC B[nmat][SIMDVLEN], S[nmat][SIMDVLEN], L[n][SIMDVLEN]; 

14

             __declspec(align(64)) FPPREC sqez[SIMDVLEN], lam[SIMDVLEN], con1[SIMDVLEN], v_scal[SIMDVLEN], vrat[SIMDVLEN], vrat_tmp[SIMDVLEN]; 

 

15

             __declspec(align(64)) FPPREC b[SIMDVLEN], s[SIMDVLEN], swapval[SIMDVLEN]; 

16

              FPPREC *zlocal=z+nmat*(tid*chunk_inner + path_); 

 

17

             __declspec(align(64)) FPPREC old_output[SIMDVLEN]; 

18

                        

 

19

             old_output[:] = v[tid*chunk_inner+path_:SIMDVLEN];  

20

 

 

21

             for(i=0;i<n;i++) { 

22

                     L[i][:] = L0[i]; 

 

23

             } 

24

 

 

25

             for(j=0; j<nmat; j++) { 

26

                     sqez[:] = SQRT(delta)*zlocal[0:SIMDVLEN]; 

 

27

                     v_scal[:] = ZERO; 

28

                             zlocal += SIMDVLEN; 

 

29

                     for (i=j+1; i<n; i++) { 

30

                             lam[:]  = lambda[i-j-1]; 

 

31

                             con1[:] = delta*lam[:]; 

32

                             v_scal[:]   += con1[:]*L[i][:]/(ONE+delta*L[i][:]); 

 

33

                             vrat_tmp[:] = (con1[:]*v_scal[:] + lam[:]*(sqez[:]-HALF*con1[:])); 

34

                                            vrat[:] = EXP(vrat_tmp[:]); 

 

35

                             L[i][:] = L[i][:]*vrat[:]; 

36

                     } 

 

37

             } 

38

 

 

39

             b[:] = ONE; 

40

             s[:] = ZERO; 

 

41

 

42

             for (j=nmat; j<n; j++) { 

 

43

                     b[:] = b[:]/(ONE+delta*L[j][:]); 

44

                     s[:] = s[:] + delta*b[:]; 

 

45

                     B[j-nmat][:] = b[:]; 

46

                     S[j-nmat][:] = s[:]; 

 

47

             } 

48

 

 

49

             v_scal[:] = ZERO; 

50

 

 

51

             for (i=0; i<nopt; i++){ 

52

                     k = maturities[i] -1; 

 

53

                     swapval[:] = B[k][:] + swaprates[i]*S[k][:] - ONE; 

54

                     if (swapval[:] < ZERO) 

 

55

                             v_scal[:] += MHUND*swapval[:]; 

56

             } 

 

57

 

58

             // apply discount //         

 

59

             for (j=0; j<nmat; j++){ 

60

                     v_scal[:] = v_scal[:]/(ONE+delta*L[j][:]); 

 

61

             } 

62

             v[tid*chunk_inner+path_:SIMDVLEN] = old_output[:] + v_scal[:]; 

 

63

      } 

64

}

要点

尽管看上去以结构数组 (AoS) 来组织数据更为合理但是这种方式会使访问读内存变得极为困难。这会妨碍编译器执行的许多优化操作,最明显的是它会防止有效的执行矢量化。具有此特征的算法通常在英特尔® 至强架构和英特尔® MIC 架构上的性能较差。

如果可能最好使用数组结构 (SoA) 来重组数据。不幸的是,在现实应用中,这项工作可能要付出极大的精力。但是,此类转换所带来的优势是非常明显的。

下一步工作

要在英特尔® 至强 融核协处理器上成功调试您的应用请务必通读此指南并点击文中的超链接查看相关内容。本指南提供了实现最佳应用性能所要执行的步骤。

返回为英特尔® 集成众核架构(英特尔® MIC 架构)做好准备

Nähere Informationen zur Compiler-Optimierung finden Sie in unserem Optimierungshinweis.