为什么启动更多进程时基于 Fortran 的 MPI 例程需要更多计算时间

Why does Fortran based MPI routine take more compute time when more processes are launched

我基于 Fortran 的 MPI 代码不涉及进程间通信。只有每个进程的计算才会完成并计时。我的平台是 Intel Sandy Bridge。代码是使用 mpiifort 包装器编译的。我有两个无法解释的观察结果:

1) 计算时间随着涉及的机器越来越多而增加,其中每台机器有 16 个内核(2 个处理器,每个处理器 8 个内核)。例如,16 个 MPI 等级最多耗时 5.74 秒,32 个耗时 13.64 秒,48 个耗时 18.26 秒,而 64 个耗时 25.92 秒。由于不涉及任何通信,因此无论名称 MPI 排名如何启动,我都希望得到大致相同的时间。代码包含在下面。

2) 在下面代码的第 2 步中,调用了一个子例程。如果我用实际代码替换子例程调用,程序运行得更快。例如,16 个 MPI rank 最多耗时 5.63E-02 秒,32 个耗时 0.1762,48 个耗时 0.33 秒,而 64 个耗时 0.3612 秒。

使用 ifort 编译的程序的串行版本也表现出类似的行为:子程序调用需要 0.77 秒,没有子程序调用需要 5.19E-02 秒。

我在这里附上代码。一是不调用子程序,二是调用子程序,三是子程序本身

mult.f(无子程序调用):

  program mul
  include 'mpif.h'

  integer DIM, dim1, dim2, dim3, E
  parameter (DIM=8, E=512, dim1=DIM, dim2=DIM, dim3=DIM)
  integer ierr, rank, size, t2, t4, t6, t8
  real*8 A(dim1, dim1), B(dim2, dim2), C(dim3, dim3)
  real*8 u(dim1, dim2, dim3, E)
  real*8 dudr (dim1, dim2, dim3, E), duds (dim1, dim2, dim3, E)
  real*8 dudt (dim1, dim2, dim3, E), dudr2 (dim1, dim2, dim3)
  integer val, i, j, k, l, r, s, t, start, end
  integer iseed /3/
  real tbeg, tend
  CHARACTER(len=32) :: arg

  call getarg(1, arg)
  call MPI_INIT (ierr)
  call MPI_COMM_SIZE(MPI_COMM_WORLD, size, ierr)
  call MPI_COMM_RANK(MPI_COMM_WORLD, rank, ierr)

  ! Step 1: Initialize
  do i = 1, dim1
       do j = 1, dim1
           A (i, j) = ran(iseed);
       enddo
  enddo

  do i = 1, dim2
       do j = 1, dim2
           B (i, j) = A(j, i);
       enddo
  enddo

  do i = 1, dim3
       do j = 1, dim3
           C (i, j) = A(j, i);
       enddo
  enddo

  do m = 1, E
  do i=1, dim1
        do j=1, dim2
              do k = 1, dim3
                     u (i, j, k, m) = (ran(iseed)*400.0) - 200.0
                  enddo
        enddo
  enddo
  enddo

  ! Step 2: Compute derivatives
  tbeg = mpi_wtime()

  do m = 0, 5000
      do ie=1, E
         DO t2=1,8
         DO t4=1,8
         DO t6=1,8
         DO t8=1,8
             dudr(t8,t4,t6,ie) = 0
             dudr(t8,t4,t6,ie)=dudr(t8,t4,t6,ie)+u(t2,t4,t6,ie)
 $                             *C(t8,t2)
         ENDDO
         DO t8=1,8
             dudr(t8,t4,t6,ie)=dudr(t8,t4,t6,ie)
 $                             +u(t2,t4,t6,ie)*C(t8,t2)

         ENDDO
         ENDDO
         ENDDO
         ENDDO
      enddo
  enddo
  tend = mpi_wtime()
  print *, DIM, E, tend-tbeg, rank

  call MPI_FINALIZE (ierr)

  end

mult.f(带子程序)

  program mul
  include 'mpif.h'

  integer DIM, dim1, dim2, dim3, E
  parameter (DIM=8, E=512, dim1=DIM, dim2=DIM, dim3=DIM)
  integer ierr, rank, size
  real*8 A(dim1, dim1), B(dim2, dim2), C(dim3, dim3)
  real*8 u(dim1, dim2, dim3, E)
  real*8 dudr (dim1, dim2, dim3, E), duds (dim1, dim2, dim3, E)
  real*8 dudt (dim1, dim2, dim3, E), dudr2 (dim1, dim2, dim3)
  integer val, i, j, k, l, r, s, t, start, end
  integer iseed /3/
  real tbeg, tend
  CHARACTER(len=32) :: arg

  call getarg(1, arg)
  call MPI_INIT (ierr)
  call MPI_COMM_SIZE(MPI_COMM_WORLD, size, ierr)
  call MPI_COMM_RANK(MPI_COMM_WORLD, rank, ierr)

  ! Step 1: Initialize
  do i = 1, dim1
       do j = 1, dim1
           A (i, j) = ran(iseed);
       enddo
  enddo

  do i = 1, dim2
       do j = 1, dim2
           B (i, j) = A(j, i);
       enddo
  enddo

  do i = 1, dim3
       do j = 1, dim3
           C (i, j) = A(j, i);
       enddo
  enddo

  do m = 1, E
  do i=1, dim1
        do j=1, dim2
              do k = 1, dim3
                     u (i, j, k, m) = (ran(iseed)*400.0) - 200.0
                  enddo
        enddo
  enddo
  enddo

  ! Step 2: Compute derivatives
  tbeg = mpi_wtime()

  do m = 0, 5000
      do ie=1, E
         call mysubroutine(u, C, dudr, ie)
      enddo
  enddo
  tend = mpi_wtime()
  print *, DIM, E, tend-tbeg, rank

  call MPI_FINALIZE (ierr)

  end

mysubroutine(子程序调用):

   SUBROUTINE mysubroutine(u, a, dudr, ie)
   integer DIM, dim1, dim2, dim3, E
   parameter (DIM=8, E=512, dim1=DIM, dim2=DIM, dim3=DIM)
   real*8 u(DIM, DIM, DIM, E)
   real*8 a(DIM, DIM)
   real*8 dudr (DIM, DIM, DIM, E)
     integer t8
     integer t6
     integer t4
     integer t2
     integer i
     integer j
     integer k
     integer l
   !dir$ ASSUME_ALIGNED a:    64
   !dir$ ASSUME_ALIGNED u:    64
   !dir$ ASSUME_ALIGNED dudr: 64
     DO t2=1,8
     DO t4=1,8
     DO t6=1,8
     DO t8=1,8
     dudr(t8,t4,t6,ie)
 $     =0
     dudr(t8,t4,t6,ie)=dudr(t8,t4,t6,ie)+u(t2,t4,t6,ie)
 $     *a(t8,t2)
   ENDDO
     DO t8=1,8
     dudr(t8,t4,t6,ie)=dudr(t8,t4,t6,ie)
 $     +u(t2,t4,t6,ie)*a(t8,t2)
   ENDDO
   ENDDO
   ENDDO
   ENDDO
   END

使用以下命令编译代码:

mpiifort -O3 -mcmodel=large -xavx mult.f rose.f -o baseline

这里有两个不同的问题:

  1. 为什么调用子程序的代码比较慢;和
  2. 为什么随着 MPI 进程数量的增加,代码会变慢。

我将尝试按顺序回答(我必须承认我的猜测是疯狂的)。

子程序较慢

如果您查看您的代码,您会发现为了增加计算时间,您将实际计算包含在一个包含 5001 次迭代的 m 循环中。但是如果你看看你的实际计算,你会发现,虽然你确实使用你的 dudr 数组来更新它,但你总是从 dudr(t8,t4,t6,ie)=0 开始。所以它以前的值是无关紧要的,因为你删除了它。因此,只有 m 循环的最后一次迭代有任何效果...

但是为了让编译器看到它,它需要看到例程的主体。因此,通过调用子例程,您迫使编译器实际执行某项操作 5001 次,而通过内联它,您让它有机会意识到它毫无意义,而且只有一次迭代就可以了!

好吧,这主要是我的猜测,但我已经看到很多次了,我相信我离真相不远了。事实上,由于您随后对 dudr 不执行任何操作,我什至感到惊讶的是编译器会在您的大循环之外生成任何东西...

更多的 MPI 进程需要更长的时间

好吧,对于这一点,我会做一个更大胆的猜测:我相信不知何故,您不是在多个节点中提交,而是在一个节点上错误地提交了您的代码。在那里,许多 MPI 进程争相访问资源(尤其是内存带宽),并且随着进程数量的增加,花费的时间越来越长......我可能是错的,但目前,这是我最好的解释.

一句至理名言

当您尝试对性能进行基准测试时,请确保始终向编译器隐藏您对计算的实际结果不感兴趣。这可以通过打印一些值,或将其传递给外部例程(可能什么也不做,但编译器不知道)来完成。否则,您可能会惊讶于它的 聪明 以及它可以删除多少 死代码 .