Fortran中意图(in,out,inout)有何显著区别?

19

在查阅书籍、StackOverflow和一般网站一段时间后,我发现很难找到一个简单明了的解释有关Fortran参数意图之间真正的差异。 我理解的方式是这样的:

  • intent(in) -- 实参被复制到形参中。
  • intent(out) -- 形参指向实参 (它们都指向内存中的同一位置)。
  • intent(inout) -- 形参在本地创建,然后在过程完成时复制到实参。

如果我的理解正确,那么我也想知道为什么会有人想使用intent(out),因为intent(inout)需要较少的工作(不需要复制数据)。


1
你可能会对这篇关于参数传递和意图的文章感兴趣。https://software.intel.com/en-us/blogs/2009/03/31/doctor-fortran-in-ive-come-here-for-an-argument - Jeff Irwin
3个回答

21

Intents只是编译器的提示,您可以忽略此信息并违反它。 Intents存在的主要目的是确保您在子程序中只执行计划中的操作。 编译器可能会选择相信您并优化某些内容。

这意味着intent(in)并不是按值传递。 您仍然可以覆盖原始值。

program xxxx  
    integer i  
    i = 9  
    call sub(i)  
    print*,i ! will print 7 on all compilers I checked  
end  
subroutine sub(i)  
    integer,intent(in) :: i  
    call sub2(i)  
end  
subroutine sub2(i)  
    implicit none  
    integer i  
    i = 7  ! This works since the "intent" information was lost.  
end

program xxxx  
    integer i  
    i = 9  
    call sub(i)  
end  
subroutine sub(i)  
    integer,intent(out) :: i  
    call sub2(i)  
end  
subroutine sub2(i)  
    implicit none   
    integer i  
    print*,i ! will print 9 on all compilers I checked, even though intent was "out" above.  
end  

@KarlYngveLervåg,我不这么认为,因为这个回答实际上并没有回答你的原始问题。这是有用的信息,但无论如何,“正确”的(即最有帮助的)答案是由你来决定的。我认为另一个回答更接近回答你的问题。 - pattivacek
我已将此问题标记为正确答案,因为另一个答案是错误的(intent(in)不是传值)。 - Karl Yngve Lervåg
3
这个回答中的示例代码不符合规范。修改一个 intent(in) 变量是程序员的错误。在首次定义之前依赖于一个 intent(out) 变量的值也是程序员的错误。编译器和编译器运行时可能无法捕获这些编程错误,但它们仍然是编程错误。 - IanH

6
  • intent(in) - 看起来像是传值(并且在外部代码中不会反映出其更改),但实际上是按引用传递的,并且编译器禁止更改它。但是它仍然可以被更改。
  • intent(out) - 按某种方式通过引用传递,实际上是一个返回参数
  • intent(inout) - 按引用传递,正常的输入/输出参数。

如果是纯输出,请使用intent(out)来记录您的设计。不要为了非常微小的性能提升而担心。(注释表明由于intent(in)技术上也是按引用传递,因此没有任何性能提升。)


你确定性能提升无足轻重吗?如果一个函数被频繁调用并且参数是非常大的数组,会怎么样呢? - Karl Yngve Lervåg
如果你仅在微基准测试中调用方法,那么它会花费很多时间。是的,如果传递非常大的数组,可能需要考虑这一点。但大多数情况下,请记住,“过早优化是万恶之源”。当你证明瓶颈存在并且它是占用你时间前三名的事情之一时,你仍然可以随时更改它。我总是首先使用设计原则。 - Peter Kofler
好的。谢谢你的详细说明 :) - Karl Yngve Lervåg
4
意图(intent)是按值传递的吗?我一直认为它只是一个只读提示给编译器。 - wgodoy
11
是的,你的答案是错误的。intent(in) 不是传值方式。你仍然可以通过在不使用 intent 的情况下将参数进一步传递到子程序来实际更改实参。它只是一个承诺,表明你不想改变参数,实际的传递机制并没有受到影响。 - Vladimir F Героям слава
2
更改了答案。显然,我(引用文档)是错误的。 - Peter Kofler

1
不清楚是否有回答到OP问题的部分。此外,回答/讨论中似乎存在许多混淆和各种错误,可能需要一些澄清。
A)OP的问题Re “然后我也想知道为什么会想要使用intent(out),因为intent(inout)需要更少的工作(不需要复制数据)。”
可能没有得到回答,或者至少不是太直接/正确。
首先,明确一点,Intent属性至少有两个目的:“安全/卫生”和“间接性能”问题(而不是“直接性能”问题)。
1)安全/卫生:帮助生成“安全/合理”的代码,减少“搞砸事情”的机会。因此,Intent(In)不能被覆盖(至少在本地,甚至在某些情况下“全局”下面,请参见下文)。
同样,Intent(Out)要求将Arg分配一个“明确的答案”,从而有助于减少“垃圾”结果。
例如,在计算数学中最常见的问题之一——所谓的“Ax=b问题”的解决方案中,人们正在寻找的“直接结果/答案”是向量x的值。这些值应该是Intent(Out),以确保x被分配一个“明确”的答案。如果将x声明为Intent(InOut)或“没有意图”,那么Fortran会给x分配一些“默认值”(在Debug模式下可能是“零”,但在Release模式下可能是“垃圾”,即Args指针位置的内存中的任何内容),如果用户没有明确地将正确的值分配给x,则会返回“垃圾”。Intent(Out)会“提醒/强制”用户将值明确地分配给x,从而消除这种“(意外的)垃圾”。
在解决过程中,几乎肯定会产生矩阵A的逆。用户可能希望将其逆返回到调用的s/r中,以替换A,这种情况下A应该是Intent(InOut)。
另外,用户可能希望确保不对矩阵A或向量b进行任何更改,这种情况下它们将被声明为Intent(In),从而确保关键值不会被覆盖。

2 a) "间接性能"(以及“全局安全/卫生”):虽然意图并非直接影响性能,但它们会间接地影响。特别是某些类型的优化,尤其是Fortran纯净和元素结构,可以产生大大改善的性能。这些设置通常要求所有Args明确声明其Intent。

粗略地说,如果编译器事先知道所有变量的Intent,则可以更轻松有效地进行代码优化和“愚蠢检查”。

至关重要的是,如果使用Pure等结构,则很有可能也会出现“一种全局安全/卫生”,因为纯/元素s/p只能调用其他纯/元素s/p,因此不可能出现“The Glazer Guy”的示例中所示的情况。

例如,如果将Sub1()声明为Pure,则还必须将Sub2()声明为Pure,然后需要在所有级别上声明Intents,因此在“The Glazer Guy”的示例中产生的“垃圾输出”是不可能发生的。也就是说,代码将是:

Pure subroutine sub_P(i)
    integer,intent(in) :: i
    call sub2_P(i)
end  subroutine sub_P

Pure subroutine sub2_P(i)
    implicit none
!        integer i          ! not permitted to omit Intent in a Pure s/p
    integer,intent(in) :: i
    i = 7   ! This WILL NOT WORK/HAPPEN, since Pure obviates the possibility of omitting Intent, and Intent(In) prohibits assignment ... so "i" remains "safe".
end  subroutine sub2_P

当编译时,它会产生类似于

" ||错误:在变量定义上下文(赋值)中具有INTENT(IN)的虚拟参数'i' |(1)| "

当然,sub2不需要是Pure,也可以声明i为Intent(In),这将再次提供所需的“安全/卫生”。

请注意,即使i被声明为Intent(InOut),它仍将与Pure失败。也就是说:

Pure subroutine sub_P(i)
    integer,intent(in) :: i
    call sub2_P(i)
end  subroutine sub_P

Pure subroutine sub2_P(i)
    implicit none
    integer,intent(inOut) :: i
    i = 7   ! This WILL NOT WORK, since Pure obviates the possibility of "mixing" Intent's.
end  subroutine sub2_P

在编译时,这将产生类似于

"||错误:变量定义上下文中的INTENT(IN)虚拟参数 'i'(实参为INTENT = OUT / INOUT)(位于1处)|"

因此,严格或广泛依赖Pure / Elemental结构将确保(大多数情况下)“全局安全/卫生”。

在所有情况下都不可能使用Pure / Elemental等(例如,许多混合语言设置,或者依赖于超出您控制范围的外部库等)。

尽管如此,在可能的情况下始终一致地使用Intents,以及Pure等,将产生很多好处,并消除很多烦恼。

当有可能时,人们可以简单地养成随时声明Intents的习惯,无论是否为Pure ... 这是推荐的编码实践。

...这也带来了存在BOTH Intent(InOut)和Intent(Out)的另一个原因,因为Pure必须声明所有Arg的Intent,所以会有一些仅为Out的Arg,而其他Arg则为InOut(即,很难没有每个In, InOut和Out Intents的Pure)。

2 b) OP的评论表明了对Fortran及其广泛使用的传递方式存在误解,期望“性能提升”,因为不需要复制。通过引用传递意味着基本上只需要指针,事实上,通常仅需要指向数组第一个元素的指针(加上一些隐藏的数组信息)。

事实上,“旧时代”(例如Fortran IV、77等)可能会提供一些见解,当传递数组时可能会编写如下代码:

Real*8 A(1000)

Call Sub(A)

Subroutine Sub(A)

Real*8 A(1) ! this was workable since Fortran only passes the pointer/by ref to the first element of A(1000)
                ! modern Fortran may well throw a bounds check warning

在现代Fortran中,将A声明为Real(DP) A(:)是在子程序中的"等效方式"(尽管严格来说,有各种设置可以从传递数组的边界和显式声明边界中受益,但那将是另一天的冗长讨论)。
也就是说,Fortran不是按值传递,也不会为Args / Dummy变量“制作副本”。调用s/r中的A()与s/r中使用的“A”相同(当然,在s/r中,可以复制A()或其他内容,这将创建额外的工作/空间要求,但这是另一回事)。
主要是由于这个原因,Intent并不直接对性能产生很大影响,即使是大型数组Arg等。
关于“按值传递”的混淆问题:虽然上面的各种响应确实确认使用Intent不是“按值传递”,但澄清此事可能会有所帮助。
更改措辞为“Intent始终按引用传递”可能有所帮助。这并不意味着“不按值传递”,这是一个重要的微妙之处。特别是,Intents不仅是“按引用传递”,而且还可以防止按值传递。

虽然有一些特殊/更复杂的设置(例如混合语言Fortran DLL等),需要进行更多的讨论,但对于大部分的“标准Fortran”,参数是通过引用传递的。可以通过简单扩展“Glazer Guys”示例来演示这种“意图细微差别”,如下所示:

subroutine sub(i)
    integer, intent(in) :: i, j
    integer, value     :: iV, jV
    call sub2(i)
    call sub3(i, j, jV, iV)
end
subroutine sub2(i)
    implicit none
    integer i
    i = 7  ! This works since the "intent" information was lost.
end
subroutine sub3(i, j, jV, iV)
    implicit none
    integer, value, Intent(In)          :: i    ! This will work, since passed in byRef, but used locally as byVal
    integer, value, Intent(InOut)       :: j    ! This will FAIL, since ByVal/ByRef collision with calling s/r,
                                                ! ||Error: VALUE attribute conflicts with INTENT(INOUT) attribute at (1)|
    integer, value, Intent(InOut)       :: iV   ! This will FAIL, since ByVal/ByRef collision with calling s/r,
                                                ! ... in spite of "byVal" in calling s/r
                                                ! ||Error: VALUE attribute conflicts with INTENT(INOUT) attribute at (1)|
    integer, value, Intent(Out)         :: jV   ! This will FAIL, since ByVal/ByRef collision with calling s/r
                                                ! ... in spite of "byVal" in calling s/r
                                                ! ||Error: VALUE attribute conflicts with INTENT(OUT) attribute at (1)|
    jV = -7
    iV = 7
end

换句话说,任何具有“Out”方面的内容都必须是“byRef”(至少在正常设置中),因为调用s/r期望是“byRef”。因此,即使所有的s/r将Args声明为“Value”,它们也仅在本地是“byVal”(同样是在标准设置中)。因此,被调用的s/r试图返回一个声明为Value且带有任何Out意图的Arg,将由于传递风格的“冲突”而失败。
如果必须是“Out”或“InOut”和“Value”,则不能使用Intent:这比简单地说“它不是按值传递”更为复杂。

天啊,这太长了,我没看完。我强烈建议在SO上保持简洁。但是我不明白你所说的纯净和混合意图的观点。在我的看法中,在i=7的例子中,纯净根本没有起到任何作用。而且,real(8)是一个丑陋的坏习惯,在某些编译器中根本无法编译。 - Vladimir F Героям слава

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接