在 Fortran 中将数组传递给子例程:假定形状与显式形状

Passing arrays to subroutines in Fortran: Assumed shape vs explicit shape

提问人:Alessandro 提问时间:1/9/2023 最后编辑:Alessandro 更新时间:1/12/2023 访问量:417

问:

当将数组传递给过程时,在 (1) 速度和 (2) 内存、假定形状或显式形状方面,什么最好?前段时间在这个论坛上也问过一个类似的问题,但不是用这些术语:在 Fortran 过程中将大小作为参数传递 VS 假设形状

我提供了一个简单的程序来说明我的意思

! Compile with
! ifort /O3 main.f90 -o run_win.exe

module mymod
    USE iso_Fortran_env, ONLY: dp => real64
    implicit none
    private
    public :: dp, sub_trace, sub_trace_es
    
    contains
    
    subroutine sub_trace(mat,trace)
    ! Assumed shape
        implicit none
        real(dp), intent(in) :: mat(:,:)
        real(dp), intent(out) :: trace
        real(dp) :: V(size(mat,dim=1))
        integer :: i,N
        
        if (size(mat,dim=1) /= size(mat,dim=2)) then
            error stop "Input matrix is not square!"
        endif
        
        N = size(mat,dim=1)
        do i=1,N
            V(i) = mat(i,i)
        enddo
        trace = sum(V)
    
    end subroutine sub_trace
    
    subroutine sub_trace_es(n,mat,trace)
    ! Passing array explicit shape
        implicit none
        integer, intent(in) :: n
        real(dp), intent(in) :: mat(n,n)
        real(dp), intent(out) :: trace
        real(dp) :: V(n)
        integer :: i
        
        do i=1,n
            V(i) = mat(i,i)
        enddo
        trace = sum(V)
    
    end subroutine sub_trace_es
    
end module mymod    
    
program main
    use mymod, only: dp, sub_trace,sub_trace_es
    implicit none
    integer, parameter :: nn=2
    real(dp) :: mat(nn,nn)
    real(dp), allocatable :: mat4(:,:)
    real(dp) :: trace1,trace2,trace3,trace4
    
    write(*,*) "Passing arrays to subroutines:"
    write(*,*) "Assumed-shape vs explicit shape"
    
    mat(1,:) = [2_dp,3_dp]
    mat(2,:) = [4_dp,5_dp]
    
    call sub_trace(mat,trace1)
    
    write(*,*) "trace1 = ", trace1
    
    call sub_trace_es(nn,mat,trace2)
    
    write(*,*) "trace2 = ", trace2
    
    ! First example offered by francescalus:
    call sub_trace_es(2,real([1,2,3,4,5,6,7,8,9],dp), trace3)
    
    write(*,*) "trace3 = ", trace3
    
    ! Second example
    mat4 = reshape(real([1,2,3,4,5,6,7,8,9],dp),[3,3])
    call sub_trace(mat4, trace4)
    
    write(*,*) "trace4 = ", trace4
        
    pause

end program
数组 fortran 按引用传递 子例程

评论

2赞 francescalus 1/9/2023
假设形状和显式形状数组是完全不同的东西(正如虚拟参数一样),因此将“速度”和“内存”降低到甚至可能还不够。你能解释一下你所理解和关心的内容吗?
0赞 Alessandro 1/9/2023
在现代 fortran 中,有两种主要方式将数组传递给子程序和函数:假定形状虚拟数组(我在sub_trace中进行了说明)和显式形状虚拟数组(我在sub_trace_es中进行了说明)。问题是 (1) 如果想要最大限度地提高代码的执行速度,哪种方法最好?(2) 如果想要最小化代码的内存占用,即避免来回复制数组,哪种方法最好?
2赞 Vladimir F Героям слава 1/9/2023
有很多微妙之处。在一种情况下速度较快,在另一种情况下速度较慢。即使使用不小心,您也可以拥有假定的形状,也可以拥有临时副本。一般来说,可能不可能说什么更快。复制速度很慢,但在连续副本上,函数内部的迭代速度可能更快。contiguous
2赞 francescalus 1/9/2023
在现代 Fortran 中,传递数组的主要方式有四种:假设形状、显式形状、延迟形状;假设大小(是的,即使在现代 Fortran 中也是如此)。但它们各自有不同的含义。
1赞 Vladimir F Героям слава 1/9/2023
请注意,这是不合法的 Fortran。它被删除了,而不仅仅是标记为过时,已经在 Fortran 90 或 95 中。对于一般情况,它从来没有真正明确定义过。如果您的程序应该等待 Enter,只需使用 .也不是便携式的,像这样的幻数是丑陋的,不属于这些上下文。pauseprint *,real(8)8

答:

4赞 Vladimir F Героям слава 1/9/2023 #1

使用假定的形状,您可以实现传递非连续数组或无需临时副本的数组。接收子程序知道各个部分在内存中的位置,并且由于数组描述符中的兴奋剂向量,可以在它们之间跳转。这意味着您可以避免临时副本,但通过数组的迭代更复杂,并且可能更慢。

如果假定的形状数组具有该属性,则编译器可以生成更简单、更快的代码,但如果实际参数不连续,则必须创建临时副本。contiguous

对于显式大小的数组,虚拟参数始终是连续的。但是,如果实际参数不连续,则需要临时副本。


使用假定的形状数组,编译器可以在编译过程中更好地检查参数,因为显式接口始终可用。如果显式接口可用,即使对于显式大小的数组,也可以进行一些检查,有时甚至在显式接口不可用时,但可能性更加有限。

其中一个原因是,由于存储关联规则,可以传递一个具有不同秩且总大小(元素数)大于(或等于)大小的数组,该数组以显式大小数组的虚拟参数的形状声明。

对于假定的形状,该形状会与数组描述符一起自动传递。因此,传递小于或大于声明的尺寸对他们来说是一个不存在的概念,它们只是工作方式不同。

在许多方面,这些类型的数组虚拟参数差异太大,并且您可以使用它们执行的操作不同。由于速度,它不仅仅是一个或另一个。它们的使用方式大不相同。对于显式大小数组,您必须以某种方式提供大小。

这种差异可以通过弗朗西斯卡鲁斯提供的例子来说明:

call sub_trace_es(2, real([1,2,3,4,5,6,7,8,9], dp), trace)

这需要跟踪 2x2 数组。传递的参数是一个包含 9 个元素的一维数组。但是,只会考虑前四个。子例程将查看的矩阵是

1 3
2 4

(column-major order),跟踪将为 5。

call subtrace( reshape(real([1,2,3,4,5,6,7,8,9], dp), [3,3]), trace)

相同的 9 元素数字序列被重塑为 3x3 数组。因此,子例程将查看的矩阵是

1   4   7
2   5   8
3   6   9

跟踪将为 15。


我个人在超级计算机生产代码的几个地方使用带有显式属性的假定形状,其中传递了大型数组。但是,请注意启用有关临时副本的警告,很容易在一个位置忘记这一点,并且您会通过不必要的临时副本破坏所有内容。contiguous

在我的代码的大多数部分,这些部分对性能不是那么重要,我只是使用假定的形状,而没有其他属性。

评论

0赞 francescalus 1/9/2023
也许答案的要点是包括实际和虚拟论证中元素的等级和数量?
1赞 francescalus 1/9/2023
我当然不会过分(在相关问题的上下文中),但如果我回答,我会强调这与远远超出其他方面非常不同。call sub_trace_es(2, [1.,2.,3.,4.,5.,6.,7.,8.,9.], trace)call subtrace(reshape([1.,2.,3.,4.,5.,6. 7.,8.,9.],[3,3]),trace)
1赞 Vladimir F Героям слава 1/11/2023
@Alessandro 不要混淆类型、种类和等级。类型不匹配意味着错误消息中的“实际上是指基本类型或种类中的不匹配”。因为你的子例程需要 ,所以你必须提供,而不仅仅是默认的 real。在此类示例中,请始终将此类示例作为可能的简化示例。real(dp)real(dp)
1赞 Vladimir F Героям слава 1/11/2023
dp real 可以通过使用或使用转换函数来实现。在您的原始版本中,它是一样的,只是使用 8 而不是 dp。1._dpreal(x, dp)real(8)
1赞 Vladimir F Героям слава 1/12/2023
@Alessandro 这方面的许多用例都非常有意义。您可以在此站点和网络上搜索“序列关联”。矩阵和向量是线性代数对象,在某些线性向量空间的基础上为线性算子定义。Fortran 知道数组(matmul() 除外)。矩阵和向量可以以某种方式存储在各种等级的数组中。