【问题标题】:Plain vs. allocatable/pointer arrays, Fortran advice?普通数组与可分配/指针数组,Fortran 建议?
【发布时间】:2019-07-17 15:43:27
【问题描述】:

我为矩阵乘法编写了以下人为的示例,只是为了检查声明不同类型的数组如何影响性能。令我惊讶的是,我发现声明时已知大小的普通数组的性能不如可分配/指针数组。我认为allocatable 仅适用于不适合堆栈的大型数组。这是使用 gfortran 和英特尔 Fortran 编译器的代码和时序。 Windows 10 平台分别与编译器标志 -Ofast-fast 一起使用。

program matrix_multiply
   implicit none
   integer, parameter :: n = 1500
   real(8) :: a(n,n), b(n,n), c(n,n), aT(n,n)                 ! plain arrays 
   integer :: i, j, k, ts, te, count_rate, count_max
   real(8) :: tmp

   ! real(8), allocatable :: A(:,:), B(:,:), C(:,:), aT(:,:)  ! allocatable arrays
   ! allocate ( a(n,n), b(n,n), c(n,n), aT(n,n) )

   do i = 1,n
      do j = 1,n
         a(i,j) = 1.d0/n/n * (i-j) * (i+j)
         b(i,j) = 1.d0/n/n * (i-j) * (i+j)
      end do 
   end do 

   ! transpose for cache-friendliness   
   do i = 1,n
      do j = 1,n
         aT(j,i) = a(i,j)
      end do 
   end do 

   call system_clock(ts, count_rate, count_max)
   do i = 1,n
      do j = 1,n
         tmp = 0 
         do k = 1,n
            tmp = tmp + aT(k,i) * b(k,j)
         end do
         c(i,j) = tmp
      end do
   end do
   call system_clock(te)
   print '(4G0)', "Elapsed time: ", real(te-ts)/count_rate,', c_(n/2+1) = ', c(n/2+1,n/2+1)    
end program matrix_multiply

时间安排如下:

! Intel Fortran
! -------------
Elapsed time: 1.546000, c_(n/2+1) = -143.8334 ! Plain Arrays
Elapsed time: 1.417000, c_(n/2+1) = -143.8334 ! Allocatable Arrays  

! gfortran:
! -------------
Elapsed time: 1.827999, c_(n/2+1) = -143.8334 ! Plain Arrays 
Elapsed time: 1.702999, c_(n/2+1) = -143.8334 ! Allocatable Arrays

我的问题是为什么会这样?可分配数组是否为编译器提供了更多保证以更好地优化?在 Fortran 中处理固定大小的数组时,一般来说最好的建议是什么?

冒着延长问题的风险,下面是另一个英特尔 Fortran 编译器表现出相同行为的示例:

program testArrays
  implicit none
  integer, parameter :: m = 1223, n = 2015 
  real(8), parameter :: pi = acos(-1.d0)
  real(8) :: a(m,n)
  real(8), allocatable :: b(:,:)
  real(8), pointer :: c(:,:)
  integer :: i, sz = min(m, n), t0, t1, count_rate, count_max

  allocate( b(m,n), c(m,n) )
  call random_seed()
  call random_number(a)
  call random_number(b)
  call random_number(c)

  call system_clock(t0, count_rate, count_max)
    do i=1,1000
      call doit(a,sz)
    end do 
  call system_clock(t1)
  print '(4g0)', 'Time plain: ', real(t1-t0)/count_rate, ',  sum 3x3 = ', sum( a(1:3,1:3) )

  call system_clock(t0)
    do i=1,1000
      call doit(b,sz)
    end do 
  call system_clock(t1)
  print '(4g0)', 'Time alloc: ', real(t1-t0)/count_rate, ',  sum 3x3 = ', sum( b(1:3,1:3) )

  call system_clock(t0)
    do i=1,1000 
      call doitp(c,sz)
    end do 
  call system_clock(t1)
  print '(4g0)', 'Time p.ptr: ', real(t1-t0)/count_rate, ',  sum 3x3 = ', sum( c(1:3,1:3) )

  contains 
  subroutine doit(a,sz)
    real(8) :: a(:,:)
    integer :: sz 
    a(1:sz,1:sz) = sin(2*pi*a(1:sz,1:sz))/(a(1:sz,1:sz)+1)
  end

  subroutine doitp(a,sz)
    real(8), pointer :: a(:,:)
    integer :: sz
    a(1:sz,1:sz) = sin(2*pi*a(1:sz,1:sz))/(a(1:sz,1:sz)+1)
  end    
end program testArrays 

ifort计时:

Time plain: 2.857000,  sum 3x3 = -.9913536
Time alloc: 2.750000,  sum 3x3 = .4471794
Time p.ptr: 2.786000,  sum 3x3 = 2.036269  

gfortran 的时间要长得多,但符合我的预期:

Time plain: 51.5600014,  sum 3x3 = 6.2749456118192093
Time alloc: 54.0300007,  sum 3x3 = 6.4144775892064283
Time p.ptr: 54.1900034,  sum 3x3 = -2.1546109819149963

【问题讨论】:

    标签: arrays performance fortran


    【解决方案1】:

    要了解编译器是否认为存在差异,请查看为过程生成的程序集。根据此处的快速浏览,就处理器必须执行的工作而言,第一个示例的两种情况的定时部分的组装似乎或多或少是等效的。这正如预期的那样,因为呈现给定时部分的数组或多或少是等价的——它们很大、连续、不重叠,并且元素值仅在运行时才知道。

    (在编译器之外,由于数据在运行时在各种缓存中的呈现方式可能会有所不同,但这两种情况也应该相似。)

    显式形状和可分配数组之间的主要区别在于为后者分配和取消分配存储所需的时间。在您的第一个示例中最多只有四个分配(因此相对于后续计算不太可能繁重),并且您不会对程序的那部分进行计时。将分配/隐式释放对粘贴在一个循环中,然后看看你是怎么做的。

    具有指针或目标属性的数组可能会产生别名,因此编译器可能需要做额外的工作来解决数组重叠存储的可能性。然而,第二个示例中的表达式的性质(仅引用了一个数组)使得编译器可能知道在这种特殊情况下不需要额外的工作,并且操作再次变得等价。

    回应“我认为只有不适合堆栈的大型数组才需要allocatable” - allocatable 是需要(即你没有真正的选择) 当您无法确定在负责整个事物存在的程序的规范部分中分配的事物的大小或其他特征时。即使对于直到运行时才知道的事情,如果您仍然可以确定相关过程的规范部分中的特征,那么 自动变量 是一种选择。 (尽管您的示例中没有自动变量 - 在不可分配、非指针的情况下,数组的所有特征在编译时都是已知的。)在 Fortran 处理器实现级别,编译器和编译选项之间有所不同,自动变量可能需要比可用的更多的堆栈空间,这可能会导致可分配的问题可能会缓解(或者您可以更改编译器选项)。

    【讨论】:

      【解决方案2】:

      这不是你为什么得到你所观察到的答案的答案,而是一份与你的观察不符的报告。你的代码,

      program matrix_multiply
         implicit none
         integer, parameter :: n = 1500
        !real(8) :: a(n,n), b(n,n), c(n,n), aT(n,n)                 ! plain arrays 
         integer :: i, j, k, ts, te, count_rate, count_max
         real(8) :: tmp
      
         real(8), allocatable :: A(:,:), B(:,:), C(:,:), aT(:,:)  ! allocatable arrays
         allocate ( a(n,n), b(n,n), c(n,n), aT(n,n) )
      
         do i = 1,n
            do j = 1,n
               a(i,j) = 1.d0/n/n * (i-j) * (i+j)
               b(i,j) = 1.d0/n/n * (i-j) * (i+j)
            end do 
         end do 
      
         ! transpose for cache-friendliness   
         do i = 1,n
            do j = 1,n
               aT(j,i) = a(i,j)
            end do 
         end do 
      
         call system_clock(ts, count_rate, count_max)
         do i = 1,n
            do j = 1,n
               tmp = 0 
               do k = 1,n
                  tmp = tmp + aT(k,i) * b(k,j)
               end do
               c(i,j) = tmp
            end do
         end do
         call system_clock(te)
         print '(4G0)', "Elapsed time: ", real(te-ts)/count_rate,', c_(n/2+1) = ', c(n/2+1,n/2+1)    
      end program matrix_multiply
      

      在 Windows 上使用 Intel Fortran 编译器 18.0.2 编译并打开优化标志,

      ifort /standard-semantics /F0x1000000000 /O3 /Qip /Qipo /Qunroll /Qunroll-aggressive /inline:all /Ob2 main.f90 -o run.exe
      

      实际上,与您观察到的相反:

      Elapsed time: 1.580000, c_(n/2+1) = -143.8334   ! plain arrays
      Elapsed time: 1.560000, c_(n/2+1) = -143.8334   ! plain arrays
      Elapsed time: 1.555000, c_(n/2+1) = -143.8334   ! plain arrays
      Elapsed time: 1.588000, c_(n/2+1) = -143.8334   ! plain arrays
      Elapsed time: 1.551000, c_(n/2+1) = -143.8334   ! plain arrays
      Elapsed time: 1.566000, c_(n/2+1) = -143.8334   ! plain arrays
      Elapsed time: 1.555000, c_(n/2+1) = -143.8334   ! plain arrays
      
      Elapsed time: 1.634000, c_(n/2+1) = -143.8334   ! allocatable arrays
      Elapsed time: 1.634000, c_(n/2+1) = -143.8334   ! allocatable arrays
      Elapsed time: 1.602000, c_(n/2+1) = -143.8334   ! allocatable arrays
      Elapsed time: 1.623000, c_(n/2+1) = -143.8334   ! allocatable arrays
      Elapsed time: 1.597000, c_(n/2+1) = -143.8334   ! allocatable arrays
      Elapsed time: 1.607000, c_(n/2+1) = -143.8334   ! allocatable arrays
      Elapsed time: 1.617000, c_(n/2+1) = -143.8334   ! allocatable arrays
      Elapsed time: 1.606000, c_(n/2+1) = -143.8334   ! allocatable arrays
      Elapsed time: 1.626000, c_(n/2+1) = -143.8334   ! allocatable arrays
      Elapsed time: 1.614000, c_(n/2+1) = -143.8334   ! allocatable arrays
      

      如您所见,平均而言,可分配数组实际上稍微慢一些,这是我所期望的,这也与您的观察相矛盾。我可以看到的唯一差异来源是使用的优化标志,尽管我不确定这会如何产生影响。也许您想在多种不同的无优化模式和不同优化级别的模式下运行测试,看看是否在所有模式下都能获得一致的性能差异。要获取有关所用优化标志含义的更多信息,请参阅Intel's reference page

      另外,不要将real(8) 用于变量声明。它是一种非标准语法,不可移植,因此可能存在问题。根据 Fortran 标准,更一致的方法是使用 iso_fortran_env 内在模块,例如:

      !...
      use, intrinsic :: iso_fortran_env, only: real64, int32
      integer(int32), parameter :: n=100
      real(real64) :: a(n)
      !...
      

      这个内在模块有以下几种,

         int8 ! 8-bit integer
        int16 ! 16-bit integer
        int32 ! 32-bit integer
        int64 ! 64-bit integer
       real32 ! 32-bit real
       real64 ! 64-bit real
      real128 ! 128-bit real 
      

      因此,例如,如果你想声明一个包含 64 位类型组件的复杂变量,你可以这样写:

      program complex
          use, intrinsic :: iso_fortran_env, only: RK => real64, output_unit
          ! the intrinsic attribute above is not essential, but recommended, so this would be also valid:
          ! use iso_fortran_env, only: RK => real64, output_unit
          complex(RK) :: z = (1._RK, 2._RK)
          write(output_unit,"(*(g0,:,' '))") "Hello World! This is a complex variable:", z
      end program complex
      

      给出:

      $gfortran -std=f2008 *.f95 -o main
      $main
      Hello World! This is a complex variable: 1.0000000000000000 2.0000000000000000
      

      请注意,这需要符合 Fortran 2008 的编译器。 iso_fortran_env 中还有其他功能和实体,例如 output_unit,它是预连接标准输出单元的单元号(与 printwrite 使用的单元号相同,单元说明符为 * ),以及compiler_version()compiler_options() 等其他几个。

      【讨论】:

      • 感谢@King 的回答。我很高兴你的观察与我的相矛盾,因为我长期以来一直认为这是正确的。但是,我仍然在不同的优化级别看到相同的行为,使用你的标志,例如,我得到以下时间:4.918000 (plain)4.785000 (allocated)4.771000 (pointer),这很奇怪。还有其他指针吗?是的,我在实际代码中不使用real(8),我总是使用selected_real_kind(15)
      • 在其中一个在线 Fortran 编译器上尝试您的测试,看看您是否在您自己的系统上使用相同的标志在云上获得相同的计时结果。如果时序不一致,则可能意味着观察到的行为特定于您的编译器或处理器。这是一个在线编译器:tutorialspoint.com/compile_fortran_online.php。您可以从“项目”选项卡设置编译器标志和标准。
      • @ClintonWinant 与其他经典方法相同。请参阅上面的修改后的答案以及示例复杂声明。
      • @ClintonWinant g0.d 编辑描述符是 Fortran 2008 的新增功能,它会自动查找并为输出类型分配适当的描述符。对于实数和复数,输出精度将设置为d(如果存在),其余类型(整数、字符、...)将被忽略。 *() 是一个重复模式,可以满足许多需要。 ,:,' ' 指示以空格分隔的输出,,:, 告诉编译器在最后一个输出之后删除分隔符。例如,可以生成"(*(g0,:,','))"格式的CSV文件,其中分隔符为,
      • @King 我感激不尽。像你这样的人使像我这样的人的工作成为可能!
      猜你喜欢
      • 1970-01-01
      • 2014-10-09
      • 1970-01-01
      • 1970-01-01
      • 2012-03-11
      • 1970-01-01
      • 2021-11-17
      • 2017-01-30
      • 2010-12-28
      相关资源
      最近更新 更多