【发布时间】: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