【问题标题】:Fortran 2008: How are function return values returned?Fortran 2008:如何返回函数返回值?
【发布时间】:2016-10-23 03:45:53
【问题描述】:

在现代 Fortran 中是否可以从函数返回一个数组,其性能等同于让子例程填充作为参数传递的数组?

考虑例如举个简单的例子

PROGRAM PRETURN 

  INTEGER :: C(5)
  C = FUNC()
  WRITE(*,*) C
  CALL SUB(C)
  WRITE(*,*) C

CONTAINS 

  FUNCTION FUNC() RESULT(X)
    INTEGER :: X(5)
    X = [1,2,3,4,5]
  END FUNCTION FUNC

  SUBROUTINE SUB(X)
    INTEGER :: X(5)
    X = [1,2,3,4,5]
  END SUBROUTINE SUB

END PROGRAM PRETURN

这里C = FUNC() 行将复制函数返回值中的值,然后从堆栈中丢弃返回的数组。子例程版本CALL SUB(C) 将直接填充C,避免了与临时数组相关的额外处理步骤和内存使用——但在SUM(FUNC()) 这样的表达式中使用是不可能的。

但是,如果编译器实现选择在堆上分配所有数组,则可以简单地通过更改 C 的底层指针来分配返回值,从而在两个版本之间获得相同的性能。*

这些优化是由常见的编译器进行的,还是有其他方法可以在没有性能开销的情况下获得函数语义?


* 使用可分配数组会更明显,但这会遇到编译器支持问题。默认情况下,英特尔 fortran 不会在分配不同大小的数组时(重新)分配数组,但通过使用 ALLOCATE(C, SOURCE=FUNC()) 语句可以实现相同的效果。同时,Gfortran 会在分配时自动分配,但有一个错误会阻止 ALLOCATE 语句,其中形状是从 SOURCE 参数派生的,并且该修复尚未包含在二进制版本中。

【问题讨论】:

    标签: arrays fortran return-value return-by-reference return-by-value


    【解决方案1】:

    Fortran 标准对用该语言实现几乎所有内容的实际机制保持沉默。该语言的语义是在赋值开始之前对函数结果进行完全评估。如果将目标作为输出传递,那么如果函数由于某种原因没有完成,则变量可能会被部分修改。编译器可能能够进行足够的重叠分析来对此进行一些优化。我很确定英特尔 Fortran 不会这样做 - 语义限制很重要。

    您的示例是一个玩具程序 - 更有趣的问题是是否存在这样的优化适用且值得的生产应用程序。

    我将评论英特尔 Fortran 将更改其分配给可分配数组的默认行为,以便从版本 17 开始,将按照标准的规定进行自动重新分配。

    【讨论】:

    • 您是否知道尝试为 fortran 建立“移动分配”语义?虽然您在默认情况下反对这样做,但它似乎可以作为函数/返回值的显式属性来实现。
    • 我从未听说过标准委员会讨论过这个问题(自 2008 年以来一直是成员)。由于您可以通过子例程调用完成您想要的操作,因此对我来说这似乎是不必要的复杂化。
    【解决方案2】:

    我有时也有同样的情况。当我停下来想一想时,我意识到在 fortran 中函数很好,子程序也很好。

    想象一下,能力存在,我们有以下功能:

    function doThings(param) results(thing)
        integer :: thing
        integer, intent(in out) :: param
        ! Local variables
        integer :: genialUpdatedValue, onOfThePreviousResult
        ! some other declarations
        ! serious computation to do things
        ! and compute genialUpdatedValue and onOfThePreviousResult
        param = genialUpdatedValue
        thing = onOfThePreviousResult
    end function doThings
    

    我们有以下调用:

    ! some variables first
    integer, parameter :: N_THINGS = 50 ! just love 50
    integer :: myThing, myParam
    integer, dimension(N_THINGS) :: moreThings
    !
    ! Reading initial param from somewhere
    ! myParam now has a value
    !
    myThing = doThings(myParam)
    

    肯定没问题,下面呢

    !
    ! Reading initial param from somewhere
    ! myParam now has a value
    !
    moreThing = doThings(myParam)
    

    结果会是什么?会不会

    integer :: i
    do i = 1, N_THINGS
        moreThings(i) = doThings(myParam)
    end do
    

    还是这个

    integer :: i, thing
    thing = doThings(myParam)
    do i = 1, N_THINGS
        moreThings(i) = thing
    end do
    

    请记住,myParam 会被函数更改。有人可能会说这是一个简单的案例,但可以想象结果不是整数,而是具有大数组成员的用户定义类型。

    如果你仔细想想,你肯定会发现一些类似的问题。当然,可以在这里和那里添加更多限制以允许该功能,最终,当我们有足够的需求时,它将在必要的限制下实施。我希望这会有所帮助。

    【讨论】:

    • 能否请您澄清一下您认为这里的问题是什么?代码有些混乱,但我认为问题在于返回一个具有 SAVE 属性的变量,从而使返回指向数据的指针和通过复制返回值不同。
    • 我们这里没有保存属性。问题是返回值是根据param 计算的,param 一直在更改。所以调用一次并使用相同的值来设置数组的所有单元格与为数组的每个单元格调用函数是完全不同的。因为任何时候调用,返回值都是不同的,因为param在函数中被改变了。最简单的例子是函数递增param,返回值是param的平方。
    • 所以基本上你没有返回一个数组根本?我错过了那部分。据我所知,这将使答案与问题完全无关。虽然我的示例故意是一个简单的玩具示例 - 可能过于简化,因为我什至没有使用可分配对象 - 它专门关于通过避免将数组从一个内存区域复制到另一个内存区域来有效地返回数组值函数的结果。跨度>
    • @kdb 你看对了!它没有返回数组,这正是重点,因为 fortran 允许左侧是数组而右侧是标量的赋值。此外,我完全理解效率问题,因为我一直在考虑完全相同的功能,特别是对于分配重载。我想指出程序员在使用此类功能时将面临的一些问题。这是提高效率的一项重要功能。我们只需要考虑最终的问题以及如何克服它们。效率可以取胜,功能最终实现。
    猜你喜欢
    • 2010-11-22
    • 2014-07-24
    • 2015-10-23
    • 2020-06-02
    • 2012-08-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多