Fortran过程中的数组

本文主要讨论的是Fortran过程(subroutine和function)中作为局部变量的数组。关于数组作为参数传递,看FAQ之 三种数组传递方式。本文参考了Modern Fortran in Practice

此外,本文讨论的作为局部变量的数组,其大小在整个程序可动态变化,不是固定大小的数组。

在Fortran中有三种类型的数组可实现动态大小:

  • 自动数组(automatic arrays)
  • 可分配大小数组(allocatable arrays)
  • 指针(pointers)

例子:

subroutine flexible_arrays(N)
  integer, intent(in) :: N
  integer, dimension(N) :: auto_arr ! 自动数组
  integer, dimension(:), allocatable :: alloc_arr ! 可分配大小数组
  integer, dimension(:), pointer :: ptr ! 指针
end subroutine

接下来分别简单讨论一下各种方式的细节。

自动数组

自动数组比较trivial,其具体大小由传入过程的实参确定,过程结束后自动销毁。唯一的要注意的一点是自动数组储存在堆栈(stack)中1,所以不应该太大(若需要很大的数组,可使用可分配大小数组)。顺便一提,由于自动数组在退出过程时自动销毁,所以自动数组不可能有save属性。

可分配大小数组

可分配大小数组显然需要allocatable属性,在执行过程中通过allocate语句设定大小,分配内存。

可分配大小数组没有save属性时,在过程结束后会自动清除。例:

module m
  implicit none
  contains
  subroutine withoutSave(result, n)
    real, intent(out) :: result
    integer, intent(in) :: n
    ! local variables
    real, dimension(:), allocatable :: working_array
    integer :: istat

    write(*,*) allocated(working_array)
    allocate(working_array(n), stat=istat)

    write(*,"('istat: ',I5)") istat
    write(*,"('size of array: ',I2)") size(working_array)

    call random_number(working_array)
    result = sum(working_array)

  end subroutine

end module

program main
  use m
  implicit none
  real :: res
  integer :: n

  n = 10
  call withoutSave(res, n)
  write(*,*) res

  n = 15
  call withoutSave(res, n)
  write(*,*) res

end program

运行结果为:

 F
istat:     0
size of array: 10
   5.55610991
 F
istat:     0
size of array: 15
   7.75853586

每次进入子程序后,working_array都是未分配大小的状态,且正常分配内存。

若给数组加上save属性后,会变成怎样呢?结果如下:

 F
istat:     0
size of array: 10
   5.81710291
 T
istat:  5014
size of array: 10
   4.41319990

数组在第一次分配大小后,即使退出子程序也依然保留在内存中。第二次进入子程序时,试图给它分配内存时,就会遇到错误。所以一般来说不要给可分配大小数组设定save属性。如果确实有这个需求,则需要在适当的时候用deallocate释放掉内存。

指针

指针的用法和可分配大小数组差不多,这里指给指针分配指向的内存,不是用指针指向其他数组。同样用allocate语句分配内存。

但是这里有一个坑,如果完全像可分配大小数组一样使用,有可能会出现内存泄漏。我们将前面代码中的局部变量数组改成指针:

subroutine pointer_sub(result, n)
  real, intent(out) :: result
  integer, intent(in) :: n
  ! local variables
  real, dimension(:), pointer :: working_array
  integer :: istat

  write(*,*) associated(working_array)
  allocate(working_array(n), stat=istat)

  write(*,"('istat: ',I5)") istat
  write(*,"('size of array: ',I2)") size(working_array)

   call random_number(working_array)
  result = sum(working_array)

end subroutine

运行的结果:

 T
istat:     0
size of array: 10
   4.38992596
 T
istat:     0
size of array: 15
   7.79849720

是不是看起来很正常,分配内存也没有出现错误。但是这个子程序的确会造成内存泄漏,我们用Visual Studio调试进行验证。

我们将调用部分改成这样,每循环一次检查一下内存的使用。

do i = 1, 10
  call pointer_sub(res, 1000)
  write(*,*) res
end do

从图中可以看出,程序每循环一次,内存的使用就多了3.92 kB。我们每次调用子程序时,创建了一个含有1000个real类型元素的数组,其大小为\(1000\times 4/1024=3.9\ \text{kB}\),也证明了内存泄漏来自于这个子程序。

因为每次过程结束时,程序会将指针对内存的指向清除,但不像可分配大小数组一样释放内存,所以下一次进入程序时指针已丢失原来的内存。从而造成了内存泄漏,且使用allocate语句时不会出错。

所以使用指针作为局部变量数组时,在退出过程前要记得回收内存。

其他

理论上来讲,后两种数组在使用过程中需要分配和释放内存,显然存在一些时间开销。Modern Fortran in Practice的作者对三种数组的性能进行了简单的测试。结果表明指针的性能是最差的,而前两种则差异不大。


  1. 根据ARRAYS AND MEMORY IN FORTRAN这篇文章所述,自动数组是否存储在栈中取决于编译器。Intel Visual Fortran是存在栈中,而GNU Fortran编译的程序存在堆(heap)中。所以如果是后者,不必担心数组过大导致栈溢出。

发表评论

电子邮件地址不会被公开。 必填项已用*标注