如何使用OpenMP有效地并行化链表(使用任务?)

时间:2013-06-11 16:34:22

标签: fortran openmp

这是一个很长的帖子 - 问题之前的很多背景。快速版本是我尝试在链接列表的元素上使用OpenMP - 以我在别处看到的方式使用OpenMP任务,但这会导致显着的减速。但是,如果我以不同的方式划分,我可以获得显着的加速,但我想知道是否有办法获得第一种工作方式,因为它更干净/更简单(我认为)它可以动态平衡线程中的工作。

我有一个相当长的链表(可能是几百万个元素)的Fortran类型(C结构)和 - 几次 - 我必须迭代列表并对每个元素进行操作。所以,我有一个子程序(eachPhonon),它将子程序作为参数(srt)并在列表的每个元素上进行操作:

subroutine eachPhonon(srt)
  external :: srt
  type(phonon), pointer :: tptr

  tptr => head

  do while(associated(tptr))
    call srt(tptr)
    tptr => tptr%next
  enddo
endsubroutine

这似乎是并行加速的好地方,因为srt的每次调用都可以独立于其他调用完成。如果我有一个Fortran do(C for)循环,那么使用openmp会非常简单。但是,我已经在stackoverflowintel上看到了如何使用链接列表进行操作的方法。基本上,它会使每次调用都成为自己的任务 - 类似于:

subroutine eachPhonon(srt)
  external :: srt
  type(phonon), pointer :: tptr

  tptr => head

  !$OMP PARALLEL
  !$OMP SINGLE    
    do while(associated(tptr))
      !$OMP TASK FIRSTPRIVATE(tptr)
        call srt(tptr)
      !$OMP END TASK
      tptr => tptr%next
    enddo
  !$OMP END SINGLE
  !$OMP END PARALLEL
endsubroutine

这似乎有效,但它比仅使用一个线程慢得多。

我重写了一些东西,所以给出了4个线程,一个线程可以运行元素1,5,9 ...,另一个运行元素2,6,10 ......等等:

subroutine everyNth(srt, tp, n)
  external :: srt

  type(phonon), pointer :: tp
  integer :: n, j

  do while(associated(tp))
    call srt(tp)

    do j=1,n
      if(associated(tp)) tp => tp%next
    enddo
  enddo
endsubroutine

subroutine eachPhononParallel(srt)
  use omp_lib
  external :: srt

  type(phonon), pointer :: tp
  integer :: j, nthreads

  !$OMP PARALLEL
  !$OMP SINGLE
    nthreads = OMP_GET_NUM_THREADS()
    tp => head
    do j=1,nthreads
      !$OMP TASK FIRSTPRIVATE(tp)
        call everyNth(srt, tp, nthreads)
      !$OMP END TASK
      tp => tp%next
    enddo
  !$OMP END SINGLE
  !$OMP END PARALLEL
endsubroutine

这可以带来显着的加速。

有没有办法让第一种方法有效?

我是并行处理的新手,但我的阅读是第一种方法有太多的开销,因为它试图为每个元素创建一个任务。第二种方法只为每个线程创建一个任务,并避免这种开销。缺点是不那么干净的代码,如果没有openmp就无法编译,并且它不会动态平衡线程间的工作 - 它们在开始时都是静态分配的。

2 个答案:

答案 0 :(得分:4)

如果您的并行度的粒度太小,您可能会尝试使用更大尺寸的块:

subroutine eachPhonon(srt,chunksize)
  external            :: srt
  integer, intent(in) :: chunksize

  type(phonon), pointer :: tptr

  tptr => head

  !$OMP PARALLEL
  !$OMP SINGLE    
    do while(associated(tptr))
      !$OMP TASK FIRSTPRIVATE(tptr)
        ! Applies srt(tptr) chunksize times or until 
        ! associated(tptr)
        call chunk_srt(tptr,chunksize) 
      !$OMP END TASK
      ! Advance tptr chunksize times if associated(tptr)
      advance(tprt,chunksize) 
    enddo
  !$OMP END SINGLE
  !$OMP END PARALLEL
endsubroutine

我们的想法是将chunksize设置为足以掩盖与任务创建相关的开销的值。

答案 1 :(得分:2)

减速意味着srt()执行时间太短,因此开销会淹没可能的并行加速。除了Massimiliano的解决方案,你还可以将链表转换为指针数组,然后在结果结构上使用PARALLEL DO

type phononptr
  type(phonon), pointer :: p
endtype phononptr

...

subroutine eachPhonon(srt)
  external :: srt
  type(phonon), pointer :: tptr
  type(phononptr), dimension(:), allocatable :: ptrs
  integer :: i

  allocate(ptrs(numphonons))

  tptr => head
  i = 1

  do while(associated(tptr))
    ptrs(i)%p => tptr
    i = i + 1
    tptr => tptr%next
  enddo

  !$OMP PARALLEL DO SCHEDULE(STATIC)
  do i = 1, numphonons
    call srt(ptrs(i)%p)
  enddo
  !$OMP END PARALLEL DO

endsubroutine

如果没有明确地将列表项的数量保存在单独的变量中(在这种情况下为numphonons),则必须遍历列表两次。 phononptr类型是必需的,因为Fortran缺少一种更简单的方法来声明指针数组。

将Massimiliano的解决方案中的chunksize设置为numphonons / omp_get_num_threads()也可以实现同样的效果。