为了练习并行化do循环,我在Fortran中进行以下积分
$\integral{0}{1} \frac{4}{1+x^{2}} = \pi$
以下是我实施的代码:
program mpintegrate
integer i,nmax,nthreads,OMP_GET_NUM_THREADS
real xn,dx,value
real X(100000)
nthreads = 4
nmax = 100000
xn = 0.0
dx = 1.0/nmax
value = 0.0
do i=1,nmax
X(i) = xn
xn = xn + dx
enddo
call OMP_SET_NUM_THREADS(nthreads)
!$OMP Parallel
!$OMP Do Schedule(Static) Private(i,X)
do i=1,nmax
value = value + dx*(4.0/(1+X(i)*X(i)))
enddo
!$OMP End DO NoWait
!$OMP End Parallel
print *, value
end
编译程序时没有问题
gfortran -fopenmp -o mpintegrate mpintegrate.f
问题是当我执行程序时。当我按原样运行程序时,我得到的值(1,4)。然而,当我使用omp do循环取消注释print语句时,最终值大约是应该是什么,pi。
为什么value
的答案不正确?
答案 0 :(得分:5)
这里的一个问题是X
不需要是私有的(需要在并行线上指定,而不是do行);每个人都需要看到它,并且每个线程都有单独的副本没有意义。更糟糕的是,您在此处访问私有副本所获得的结果是未定义的,因为一旦您进入私有区域,私有变量就没有被初始化。您可以使用firstprivate
而不是private
,它会根据并行区域之前的内容为您初始化它,但最简单/最好的只是shared
。
由于no wait
必须等待所有人都完成,因此也没有多大意义让end parallel
结束。
然而,话虽如此,你仍然有一个非常重要(和经典)的正确性问题。如果您在循环中更明确一点,那么此处发生的事情会更清楚(由于问题不依赖于所选择的时间表而放弃了明确的时间表):
!$OMP Parallel do Private(i) Default(none) Shared(value,X,dx,nmax)
do i=1,nmax
value = value + dx*(4.0/(1+X(i)*X(i)))
enddo
!$OMP End Parallel Do
print *, value
重复运行会产生不同的值:
$ ./foo
1.6643878
$ ./foo
1.5004054
$ ./foo
1.2746993
问题是所有线程都写入相同的共享变量value
。这是错误的 - 每个人都在立即写作,结果是胡言乱语,因为一个主题可以计算它自己的贡献,准备好将它添加到value
,就像它要做的那样,另一个线程可以 写入value
,然后迅速被破坏。对同一共享变量的并发写入是一个经典的race condition,这是一个标准的错误系列,特别是在与OpenMP一样的共享内存编程中。
除了错误之外,它还很慢。争用相同的几个字节的内存的许多线程 - 内存足够接近以便落入同一缓存行 - 由于内存系统中的争用而非常慢。即使它们不是完全相同的变量(在这种情况下也是如此),这种内存争用 - False Sharing在它们恰好是相邻变量的情况下 - 可以事情明显减慢了。取出显式线程数设置,并使用环境变量:
$ export OMP_NUM_THREADS=1
$ time ./foo
3.1407621
real 0m0.003s
user 0m0.001s
sys 0m0.001s
$ export OMP_NUM_THREADS=2
$ time ./foo
3.1224852
real 0m0.007s
user 0m0.012s
sys 0m0.000s
$ export OMP_NUM_THREADS=8
$ time ./foo
1.1651508
real 0m0.008s
user 0m0.042s
sys 0m0.000s
因此,使用更多线程运行的速度几乎慢了3倍(而且越来越多)。
那么我们可以做些什么来解决这个问题呢?我们可以做的一件事是确保每个人的添加都不会被atomic
指令覆盖:
!$OMP Parallel do Schedule(Static) Private(i) Default(none) Shared(X,dx, value, nmax)
do i=1,nmax
!$OMP atomic
value = value + dx*(4.0/(1+X(i)*X(i)))
enddo
!$OMP end parallel do
解决了正确性问题:
$ export OMP_NUM_THREADS=8
$ ./foo
3.1407621
但对速度问题没有任何作用:
$ export OMP_NUM_THREADS=1
$ time ./foo
3.1407621
real 0m0.004s
user 0m0.001s
sys 0m0.002s
$ export OMP_NUM_THREADS=2
$ time ./foo
3.1407738
real 0m0.014s
user 0m0.023s
sys 0m0.001s
(注意,对于不同的线程数,你会得到略有不同的答案。这是由于最终总和的计算顺序与串行情况不同。使用单精度实数,差异显示在第7位,因为不同操作的排序很难避免,在这里我们要做100,000次操作。)
那我们还能做些什么呢?一种方法是让每个人都能跟踪他们自己的部分金额,然后在我们完成时将它们汇总在一起:
!...
integer, parameter :: nthreads = 4
integer, parameter :: space=8
integer :: threadno
real, dimension(nthreads*space) :: partials
!...
partials=0
!...
!$OMP Parallel Private(value,i,threadno) Default(none) Shared(X,dx, partials)
value = 0
threadno = omp_get_thread_num()
!$OMP DO
do i=1,nmax
value = value + dx*(4.0/(1+X(i)*X(i)))
enddo
!$OMP END DO
partials((threadno+1)*space) = value
!$OMP end parallel
value = sum(partials)
print *, value
end
这很有效 - 我们得到了正确的答案,如果你玩线程的数量,你会发现它非常活泼 - 我们在部分和数组中间隔出条目避免错误共享(这是错误的,这一次,因为每个人都在写入数组中的不同条目 - 没有覆盖)。
尽管如此,这是一项愚蠢的工作,只是为了在线程之间获得正确的总和!有一种更简单的方法--OpenMP有一个reduction构造来自动执行此操作(并且比上面的手工版本更有效:)
!$OMP Parallel do reduction(+:value) Private(i) Default(none) Shared(X,dx)
do i=1,nmax
value = value + dx*(4.0/(1+X(i)*X(i)))
enddo
!$OMP end parallel do
print *, value
现在程序运行正常,速度快,而且代码非常简单。更现代的Fortran中的最终代码看起来像这样:
program mpintegrate
use omp_lib
integer, parameter :: nmax = 100000
real :: xn,dx,value
real :: X(nmax)
integer :: i
integer, parameter :: nthreads = 4
xn = 0.0
dx = 1.0/nmax
value = 0.0
partials=0
do i=1,nmax
X(i) = xn
xn = xn + dx
enddo
call omp_set_num_threads(nthreads)
!$OMP Parallel do reduction(+:value) Private(i) Default(none) Shared(X,dx)
do i=1,nmax
value = value + dx*(4.0/(1+X(i)*X(i)))
enddo
!$OMP end parallel do
print *, value
end