首页 > 解决方案 > 为什么阵列排列会影响加速?

问题描述

我正在使用 gfortran 和 openmp 编写并行计算代码。在台式机(配备 Intel(R) Core(TM) i3-3220 CPU @ 3.30GHz)上测试代码时,我注意到您排列数据的方式会显着影响运行时间和加速。实际上,将阵列排列在具有 nx 个单元的 1D 上或具有相同数量的单元的 2D 上,这些单元分布在 nxx 行和 N_threads 行上。请注意,N_threads 表示已使用的线程数。

为了更好地理解和量化这一点,我编写了一个运行完全相同数量的操作的简短代码。代码如下:

program testMem
Use omp_lib            
implicit real*8 (a-h,o-z)
integer nx,nxx,N_threads 
parameter (N_threads=4,nx=1E8,nxx=int(nx/N_threads))
real*8  x(1:nx)
real*8  xx(1:nxx,0:N_threads-1)

x(1:nx)=0.
xx(1:nxx,0:N_threads-1)=0.

call system_clock(count_rate=icr)
timerRate=real(icr)     

CALL OMP_SET_NUM_THREADS(N_threads)

! 1D
call system_clock(ic1)
t0=omp_get_wtime()      
!$omp parallel do shared (x) private(i,j)
do i=1,nx
  do j=1,100 
    x(i)=x(i)+real(j*j)
  end do
end do 
!$omp end parallel do
call system_clock(ic2)      
t1=omp_get_wtime()
write (*,*) (ic2-ic1)/timerRate,t1-t0

! 2D
call system_clock(ic1)
t0=omp_get_wtime()   
!$omp parallel do shared(x) private(i_threads,i,j)
do i_threads=0,N_threads-1
  do i=1,nxx
    do j=1,100 
      xx(i,i_threads)=xx(i,i_threads)+real(j*j)
    end do
  end do 
end do
!$omp end parallel do
call system_clock(ic2)      
t1=omp_get_wtime()
write (*,*) (ic2-ic1)/timerRate,t1-t0

end program

我的期望是代码的并行版本比串行版本运行得更快,与阵列的排列无关。但是,我发现我的机器花费

                   1D           2D
serial            5.96         5.96     
1thread          21.30         5.98
2threads         10.72         2.98
3threads          8.20         8.11
4threads          6.30         2.91

我想知道是否有人可以解释这里发生了什么?为什么在 1D 部分中,从串行(不使用 -fopenmp 编译)到与 1 个线程并行的时间从 ~6s 增加到 ~20s?为什么二维数组的行为与观察到的一维数组的行为如此不同?

正如@IanBush 所建议的那样,我使用implicit none 并以真正简单的精度声明所有变量。现在运行代码需要

                   1D           2D
serial            4.61         4.13     
1thread          12.16         4.18
2threads          6.14         2.09
3threads          5.46         5.65
4threads          4.63         1.85

运行时间更好,因为真正的简单精度是 32 位长,而 real*8(双精度)是 64 位长。但是,问题仍然存在。

我还按照@IanBush 的建议澄清我使用安装在 Ubuntu 12.04 上的 GNU Fortran (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3。代码使用 gfortran -mcmodel=medium -O3 -fopenmp -o cmd 编译。

预先感谢您的帮助。

标签: performancefortranopenmpgfortran

解决方案


上述行为是由于对一维零件产生负面影响的错误共享问题造成的。这个问题在文献中是众所周知的,简言之,当多线程在同一高速缓存行上运行时就会出现。在 1D 部分,这可能会经常发生,因为 2 个线程可能会访问相邻的数组元素。相比之下,上述代码的 2D 部分,每个线程都在自己的列上工作。因此,由于 fortran 是列顺序的,不同的线程处理从不在同一高速缓存行中的数据。

为了避免错误共享问题,建议使用“填充”,即强制线程访问不同的缓存行,这意味着线程访问的数组元素不能在同一个缓存行中。假设一个缓存行可以包含8个真实*8,我修改了1D部分以摆脱错误共享问题如下

! 1D
call system_clock(ic1)
t0=omp_get_wtime()      
!$omp parallel do shared (x) private(i,j,i_leap,ii)
do i=1,int(nx/nCacheSize)
  do i_leap=1,nCacheSize
    ii=(i-1)*nCacheSize+i_leap
    do j=1,100 
      x(ii)=x(ii)+real(j*j)
    end do
  end do
end do 
!$omp end parallel do
call system_clock(ic2)      
t1=omp_get_wtime()
write (*,*) (ic2-ic1)/timerRate,t1-t0  

请注意,nCacheSize 被声明为parameter (nCacheSize=8)

我在 Intel(R) Core(TM) i5-3320M CPU @ 2.60GHz 上测试代码,结果如下

                   1D           2D
serial            3.99         3.92     
1thread           3.86         3.93
2threads          2.05         2.07
3threads          2.19         5.46
4threads          1.98         1.98

3 个线程的运行时间增加可能是由于 CPU 硬件由 2 个内核和每个内核 2 个线程组成。

我强调nCacheSize必须通过 声明为常量parameter (nCacheSize=8),否则使用包含变量的边界进行计算i_leap并不能解决错误共享问题。


推荐阅读