performance - 为什么阵列排列会影响加速?
问题描述
我正在使用 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 编译。
预先感谢您的帮助。
解决方案
上述行为是由于对一维零件产生负面影响的错误共享问题造成的。这个问题在文献中是众所周知的,简言之,当多线程在同一高速缓存行上运行时就会出现。在 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
并不能解决错误共享问题。
推荐阅读
- c# - 如何制作新的 IEnumerable 集合并返回它?
- excel - 重复剪切和粘贴范围/块/数据(具有不同行的范围由特定字符串确定)
- macos - 适用于 MacOS 的虚拟打印机驱动程序
- javascript - JS先按首都过滤对象数组,然后按字母顺序
- ruby-on-rails - 出乎意料的是,我无法使用 rails -s 启动 Rails 服务器
- laravel - 在 Flutterwave 实时模式下遇到问题
- ios - 在 XCode 的嵌入框架中使用“仅在安装时复制”
- javascript - 从 .bind 和 .MouseUp 停止 jquery 函数
- python - 如何将文本框中的输入文本存储在数据库中,同时防止安全漏洞
- html - 指针事件仅在桌面上禁用