roy*_*vib 5 arrays fortran pointers loops
在下面的代码中,我们将两个数组传递给子例程,并在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
Run Code Online (Sandbox Code Playgroud)
这里,我们注意到这个指针通过模块变量(n_mod)链接到DO循环的上限.因此,更改循环内的指针变量应该会影响循环的行为.但请注意,我们不会改变实践中的界限(只是变量的副本).gfortran 4.8和ifort 14.0与-O3给出了上面列出的时间.值得注意的是,与案例1相比,案例2非常缓慢,尽管净计算似乎没有太大差别.我怀疑这可能是因为编译器无法确定第二个循环(对于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
Run Code Online (Sandbox Code Playgroud)
这里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
Run Code Online (Sandbox Code Playgroud)
到目前为止,我还没有太注意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
Run Code Online (Sandbox Code Playgroud)
虽然ifort在test_2dim()中对案例3给出了一些不规则的结果(6.4 s),但所有其他结果基本上都表现出最佳性能.这有力地表明编译器对边界的处理确实会影响性能(不是由于指针分配的代价).因为告诉编译器边界是常量似乎很重要,我还尝试将伪参数n(此处,不是"intent(in),value")复制到局部变量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
...
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
不幸的是(与我的期望相反),案例2根本没有改进......虽然复制到本地n_应该保证n_在DO循环中是常量,编译器似乎不高兴,因为数组形状仍然是由n确定,而不是由n确定,因此仍然避免积极优化(< - 只是我的猜测).
UPDATE2
根据@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
Run Code Online (Sandbox Code Playgroud)
但正如答案所暗示的那样,这种效率可能是因为编译器完全消除了赋值语句.所以我认为我需要考虑更实用的代码(不是太简单的代码)来测试指针或指针组件对循环优化的影响......
(......对不起,我很抱歉......)
当您进行优化时,编译器会花费更多时间来了解有关程序的更多信息,以便除了利用架构之外还可以避免任何不必要的计算。这在很大程度上取决于编译器和体系结构。
所以我的猜测是,编译器事先知道 和n_ptr是n_mod完全相同的事情,甚至不会花时间为情况 3 进行赋值。情况与情况 2 类似,当n意图(in)时,编译器可以预测它不需要在循环中进行赋值,只需要进行一次,因为n_ptr不参与子程序中的任何其他计算。我怀疑我忽略了这一点。此外,您可能运行在基于英特尔的架构上,这给 ifort 带来了一些优势。
对于情况2,当n不是intent(in)时,编译器会记住它是一个目标,并且可以通过许多其他方式进行更改,并且绝对没有指向它的指针的线索。这增加了赋值期间指针的解引用,从而节省了计算时间。基本上,从指针变量加载/保存指向的值需要比从非指针变量加载/保存值多两倍的时间。我不知道指针在 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
Run Code Online (Sandbox Code Playgroud)