缓存模块化技术
概述
一类重要的算法变化涉及模块数据结构以便能够适应高速缓存。通过合理组织数据内存访问,用户可以使用大型数据集中的一小部分来加载高速缓存。此方法也适用于缓存中的该数据模块。通过使用/重复使用高速缓存中的数据,我们可显著降低内存访问需求(减少内存带宽压力)。
主题
模块化是一种非常著名的优化技术,可在许多应用中帮助避免内存带宽瓶颈。模块化的关键思路是确保数据在多种使用情形下都位于高速缓存中,从而尽可能地重复利用应用中的数据。模块化可在 1-D、2-D 或 3-D 空间数据结构上执行。某些迭代应用可以从多次迭代操作(通常被称为临时模块化)的模块化获益,并进一步减少带宽瓶颈。代码变更方面,模块化通常涉及循环分割与变换的组合。在大多数应用代码中,如果用户能够做出正确的源代码调整,并对模块化因素进行一定的参数化处理,便可取得最佳的模块化执行效果。
原始源代码:
for (body1 = 0; body1 < NBODIES; body1 ++){
for (body2=0; body2 < NBODIES; body2++) {
OUT[body1] += compute(body1, body2);
}
}
在本例中,数据 (body2) 从内存流出。假设 NBODIES 较大,我们不能在高速缓存中重复使用 => 该应用受内存带宽的限制(应用将以内存到处理器的速度运行,这并不是最理想的)
修改源代码(带 1-D 模块化):
for (body2 = 0; body2 < NBODIES; body2 += BLOCK) {
for (body1=0; body1 < NBODIES; body1 ++) {
for (body22=0; body22 < BLOCK; body22 ++) {
OUT[body1] += compute(body1, body2 + body22);
}
}
}
在修改后的代码中,高速缓存中保留了数据 (body22) 并可以重复使用 => 更高的性能
例如,上面的代码片段显示了模块化 NBody 代码的示例。所有主体部分都有两个循环(body1 和 body2)进行迭代顶部数据流上的原始代码到内部循环中的整个主体集,而且每次迭代时必须从内存加载 body2。底部的模块化代码通过将 body2 分割为一个外部循环(在多个 BLOCK 的主体上进行迭代)、一个内部 body22(迭代模块内的相关元素)循环,并将 body1 和 body2 循环进行交叉而获得。该代码可重复使用跨 body1 循环的多个迭代的一组 BLOCK body2 值。如果所选择的 BLOCK(例如这一组值)适合高速缓存,内存流量便会下降,具体取决于 BLOCK 因素。
以下是 NBody 性能指标评测 OpenMP* 版的相关代码片段(带有使用 CHUNK_SIZE 因素的模块化 )。在本例中,unroll-jam 转换表示为编译指令,并由编译器负责处理。 在这些情况下,研究来自-opt-报告的输出可证实该编译器是否针对您的循环执行了 unroll-jam 优化。
#define CHUNK_SIZE 8192
#pragma omp parallel private(body_start_index)
for(body_start_index = 0; body_start_index < global_number_of_bodies; body_start_index += CHUNK_SIZE){
int i;
int body_end_index = body_start_index + CHUNK_SIZE;
#pragma omp for private(i) schedule(guided)
#pragma unroll_and_jam (4)
for(i=starting_index; i<ending_index; i++){
int j;
TYPE acc_x_0 = 0, acc_y_0 = 0, acc_z_0 = 0;
for(j=body_start_index; j<body_end_index; j+=1){
TYPE delta_x_0 = Input_Position_X[(j+0)] - Input_Position_X[i];
TYPE delta_y_0 = Input_Position_Y[(j+0)] - Input_Position_Y[i];
TYPE delta_z_0 = Input_Position_Z[(j+0)] - Input_Position_Z[i];
TYPE gamma_0 = delta_x_0*delta_x_0 + delta_y_0*delta_y_0 + delta_z_0*delta_z_0 + epsilon_sqr;
TYPE s_0 = Mass[j+0]/(gamma_0 * SQRT(gamma_0));
acc_x_0 += s_0*delta_x_0;
acc_y_0 += s_0*delta_y_0;
acc_z_0 += s_0*delta_z_0;
}
Output_Acceleration[3*(i+0)+0] += acc_x_0;
Output_Acceleration[3*(i+0)+1] += acc_y_0;
Output_Acceleration[3*(i+0)+2] += acc_z_0;
}
}
下面的示例为 Fortran 中的矩阵乘法代码,用户在其中执行高级转换(在修改版本中),其中涉及本地复制-数组,以实现最高的性能。
Fortran 源代码示例:
do j=1,N
do k = 1,N
do i = 1,N
c(i,j) = c(i,j) + a(i,k) * b(k,j)
end do
end do
end do
修改后的 Fortran 源代码:
do JJ = 1, N, TJ
do KK = 1, N, TK
do jjj = 1,min(tj,N-jj+1) ! BCOPY - no transpose
do kkk = 1, min(tk,N-kk+1)
p(kkk,jjj-1+1) = B(kk+kkk-1, jj+jjj-1)
end do
end do
do II = 1, N, TI
do iii = 1,
min(ti,N-ii+1) !ACOPY - transpose
do kkk = 1, min(tk,N-kk+1)
Q(kkk,iii) = A(ii+iii-1, kk+kkk-1)
end do
end do
do J = 1, min(tj,N-jj+1), 4
do I = 1, min(ti,N-ii+1), 2
t1 = 0 ; t2 = 0 ; t5 = 0 ; t6 = 0 ; t9 = 0 ; t10 = 0 ; t13 =0 ; t14 = 0
!DIR$ vector aligned !DIR$ unroll(2)
do K = 1,min(TK,N-kk+1) ! Innermost loop, vectorized and unrolled by 2 after that
qi = Q(K,I) ; qi1 = Q(K,I+1)
t1 = t1+qi*P(K,J) ; t2 = t2+ qi1*P(K,J)
t5 = t5+ qi*P(K,J+1) ; t6 = t6+ qi1*P(K,J+1)
t9 = t9+ qi*P(K,J+2) ; t10 = t10+ qi1*P(K,J+2)
t13 = t13+ qi*P(K,J+3); t14 = t14+qi1*P(K,J+3)
end do
c(i+ii-1,j+jj-1) = c(i+ii-1,j+jj-1) +t1 ; c(i+1+ii-1,j+jj-1) = c(i+1+ii-1,j+jj-1) + t2
c(i+ii-1,j+1+jj-1) = c(i+ii-1,j+1+jj-1) + t5 ; c(i+1+ii-1,j+1+jj-1) = c(i+1+ii-1,j+1+jj-1) + t6
c(i+ii-1,j+2+jj-1) = c(i+ii-1,j+2+jj-1) + t9 ; c(i+1+ii-1,j+2+jj-1) = c(i+1+ii-1,j+2+jj-1) + t10
c(i+ii-1,j+3+jj-1) = c(i+ii-1,j+3+jj-1) + t13 ; c(i+1+ii-1,j+3+jj-1) = c(i+1+ii-1,j+3+jj-1) + t14
end do
end do
end do
end do
end do
Take Aways
高速缓存模块化是一种重新安排数据访问的技术,它能够将数据子集(模块)放入高速缓存中以便在该模块上运行,进而避免从主内存重复预取数据。正如上面的示例所示,用户可以这种方法手动实现循环数据的模块化以重复使用高速缓存。对于性能关键型循环,性能分析指出了内存带宽限制,而且 -opt- 报告显示编译器不能以最优的方式对循环进行模块化,您可能要考虑手动展开循环,以便更好的进行数据模块化处理进而重复使用高速缓存。
下一步工作
要在英特尔® 至强 融核™ 协处理器上成功调试您的应用,请务必通读此指南,并点击文中的超链接查看相关内容。本指南提供了实现最佳应用性能所要执行的步骤。
