为什么带有来自 "use module" 语句的数组的子例程比具有本地大小数组的相同子例程提供更快的性能?

Why does a subroutine with an array from a "use module" statement give faster performance than the same subroutine a locally sized array?

与此相关 ,但我相信此示例可以更清楚地识别问题。

我有一些遗留代码,如下所示:

subroutine ID_OG(N, DETERM)
  use variables, only: ID
  implicit real (A-H,O-Z)
  implicit integer(I-N)

  DETERM = 1.0
  DO 1 I=1,N
1       ID(I)=0
  DETERM = sum(ID)
end subroutine ID_OG

use variables, only: ID 替换为 real, dimension(N) :: IDreal, dimension(:), allocatable :: ID 会导致明显的性能损失。为什么是这样?这是预期的行为吗?我在想是不是和程序需要为本地数组ID重复分配内存有关,而use语句允许程序跳过内存分配步骤

在遗留代码中 IDmodule variables 中,但仅在子例程 ID_OG 中使用。它没有在代码中的其他任何地方使用——它既不是输入也不是输出。对我来说,将 IDmodule variables 中删除并在子例程中本地定义似乎是很好的编程习惯。但也许并非如此。

最小工作示例(MWE): 使用 gfortran 8.2.0

编译为 gfortran -O3 test.f95
MODULE variables
  implicit none

  real, dimension(:),   allocatable :: ID

END MODULE variables


program test
  use variables

  implicit none

  integer             :: N
  integer             :: loop_max = 1e6
  integer             :: ii                    ! loop index
  real                :: DETERM

  real :: t1, t2
  real :: t_ID_OG, t_ID_header, t_ID_no_ID, t_OG_no_ID, t_allocate

  character(*), parameter :: format_header = '((A5, 1X), 20(A12,1X))'
  character(*), parameter :: format_data = '((I5, 1X), 20(ES12.5, 1X))'

  open(1, file = 'TimingSubroutines_ID.txt', status = 'unknown')
  write(1,format_header) 'N', 't_Legacy', 't_header', 't_head_No_ID', 't_Leg_no_ID', &
                            & 't_allocate'

  do N = 1, 100

    allocate(ID(N))


    call CPU_time(t1)
    do ii = 1, loop_max
      CALL ID_OG(N, DETERM)
    end do
    call CPU_time(t2)
    t_ID_OG = t2 - t1
    print*, N, DETERM


    call CPU_time(t1)
    do ii = 1, loop_max
      CALL ID_header(N, DETERM)
    end do
    call CPU_time(t2)
    t_ID_header = t2 - t1
    print*, N, DETERM


    call CPU_time(t1)
    do ii = 1, loop_max
      CALL ID_header_no_ID(N, DETERM)
    end do
    call CPU_time(t2)
    t_ID_no_ID = t2 - t1
    print*, N, DETERM


    call CPU_time(t1)
    do ii = 1, loop_max
      CALL ID_OG_no_ID(N, DETERM)
    end do
    call CPU_time(t2)
    t_OG_no_ID = t2 - t1
    print*, N, DETERM


    call CPU_time(t1)
    do ii = 1, loop_max
      CALL ID_OG_allocate(N, DETERM)
    end do
    call CPU_time(t2)
    t_allocate = t2 - t1
    print*, N, DETERM


    deallocate(ID)
    write(1,format_data) N, t_ID_OG, t_ID_header, t_ID_no_ID, t_OG_no_ID, t_allocate

  end do



end program test


subroutine ID_OG(N, DETERM)
  use variables, only: ID
  implicit real (A-H,O-Z)
  implicit integer(I-N)


  DETERM = 1.0
  DO 1 I=1,N
1       ID(I)=0
  DETERM = sum(ID)

end subroutine ID_OG



subroutine ID_header(N, DETERM)
  use variables, only: ID
  implicit none

  integer, intent(in)  :: N
  real,    intent(out) :: DETERM
  integer              :: I


  DETERM = 1.0
  DO 1 I=1,N
1       ID(I)=0
  DETERM = sum(ID)

end subroutine ID_header



subroutine ID_header_no_ID(N, DETERM)
  implicit none

  integer, intent(in)  :: N
  real,    intent(out) :: DETERM
  integer              :: I
  real, dimension(N)   :: ID


  DETERM = 1.0
  DO 1 I=1,N
1       ID(I)=0
  DETERM = sum(ID)

end subroutine ID_header_no_ID


subroutine ID_OG_no_ID(N, DETERM)
  implicit real (A-H,O-Z)
  implicit integer(I-N)
  real, dimension(N)   :: ID


  DETERM = 1.0
  DO 1 I=1,N
1       ID(I)=0
  DETERM = sum(ID)

end subroutine ID_OG_no_ID


subroutine ID_OG_allocate(N, DETERM)
  implicit real (A-H,O-Z)
  implicit integer(I-N)
  real, dimension(:), allocatable :: ID

  allocate(ID(N))


  DETERM = 1.0
  DO 1 I=1,N
1       ID(I)=0
  DETERM = sum(ID)

end subroutine ID_OG_allocate

分配数组需要时间。编译器可以自由分配它想要的本地数组 where-ever,但通常可以通过 compiler-specific 标志进行调整。对 gfortran 使用 -fstack-arrays 强制本地数组堆叠。

在栈上分配只是改变栈指针,几乎是免费的。然而,在堆上分配更复杂,需要一些簿记。

有局部变量有序的情况,也有全局(模块)变量有序的情况。还可以使用本地保存的变量或作为某些对象的组件的变量。 如果没有看到相关代码的完整设计,就不能说哪个更好。

FWIW,与 -fstack-arrays 我看不出有什么区别,除非使用 allocate():

显式分配

显式 allocate 将始终使用堆。

没有-fstack-arrays我确实看到了一些:

图表非常嘈杂,因为我的笔记本同时 运行 许多进程。


这并不是说一定要用-fstack-arrays,我是用来演示区别的。该选项很有用,但必须注意避免堆栈溢出错误。 -fmax-stack-var-size 可能会有所帮助。

正如您的测试所指出的,所有不使用 module 变量的方法的额外开销是由于该语言的哲学不会过多地打扰用户处理内存。

编译器将决定应该在何处分配内存,除非您开始修改编译器标志。您将 allocation/freeing 时间视为缺点,但您的分析还显示:

  • 堆栈与堆内存处理开销迅速变得越来越小:对于 N>=100,它已经 <50%。 dimension(100) 数组在现代计算机上是一个可笑的小内存块。

  • 在模块中声明一个变量只是为了加速存储是一种 Fortran 90 使其成为全局变量的方式,因此,它是一种已弃用的编码风格。

我认为使代码 well-coded 快速的最佳策略是:

  • N 会在整个运行时保持不变吗?然后,将它封装成一个 class:
  • 是个好主意
module myCalculation
    implicit none

    type, public: fancyMethod
        integer :: N = 0
        real, allocatable :: ID(:)

    contains
       
        procedure :: init 
        procedure :: compute
        procedure :: is_init
             
    end type fancyMethod

contains

    elemental subroutine init(self,n)
        class(fancyMethod), intent(inout) :: self
        integer, intent(in) :: n
        real, allocatable :: tmp(:)

        self%N = n
        allocate(tmp(N)); tmp(:) = 0
        call move_alloc(from=tmp,to=self%ID)
    end subroutine init

    elemental logical function is_init(self) 
        class(fancyMethod), intent(in) :: self
        is_init = allocated(self%ID) .and. size(self%ID)>0
    end function is_init

    real function compute(self,n,...) result(DETERM)
        class(fancyMethod), intent(inout) :: self
        integer, intent(in) :: n
        ....

        if (.not.is_init(self)) call init(self,N)
 
        DETERM = sum(self%ID(1:N)) 
    end function compute

end module myCalculation
  • N 会保持不变吗?为什么不直接使用 PARAMETER 来定义它的最大尺寸呢?如果它是一个参数,编译器可能总是将自动数组放在堆栈上:
real function computeWithMaxSize(N) result(DETERM)
    integer, intent(in) :: N
    integer, parameter :: MAX_SIZE = 1024
    real :: ID(MAX_SIZE)

    [...]

    if (N>MAX_SIZE) stop ' N is too large! '

    DETERM = sum(ID(1:N))
end function computeWithMaxSize
  • N 会变得 variable-sized 大吗?然后,in-routine 内存处理很好,它的开销可能可以忽略不计,因为 CPU 时间将由计算支配;如果您不确定大小是否会大到导致任何堆栈问题,请使用 allocatable 版本:
real function computeWithAllocatable(N) result(DETERM)
    integer, intent(in) :: N
    real, allocatable :: ID(:)

    allocate(ID(N))
 
    [...]

    DETERM = sum(ID(1:N))
end function computeWithAllocatable