内存布局转换

面向英特尔® 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 格式(修改)。

原始代码(LIBOR,AOS 格式):

long tid = (long)(tid_);
long long int chunk = npath/nthreads;

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

long long int chunk_inner = PATH_BLOCK_SIZE/nthreads // JSP
printf("chunk = %d, chunk_inner = %dn", chunk, chunk_inner);

 #pragma omp parallel

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

      #pragma omp for nowait
      for (long long int path_=0; path_<chunk_inner; path_+=1) {

             int    i, j, k;
             __declspec(align(64)) FPPREC B[nmat], S[nmat], L[n];

             FPPREC sqez, lam, con1, v_scal, vrat;
             FPPREC b, s, swapval;

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

             for(i=0;i<n;i++) {
                     L[i] = L0[i];

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

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

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

                             lam  = lambda[i-j-1];
                             con1 = delta*lam;

                             v_scal   += con1*L[i]/(ONE+delta*L[i]);
                             vrat = EXP(con1*v_scal + lam*(sqez-HALF*con1));

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

             }
             b = ONE;

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

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

                     B[j-nmat] = b;
                     S[j-nmat] = s;

             }

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

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

                     if (swapval < ZERO)
                             v_scal += MHUND*swapval;

             }

             // apply discount //
             for (j=0; j<nmat; j++){

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

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

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

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

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

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

 #pragma omp parallel 
 for(long long int path = beg; path < end; path += chunk_inner){  

      #pragma omp for nowait 

      for (long long int path_=0; path_<chunk_inner; path_+=SIMDVLEN) { 
             int    i, j, k; 

              __declspec(align(64)) FPPREC B[nmat][SIMDVLEN], S[nmat][SIMDVLEN], L[n][SIMDVLEN]; 
             __declspec(align(64)) FPPREC sqez[SIMDVLEN], lam[SIMDVLEN], con1[SIMDVLEN], v_scal[SIMDVLEN], vrat[SIMDVLEN], vrat_tmp[SIMDVLEN]; 

             __declspec(align(64)) FPPREC b[SIMDVLEN], s[SIMDVLEN], swapval[SIMDVLEN]; 
              FPPREC *zlocal=z+nmat*(tid*chunk_inner + path_); 

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

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

             for(i=0;i<n;i++) { 
                     L[i][:] = L0[i]; 

             } 

             for(j=0; j<nmat; j++) { 
                     sqez[:] = SQRT(delta)*zlocal[0:SIMDVLEN]; 

                     v_scal[:] = ZERO; 
                             zlocal += SIMDVLEN; 

                     for (i=j+1; i<n; i++) { 
                             lam[:]  = lambda[i-j-1]; 

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

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

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

             } 

             b[:] = ONE; 
             s[:] = ZERO; 

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

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

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

             } 

             v_scal[:] = ZERO; 

             for (i=0; i<nopt; i++){ 
                     k = maturities[i] -1; 

                     swapval[:] = B[k][:] + swaprates[i]*S[k][:] - ONE; 
                     if (swapval[:] < ZERO) 

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

             // apply discount //         

             for (j=0; j<nmat; j++){ 
                     v_scal[:] = v_scal[:]/(ONE+delta*L[j][:]); 

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

      } 

}

要点

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

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

下一步工作

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

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

Para obtener información más completa sobre las optimizaciones del compilador, consulte nuestro Aviso de optimización.