【问题标题】:Performance drop of DO loops by potential change of bound variables绑定变量的潜在变化导致 DO 循环的性能下降
【发布时间】:2015-08-23 14:20:05
【问题描述】:

在下面的代码中,我们将两个数组传递给一个子例程,并在 DO 循环中执行一些额外的操作。在这里,我们考虑进行不同操作的三种情况:情况 1 = 无操作,情况 2 和 3 = 指针变量的赋值。

!------------------------------------------------------------------------
module mymod
    implicit none
    integer, pointer :: n_mod
    integer :: nloop
contains

!.........................................................
subroutine test_2dim ( a, b, n )
    integer :: n
    real :: a(n,n), b(n,n)

    integer, pointer :: n_ptr
    integer i1, i2, iloop

    n_ptr => n_mod

    do iloop = 1, nloop

        do i2 = 1, n
        do i1 = 1, n
            b(i1,i2) = a(i1,i2) + b(i1,i2) + iloop

             !(nothing here)    !! Case 1 : gfort => 3.6 sec, ifort => 3.2 sec
!            n_ptr = n          !! Case 2 : gfort => 15.9 sec, ifort => 6.2 sec
!            n_ptr = n_mod      !! Case 3 : gfort => 3.6 sec, ifort => 3.5 sec
        enddo
        enddo

    enddo
endsubroutine

endmodule

!------------------------------------------------------------------------
program main
    use mymod
    implicit none
    integer, target :: n
    real, allocatable :: a(:,:), b(:,:)

    nloop = 10000 ; n = 1000
    allocate( a( n, n ), b( n, n ) )
    a = 0.0 ; b = 0.0

    n_mod => n

    call test_2dim ( a, b, n )

    print *, a(n,n), b(n,n)   !! for check
end

在这里,我们注意到这个指针通过模块变量 (n_mod) 链接到 DO 循环的上限。因此,更改循环内的指针变量应该会影响循环的行为。但请注意,我们在实践中不会更改边界(只是变量的副本)。 gfortran 4.8 和带有 -O3 的 ifort 14.0 给出了上面列出的时间。值得注意的是,与案例 1 相比,案例 2 非常慢,尽管净计算似乎没有太大差异。我怀疑这可能是因为编译器无法确定第二个循环(for i1)的上限是否被指针赋值改变,因此避免了激进的优化。为了验证这一点,我测试了以下例程来代替 test_2dim():

subroutine test_1dim ( a, b, n )
    integer :: n
    real :: a(n * n), b(n * n)

    integer, pointer :: n_ptr
    integer iloop, i

    n_ptr => n_mod

    do iloop = 1, nloop

        do i = 1, n * n
             b( i ) = a( i ) + b( i ) + iloop

             ! (nothing here)    !! Case 1 : gfort => 3.6 sec, ifort => 2.3 sec
             ! n_ptr = n         !! Case 2 : gfort => 15.9 sec, ifort => 6.0 sec
             ! n_ptr = n_mod     !! Case 3 : gfort => 3.6 sec, ifort => 6.1 sec
        enddo

    enddo
endsubroutine

这里 test_1dim() 和 test_2dim() 之间的唯一区别是数组 a 和 b 是通过 1-或 2-dim 索引访问的(计算量基本上没有差异)。令人惊讶的是,案例 2 的结果也很慢,尽管只有一个 DO 循环。因为 Fortran DO 循环在输入 [Ref] 时确定循环的上限,所以我预计 test_1dim() 不会受到指针分配的影响,尽管情况并非如此。那么,对于这种行为有什么合理的解释吗? (我希望我没有犯一些导致这个时差的大错误。)


我提出这个问题的动机:例如,我一直在广泛使用派生类型来指定多维循环

module Grid_mod
type Grid_t
    integer :: N1, N2, N3
endtype
....

subroutine some_calc ( vector, grid )
type(Grid_t) :: grid
....
do i3 = 1, grid % N3
do i2 = 1, grid % N2
do i1 = 1, grid % N1
    (... various operations...)
enddo
enddo
enddo

到目前为止,我还没有过多关注 Grid_t 对象是否被赋予了 TARGET 或 POINTER 属性(假设它对性能基本上没有影响)。但是,我现在认为,如果编译器无法确定循环内的上限是否恒定(尽管我从不在实际代码中更改边界),这可能会导致性能下降。因此,如果我应该更加小心地对绑定变量(包括派生类型组件,如上面的网格对象给出)使用 TARGET 或 POINTER 属性,我将不胜感激。


更新

按照@francescalus 的建议,我尝试将“intent(in), value”附加到虚拟参数“n”。结果如下:

test_1dim():
    Case 1: gfort => 3.6 s, ifort => 2.3 s
    Case 2: gfort => 3.6 s, ifort => 3.1 s
    Case 3: gfort => 3.6 s, ifort => 3.4 s 

test_2dim():
    Case 1: gfort => 3.7 s, ifort => 3.1 s
    Case 2: gfort => 3.7 s, ifort => 3.1 s
    Case 3: gfort => 3.7 s, ifort => 6.4 s

虽然 ifort 在 test_2dim() 中为案例 3 提供了一些不规则的结果(6.4 秒),但所有其他结果基本上显示了最佳性能。这强烈表明编译器对边界的处理确实会影响性能(不是由于指针分配的成本)。因为告诉编译器边界是恒定的似乎很重要,所以我还尝试将虚拟参数 n(这里,不是使用“intent(in),值”)复制到局部变量 n_ 并将其用作循环边界:

  integer :: n    !! dummy argument
  integer :: n_   !! a local variable
  ...
  n_ = n

  do i2 = 1, n_
  do i1 = 1, n_
      b(i1,i2) = a(i1,i2) + b(i1,i2) + iloop
      ...

test_2dim() 的结果如下:

test_2dim():
    Case 1: gfort => 3.6 s, ifort => 3.1 s
    Case 2: gfort => 15.9 s, ifort => 6.2 s
    Case 3: gfort => 3.7 s, ifort => 6.4 s

不幸的是(与我的预期相反),案例 2 根本没有改进......虽然复制到本地 n_ 应该保证 n_ 在 DO 循环中是恒定的,但编译器似乎并不高兴,因为数组形状仍然由 n 决定,而不是由 n_ 决定,因此仍然避免激进的优化(


更新2

根据@innoSPG 的建议,我还在案例 2 的 DO 循环内将 n 更改为 n_,然后结果证明代码的运行速度与案例 1 一样快!具体来说,代码是

  n_ = n

  do i2 = 1, n_
  do i1 = 1, n_
      b(i1,i2) = a(i1,i2) + b(i1,i2) + iloop
      
      n_ptr = n_          !! Case 2 : gfort => 3.7 sec, ifort => 3.1 sec

但正如答案所暗示的那样,这种效率可能是因为编译器完全消除了赋值语句。所以我觉得需要考虑更多实用的代码(不是太简单的)来测试指针或者指针组件对循环优化的影响……

(...对不起,问了一个很长的问题...)

【问题讨论】:

  • 我也觉得时间差可能是因为指针变量的赋值比较费钱,但不是很确定……
  • 尝试在子例程中将intent(in), value 添加到n 的声明中(除非您确实希望有可能进行更改)。
  • @francescalus 我已经更新了问题,所以请检查结果。如果您对此给出简短的回答(解释)(即为什么 VALUE 属性会提高性能),我将不胜感激,所以我会接受答案。
  • 恐怕我并没有完全理解发生了什么。不过,我会看看我是否可以写一些明智的东西。希望对这类事情更熟悉的人可以帮助您。
  • 好的,非常感谢,完全没有问题:) 因为我的问题本身就很模棱两可,所以我会保持开放一段时间,看看我是否可以更清楚地说明这一点。

标签: arrays loops pointers fortran


【解决方案1】:

当您进行优化时,编译器需要更多时间来了解您的程序的更多信息,这样它就可以避免任何不必要的计算,除了利用架构。这在很大程度上取决于编译器和架构。

所以我的猜测是编译器事先知道n_ptrn_mod 是完全相同的东西,甚至没有花时间为案例3 赋值。当n 时,案例2 的场景类似是intent(in),编译器可以预测不需要在循环中进行赋值,只需要进行一次,因为n_ptr不参与子程序中的任何其他计算。我怀疑 ifort 错过了这一点。此外,您可能在基于英特尔的架构上运行,这为 ifort 提供了一些优势。

对于n 不是intent(in) 的情况2,编译器记住它是一个目标,可以通过许多其他方式进行更改,并且绝对没有指向它的指针的线索。这增加了在分配期间对指针的取消引用以缩短计算时间。基本上,从指针变量加载/保存指向的值需要两倍的时间来从非指针变量加载/保存值。我不知道指针是如何在 fortran 中实际实现的,所以我无法给出时间因素的严肃提示。它强烈依赖于指针和目标的实现。

我还没有尝试过,但我建议您使用局部变量 n_ 进行测试,您还将案例 2 赋值的右侧更改为局部变量 n_。我坚信您将获得与案例 1 相同的时间,因为编译器可以预测不需要在循环中进行赋值。

n_ = n
do iloop = 1, nloop

    do i2 = 1, n_
    do i1 = 1, n_
        b(i1,i2) = a(i1,i2) + b(i1,i2) + iloop

         !(nothing here)    !! Case (1)
         !n_ptr = n_          !! Case (2)
         !n_ptr = n_mod      !! Case (3)
    enddo
    enddo

enddo

【讨论】:

  • gfortran(以及我见过的所有其他编译器)中的标量指针只是一个地址。
  • @innoSPC 非常感谢。是的,编译器可能已经消除了分配指针变量的行......(因为它没有意义)。我之前也经历过类似的事情,gfortran 删除了不必要的行,而 ifort 保守地保留了它们。在进行更多测试(包括您建议的测试)后,我会回复您。
  • @VladimirF,感谢关于 gfortran 指针实现的观点。您是否深入研究过,与没有指针属性的标量变量相比,分配给具有指针属性的标量变量的开销是多少?
  • 我建议您使用 -fdump-tree-optimized 标志并检查生成的代码。如果您了解 C 的基础知识,则很有可能阅读它。
  • @VladimirF,谢谢,我会的。我在 fortran 之前学习 C 编程,尽管现在我用 C 的次数不多。
猜你喜欢
  • 2018-10-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-17
  • 2019-02-05
  • 2011-02-13
  • 2017-08-17
  • 1970-01-01
相关资源
最近更新 更多