为什么数组排列会影响加速?

Why the array arrangment affects speedup?

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

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

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 部分中,时间从 ~6s 增加到 ~20s 从串行(不使用 -fopenmp 编译)到与 1 个线程并行?为什么二维数组的行为与观察到的一维数组的行为如此不同?

正如@IanBush 所建议的,我使用隐式 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 位长,而真正*8(双精度)是 64 位长。但是,问题依旧。

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

编译的

提前感谢您的帮助。

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

为了避免伪共享问题,建议使用"padding",即强制线程访问不同的缓存行,也就是说线程访问的数组元素不能在同一个缓存行.假设一个cache line可以包含8个real*8,我修改了1D的部分来去掉false sharing的问题如下

! 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 不能解决错误共享问题。