为什么带有“ use module”语句中的数组的子例程比具有本地大小的数组的同一子例程具有更快的性能?

问题描述

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

我有一些类似以下的旧代码

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: IDreal,dimension(N) :: ID替换real,dimension(:),allocatable :: ID会导致明显的性能损失。为什么是这样?这是预期的行为吗?我想知道它是否与需要为本地数组ID重复分配内存的程序有关,而use语句允许程序跳过内存分配步骤。

在旧式代码ID中位于module variables中,但仅在子例程ID_OG中使用。它在代码的其他任何地方都没有使用-它不是输入或输出。对我来说,将IDmodule variables删除并在子例程中本地定义似乎是一种良好的编程习惯。但也许并非如此。

enter image description here

最小工作示例(MWE): 使用gfortran 8.2.0编译为gfortran -O3 test.f95

MODULE variables
  implicit none

  real,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,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*,loop_max
      CALL ID_header_no_ID(N,DETERM)
    end do
    call cpu_time(t2)
    t_ID_no_ID = t2 - t1
    print*,loop_max
      CALL ID_OG_no_ID(N,DETERM)
    end do
    call cpu_time(t2)
    t_OG_no_ID = t2 - t1
    print*,loop_max
      CALL ID_OG_allocate(N,DETERM)
    end do
    call cpu_time(t2)
    t_allocate = t2 - t1
    print*,DETERM


    deallocate(ID)
    write(1,format_data) N,t_ID_OG,t_allocate

  end do



end program test


subroutine ID_OG(N,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,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(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,N
1       ID(I)=0
  DETERM = sum(ID)

end subroutine ID_OG_no_ID


subroutine ID_OG_allocate(N,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

解决方法

分配数组需要时间。编译器可以自由地在任何需要的地方分配本地数组,但是通常可以通过编译器特定的标志进行调整。将-fstack-arrays用于gfortran强制将本地数组堆叠。

在堆栈上分配只是更改堆栈指针,实际上是免费的。但是,在堆上进行分配会涉及更多事务,并且需要进行一些记账。

在某些情况下,局部变量按顺序排列,在某些情况下,全局(模块)变量按顺序排列。也可以使用本地保存的变量或某些对象的组成部分的变量。 如果不查看相关代码的完整设计,就无法说出哪个更好。

FWIW,对于-fstack-arrays,除了使用allocate()进行显式分配时,我看不出多少区别:

enter image description here

显式allocate将始终使用堆。

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

enter image description here

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


这并不是说应该总是使用-fstack-arrays,我曾经演示过这种区别。该选项很有用,但必须注意避免堆栈溢出错误。 -fmax-stack-var-size可能会有所帮助。

,

正如您的测试所指出的那样,所有不使用module变量的方法的额外开销是由于该语言的系统论来避免用户过多地处理内存。

除非您开始修改编译器标志,否则编译器将决定应在哪里分配内存。您认为分配/释放时间是一个缺点,但是您的分析还显示:

  • 堆栈与堆内存的处理开销很快变得越来越小:对于N>=100,它已经dimension(100)数组是现代计算机上的一个荒谬的小内存块。

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

我认为使代码快速编码的最佳策略是:

  • N在整个运行期间是否保持不变?然后,将其封装到一个类中是一个好主意:
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(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的大小会变大吗?会变大吗?这样,例程内的内存处理就很好了,其开销也可以忽略不计,因为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