我在这里遇到使用指针的问题。在我这样做之前,我有一个性能问题。假设有一个像这样的2D矩阵:
0.0 0.0 0.0.....
0.0 0.7 0.5.....
0.0 0.5 0.8.....
0.0 0.3 0.8.....
.....
我需要计算这个东西的梯度。因此,对于每个数字,我将需要该数字以及该2D矩阵的所有4个最近邻居。除了第一行和最后一行和列是0。
现在我有两种方法:
直接制作这样的NxN矩阵并计算梯度。完全按照说明进行操作。这里的内存使用是NxNxreal * 8,循环从计算(2,2)元素开始然后(2,3),...
制作一个(N-2)x(N-2)+1数组和一个NxN指针矩阵(此刻使用类型)。数组的(N-2)x(N-2)个元素将存储除边界上的0.0之外的数字。最后一个元素是0.0。对于指针矩阵,边界上的所有元素都将指向数组的最后一个元素0.0。其他指针应该指向他们想要指向的地方。
由于我正在处理的矩阵可能非常庞大或可能是3D,因此出现了性能问题。
对于方法1,没有什么可说的,因为它只是一个简单的方法。
对于方法2,我想知道编译器是否可以正确处理问题。因为根据我的理解,每个FORTRAN指针就像一个结构。如果是这种情况, FORTRAN指针比c指针慢,因为它不仅仅是一个简单的去引用?我也想知道如果指针的类型扭曲会降低性能(需要使用warp来制作指针矩阵)。我应该放弃方法2的特殊原因是因为它应该更慢吗?
让我们以Linux上的IVF,gfortran和ifort为例。因为它可以依赖于编译器。
更新: 感谢Stefan的代码。我也是自己写的。
program stencil
implicit none
type pp
real*8, pointer :: ptr
endtype pp
type(pp), allocatable :: parray(:,:)
real*8, allocatable, target :: array(:)
real*8, allocatable :: grad(:,:,:), direct(:,:)
integer, parameter :: n = 5000
integer :: i, j
integer :: clock_rate, clock_start, clock_stop
allocate(array(n**2+1))
allocate(parray(0:n+1, 0:n+1))
allocate(grad(2, n, n))
call random_number(array)
array(n**2+1) = 0
do i = 0, n + 1
parray(0,i)%ptr => array(n**2+1)
parray(n+1,i)%ptr => array(n**2+1)
parray(i,0)%ptr => array(n**2+1)
parray(i,n+1)%ptr => array(n**2+1)
enddo
do i = 1, n
do j = 1, n
parray(i,j)%ptr => array((i-1) * n + j)
enddo
enddo
!now stencil
call system_clock(count_rate=clock_rate)
call system_clock(count=clock_start)
do j = 1, n
do i = 1, n
grad(1, i, j) = (parray(i + 1,j)%ptr - parray(i - 1,j)%ptr)/2.D0
grad(2, i, j) = (parray(i,j + 1)%ptr - parray(i,j - 1)%ptr)/2.D0
enddo
enddo
call system_clock(count=clock_stop)
print *, "pointer, time cost= ", real(clock_stop-clock_start)/real(clock_rate)
deallocate(array)
deallocate(parray)
allocate(direct(0:n+1, 0:n+1))
call random_number(direct)
do i = 0, n + 1
direct(0,i) = 0
direct(n+1,i) = 0
direct(i,0) = 0
direct(i,n+1) = 0
enddo
!now stencil directly
call system_clock(count_rate=clock_rate)
call system_clock(count=clock_start)
do j = 1, n
do i = 1, n
grad(1, i, j) = (direct(i + 1,j) - direct(i - 1,j))/2.D0
grad(2, i, j) = (direct(i,j + 1) - direct(i,j - 1))/2.D0
enddo
enddo
call system_clock(count=clock_stop)
print *, "direct, time cost= ", real(clock_stop-clock_start)/real(clock_rate)
endprogram stencil
结果(o0):
指针,时间成本= 2.170000
直接,时间成本= 1.127000
结果(o2):
指针,时间成本= 0.5110000
直接,时间成本= 9.4999999E-02
所以FORTRAN指针慢得多。斯特凡早些时候指出了这一点。现在我想知道是否还有改进的余地。据我所知,到目前为止,如果我用c做了,差异不应该那么大。
答案 0 :(得分:2)
起初,我不得不道歉,因为我误解了方法,指针在Fortran工作......
最后,我对这个话题非常感兴趣,我自己创建了一个测试。它基于一个数组,它有一个零的周围。
<强>声明:强>
real, dimension(:,:), allocatable, target :: array
real, dimension(:,:,:), allocatable :: res
real, dimension(:,:), pointer :: p1, p2, p3, p4
allocate(array(0:n+1, 0:n+1), source=0.)
allocate(res(n,n,2), source=0.)
现在方法:
<强>循环:强>
do j = 1, n
do i = 1, n
res(i,j,1) = array(i+1,j) - array(i-1,j)
res(i,j,2) = array(i,j+1) - array(i,j-1)
end do
end do
数组分配:
res(:,:,1) = array(2:n+1,1:n) - array(0:n-1,1:n)
res(:,:,2) = array(1:n,2:n+1) - array(1:n,0:n-1)
<强>指针:强>
p1 => array(0:n-1,1:n)
p2 => array(1:n,2:n+1)
p3 => array(2:n+1,1:n)
p4 => array(1:n,0:n-1)
res(:,:,1) = p3 - p1
res(:,:,2) = p2 - p4
虽然最后两个方法确实依赖于额外的零层,但循环可以引入一些条件来照顾它们。
时间有趣:
loops: 0.17528710301849060
array: 0.21127231500577182
pointers: 0.21367537401965819
虽然数组和指针赋值产生大致相同的时序,但循环结构(介意循环顺序!这是5的因子!!!)是最快的方法。
更新:我试图从代码中挤出一些性能,发现一件小事。您的代码在-O2
和0.95s
0.30s
中使用n = 10000
执行。
转置矩阵以获得更线性的内存访问会为指针部分生成0.50s
的运行时间。
parray(i,j)%ptr => array((j-1) * n + i)
恕我直言,问题是关于指针的缺失信息,禁止额外的优化。使用-O3 -fopt-info-missed
,您会收到有关未知对齐和非连续访问的投诉。与我的结果相比,附加因子2应该源于这样一个事实,即您使用的是双精度,而我的代码是以单精度编写的......
答案 1 :(得分:0)
我接受斯特凡的回答是最好的答案。但我个人希望对讨论和我自己的问题做出结论。
根据Vladimir,FORTRAN指针与C指针不同。似乎FORTRAN标准旨在使数组指针成为&#34;子集&#34;对于阵列。因此&#34;指针数组&#34;在FORTRAN中,与C中的情况不同,几乎没有意义。请阅读Stefan的代码,了解使用FORTRAN指针的详细信息。此外,&#34;指针数组&#34;在FORTRAN中是可能的,但它的低性能绝对不是一个简单的解引用。
使用循环展开的直接访问可以提高计算的性能。请在Stefan的代码中找到详细信息。使用直接访问时,根据Stefan的评论,可以更好地完成编译器优化。我认为这就是为什么人们在不使用指针解决Stencil问题的情况下这样做的原因。
使用指针处理模板的想法是降低内存成本并使边界条件非常灵活。但目前不是性能选择。主要原因是&#34;非连续&#34;由于不了解指针模式,因此无法执行内存访问和编译器优化。
请参阅Stefan对FORTRAN指针的回答。