本地可分配数组和自动数组之间的区别

Difference between local allocatable and automatic arrays

我对以下摘录中 alloc_arrayautomatic_array 之间的区别感兴趣:

subroutine mysub(n)
integer, intent(in)  :: n
integer              :: automatic_array(n)
integer, allocatable :: alloc_array(:)

allocate(alloc_array(n))
...[code]...

我对分配的 基础知识 非常熟悉(对高级技术了解不多),知道分配允许您在数组的中间更改数组的大小代码(如 this question 中指出的),但我有兴趣考虑您 不需要 更改数组大小的情况;它们可能会被传递给其他子程序进行操作,但是代码和任何子程序中的变量的唯一目的是保存维度数组的数据 n (并且可能会更改数据,但不会更改大小) .

(1)内存使用有什么区别吗?我不是低级程序的专家,但我对它们的重要性和它们的作用知之甚少可以影响更高级别的编程(我正在谈论的那种经验:曾经尝试 运行 Fortran 中的一个大代码我遇到了一个我不理解的错误,系统管理员告诉我 "oh, yeah, you are probably saturating the stack; try adding this line in your running script";欢迎任何让我深入了解在实际编码时如何考虑这些事情而不必稍后修补它们的东西)。人们告诉我,它可能取决于许多其他因素,例如编译器或体系结构,但我从这些答复中解释说,他们并不完全确定这是怎么回事。它是否完全取决于多种因素,或者编码中是否存在 default/intended 行为,然后可能会被可选的编译关键字或系统首选项覆盖?

(2) 子程序会有不同的接口需求吗? 再一次,不是专家,但我之前遇到过,因为我声明子程序变量的方式,我最终不得不将子程序放在一个模块中。我被告知这可能会有所不同,具体取决于我是否使用对可分配变量有特殊意义的东西。我正在考虑这样一种情况,即我对变量所做的一切都可以通过可分配和自动完成,而不是有意使用任何特定的可分配(除了使用前分配,即)。

最后,如果这有用:我问的原因是因为我们是在一个小组中开发的,我们最近注意到不同的人在不同的地方使用这两个声明方式,我们需要确定这是否可以留给个人偏好,或者是否有任何理由可以说明为什么设置明确的标准(以及如何设置该标准)可能是个好主意。我不需要非常详细的答案,我正在尝试确定这是否是我应该做的研究,以谨慎我们如何使用它以及研究应该在哪些方面进行指导。

虽然我很想知道 "interesting tricks" 可以通过分配完成但与大小可变性的需求没有直接关系,但我将把这些留给未来可能的后续问题和这里重点关注严格的功能差异(意思是:我明确告诉编译器如何处理我的代码)。我提到的这两项是我根据以前的经验可以想出的,但是我遗漏了任何其他应该考虑的重要事项,请提出来。

为了清楚起见,我将简要提及术语。两个数组都是局部变量,都是1阶数组

  • alloc_array是可分配数组;
  • automatic_array 是一个明确形状的自动对象。

作为局部变量,它们的范围是过程的范围。自动数组和未保存的可分配数组在过程执行完成时结束(可分配数组被释放);自动对象无法保存,保存的可分配对象在执行完成后不会被释放。

同样,在链接的问题中,在分配语句之后,两个数组的大小都是 n。这仍然是两个截然不同的事情。当然,可分配数组可以更改其分配状态并移动其分配。我将把这两个(大部分)都留在这个答案的范围之外。当然,一个可分配的数组一旦被分配就不必改变这些东西。

内存使用

关于问题的先前修订的部分争议在于内存使用概念的定义有多么不明确。 Fortran,作为一种语言定义,告诉我们两个数组的大小相同,它们将具有相同的存储布局,并且都是连续的。除此之外,您还会经常听到以下术语:具体实现处理器相关.

您在评论中表达了对 ifort 的兴趣。为了不走太远,我将坚持使用那个编译器。其他编译器也有类似的概念,尽管名称和选项不同。

通常,ifort 会将自动对象和临时数组放入堆栈。有一个(默认)编译器选项 -no-heap-arrays 被描述为有效

The compiler puts automatic arrays and temporary arrays in the stack storage area.

使用替代选项 -heap-arrays 可以稍微控制一下:

This option puts automatic arrays and arrays created for temporary computations on the heap instead of the stack.

可以控制选择 heap/stack 的大小阈值(如果在编译时已知):

If the compiler cannot determine the size at compile time, it always puts the automatic array on the heap.

由于 n 不是常量,因此无论指定的大小如何,都希望 automatic_array 使用此选项位于堆上。要在编译时确定数组的大小 n,编译器可能需要进行相当多的代码分析,即使这是可能的。

可能还有更多要说的,但如果我尝试的话,这个答案会太长。但是,需要注意的一件事是,自动本地对象和 (post-Fortran 90) 可分配本地对象预计不会泄漏内存。

接口需求

子程序的接口要求没有什么特别之处mysub:局部变量对此没有影响。任何程序单元调用都会对隐式接口感到满意。你问的是两个局部数组怎么用。

这很大程度上取决于两个数组的用途。

如果第二个过程的虚拟参数具有可分配属性,则只有此处的可分配数组才能传递给该过程。它还需要有一个显式接口。无论过程是否更改分配都是如此。

当然,两个数组都可以作为参数传递给没有可分配属性的虚拟参数,这样我们就没有不同的接口要求了。

无论如何,当分配状态等没有变化时,为什么要将参数传递给可分配的虚拟对象?有充分的理由:

  • 程序中可能有一个代码路径确实有分配更改(例如,由开关控制);
  • 可分配的虚拟参数也可以传递边界;
  • 等等,

如果子程序有规范,第二个更明显

subroutine mysub(n)
integer, intent(in)  :: n
integer              :: automatic_array(2:n+1)
integer, allocatable :: alloc_array(:)

allocate(alloc_array(2:n+1))

最后,自动对象对其大小有相当严格的条件。 n 这里显然是允许的,但在分配是唯一可行的方法之前,事情不必复杂得多。取决于有多少人想玩 block 结构。

同时引用 IanH 的评论:如果我们有一个非常大的 n 自动对象很可能会导致崩溃和烧毁。对于可分配的,可以使用 stat= 选项与编译器 运行-time.

达成某种友好协议

因为 gfortran 或 ifort + Linux(x86_64) 是用于 HPC 的最流行的组合之一,我针对这些在本地 allocatable 与自动数组之间做了一些性能比较组合。 CPU使用的是Xeon E5-2650 v2@2.60GHz,编译器是gfortran4.8.2和ifort14.0。测试程序如下

In test.f90:

!------------------------------------------------------------------------           
subroutine use_automatic( n )
    integer :: n

    integer :: a( n )   !! local automatic array (with unknown size at compile-time)
    integer :: i

    do i = 1, n
        a( i ) = i
    enddo

    call sub( a )
end

!------------------------------------------------------------------------           
subroutine use_alloc( n )
    integer :: n

    integer, allocatable :: a( : )  !! local allocatable array                      
    integer :: i

    allocate( a( n ) )

    do i = 1, n
        a( i ) = i
    enddo

    call sub( a )

    deallocate( a )  !! not necessary for modern Fortran but for clarity                  
end

!------------------------------------------------------------------------           
program main
    implicit none
    integer :: i, nsizemax, nsize, nloop, foo
    common /dummy/ foo

    nloop = 10**7
    nsizemax = 10

    do i = 1, nloop
        nsize = mod( i, nsizemax ) + 1

        call use_automatic( nsize )
        ! call use_alloc( nsize )                                                   
    enddo

    print *, "foo = ", foo   !! to check if sub() is really called
end

In sub.f90:

!------------------------------------------------------------------------
subroutine sub( a )
    integer a( * )
    integer foo
    common /dummy/ foo

    foo = a( 1 )
ends

在上面的程序中,我试图通过将 sub() 放在不同的文件中并使接口隐式来避免消除 a(:) 本身(即无操作)的编译器优化。首先,我使用 gfortran as

编译程序
gfortran -O3 test.f90 sub.f90

并在保持 nloop = 10^7 的同时测试了不同的 nsizemax 值。结果如下table(时间单位是秒,通过time命令测量了几次)

nsizemax    use_automatic()    use_alloc()
10          0.30               0.31               # average result
50          0.48               0.47
500         1.0                0.90
5000        4.3                4.2
100000      75.6               75.7

因此,当使用 -O3 时,两次调用的总体时间似乎几乎相同(但请参阅编辑了解不同的选项)。接下来,我用ifort编译为

[O3]  ifort -O3 test.f90 sub.f90
or
[O3h] ifort -O3 -heap-arrays test.f90 sub.f90

在前一种情况下,自动数组存储在堆栈中,而当附加 -heap-arrays 时,数组存储在堆中。得到的结果是

         use_automatic()    use_alloc()
         [O3]    [O3h]      [O3]    [O3h]
10       0.064   0.39       0.48    0.48
50       0.094   0.56       0.65    0.66
500      0.45    1.03       1.12    1.12
5000     3.8     4.4        4.4     4.4
100000   74.5    75.3       76.5    75.5

所以对于 ifort,当主要使用相对较小的阵列时,使用自动阵列似乎是有益的。另一方面,gfortran -O3 显示没有区别,因为两个数组的处理方式相同(有关更多详细信息,请参阅编辑)。

补充比较:

以下是针对 Linux(与 f90 -O3 一起使用)的 Oracle Fortran 编译器 12.4 的结果。总体趋势似乎相似;自动数组对于小 n 更快,表明内部使用堆栈。

nsizemax    use_automatic()    use_alloc()
10          0.16               0.45
50          0.17               0.62
500         0.37               0.97
5000        2.04               2.67
100000      65.6               65.7

编辑

感谢 Vladimir 的评论,事实证明 gfortran -O3 将自动数组(编译时大小未知)放在堆上。这解释了为什么 use_automatic() 和 use_alloc() 在上面没有任何区别。所以我在下面的不同选项之间进行了另一个比较:

[O3]  gfortran -O3
[O5]  gfortran -O5
[O3s] gfortran -O3 -fstack-arrays
[Of]  gfortran -Ofast                   # this includes -fstack-arrays

这里,-fstack-arrays表示编译器将所有大小未知的局部数组放入栈中。请注意,默认情况下使用 -Ofast 启用此标志。得到的结果是

nsizemax    use_automatic()               use_alloc()
            [Of]   [O3s]  [O5]  [O3]     [Of]  [O3s]  [O5]  [O3]
10          0.087  0.087  0.29  0.29     0.29  0.29   0.29  0.29
50          0.15   0.15   0.43  0.43     0.45  0.44   0.44  0.45
500         0.57   0.56   0.84  0.84     0.92  0.92   0.92  0.92
5000        3.9    3.9    4.1   4.1      4.2   4.2    4.2   4.2
100000      75.1   75.0   75.6  75.6     75.6  75.3   75.7  76.0

其中显示了十次测量的平均值。这个table说明如果包含-fstack-arrays,小n的执行时间会变短。这个趋势与上面ifort得到的结果是一致的。

不过需要说明的是,上面的对比大概对应的是"best-case"场景,比较突出了两者的区别,所以实际中的时间差可以小很多。例如,我通过使用其他一些程序(涉及小型和大型阵列)比较了上述选项的时间,并且结果受堆栈选项的影响不大。当然,结果还应该取决于机器体系结构和编译器。所以你的里程可能会有所不同。