Performance 为什么阵列排列会影响加速?
我正在使用gfortran和openmp为并行计算编写代码。在台式机(配备Intel(R)Core(TM)i3-3220 CPU@3.30GHz)上测试代码时,我注意到您排列数据的方式会显著影响运行时间和加速。实际上,在1D上使用nx单元排列阵列,或者在2D上使用相同数量的单元排列阵列,这些单元分布在nxx行和N_行上。请注意,N_threads表示使用的线程数 为了更好地理解和量化这一点,我编写了一个运行完全相同数量操作的简短代码。代码如下所示:Performance 为什么阵列排列会影响加速?,performance,fortran,openmp,gfortran,Performance,Fortran,Openmp,Gfortran,我正在使用gfortran和openmp为并行计算编写代码。在台式机(配备Intel(R)Core(TM)i3-3220 CPU@3.30GHz)上测试代码时,我注意到您排列数据的方式会显著影响运行时间和加速。实际上,在1D上使用nx单元排列阵列,或者在2D上使用相同数量的单元排列阵列,这些单元分布在nxx行和N_行上。请注意,N_threads表示使用的线程数 为了更好地理解和量化这一点,我编写了一个运行完全相同数量操作的简短代码。代码如下所示: program testMem Use omp
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个线程并行?为什么2D阵列的行为与观察到的一维阵列的行为如此不同
正如@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
由于real simple precision的长度为32位,而real*8(双精度)的长度为64位,因此运行时间更好。然而,问题依然存在
我还澄清了@IanBush的建议,我使用安装在Ubuntu 12.04上的GNU Fortran(Ubuntu/Linaro 4.6.3-1ubuntu5)4.6.3。代码是用gfortran-mcmodel=medium-O3-fopenmp-ocmd编译的
提前感谢您的帮助 上述行为是由于错误共享问题造成的,该问题会对1D部件产生负面影响。这个问题在文献中是众所周知的,简言之,当多个线程在同一个缓存线上运行时就会出现。在1D部分,这种情况可能经常发生,因为两个线程可能会访问相邻的数组元素。相比之下,在上述代码的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
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
请注意,nCacheSize声明为参数(nCacheSize=8)
我在Intel(R)Core(TM)i5-3320M CPU@2.60GHz上测试了代码,结果如下
! 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
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
必须通过参数(nCacheSize=8)
声明为常量,否则使用包含变量的边界计算I_leap
并不能解决错误共享问题。您能告诉我们什么编译器,什么版本的编译器,以及您正在使用的编译器和链接标志。请使用隐式无-我的观点是,回答那些不必费心做防御性编码基础的问题是不值得的。请不要使用完全非标准、不可移植的real*8。如果您的问题是openmp如何影响死代码消除,那当然取决于编译器和选项。如果不值得您花时间检查生成的代码,那么可能不值得我们检查。谢谢@IanBush的建议和关注。我在桌面计算机中使用的编译器是安装在Ubuntu 12.04上的gfortran。编译器的版本是GNU Fortran(Ubuntu/Linaro 4.6.3-1ubuntu5)4.6.3。代码是用gfortran-mcmodel=medium-O3-fopenmp-o编译的。据我所知,实数*8是双精度的,是一种标准的实数。隐式声明的使用不应影响计算持续时间。但是,我使用隐式none和默认real运行代码。行为是一样的。我编辑我的问题是为了提供所有这些信息。你不是在衡量你认为你是什么。第一个并行区域是OpenMP运行时创建线程池的地方,因此您在第一次度量中包含了重要的时间,这将解释大部分差异。在进行任何计时之前,添加一个单独的、空的、未计时的平行区域……感谢@JimCownie的建议。创建线程池所需的时间不应超过几毫秒。因此,它可能会对代码的行为产生轻微的影响,但不会达到所讨论的数量级。此外,我对1D和2D部分进行了排列测试,运行时是相同的。