FORTRAN:通过指针矩阵访问数组,性能

时间:2014-09-07 03:42:49

标签: arrays performance pointers fortran

我在这里遇到使用指针的问题。在我这样做之前,我有一个性能问题。假设有一个像这样的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。

现在我有两种方法:

  1. 直接制作这样的NxN矩阵并计算梯度。完全按照说明进行操作。这里的内存使用是NxNxreal * 8,循环从计算(2,2)元素开始然后(2,3),...

  2. 制作一个(N-2)x(N-2)+1数组和一个NxN指针矩阵(此刻使用类型)。数组的(N-2)x(N-2)个元素将存储除边界上的0.0之外的数字。最后一个元素是0.0。对于指针矩阵,边界上的所有元素都将指向数组的最后一个元素0.0。其他指针应该指向他们想要指向的地方。

  3. 由于我正在处理的矩阵可能非常庞大或可能是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做了,差异不应该那么大。

2 个答案:

答案 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的因子!!!)是最快的方法。


更新:我试图从代码中挤出一些性能,发现一件小事。您的代码在-O20.95s 0.30s中使用n = 10000执行。

转置矩阵以获得更线性的内存访问会为指针部分生成0.50s的运行时间。

parray(i,j)%ptr => array((j-1) * n + i)

恕我直言,问题是关于指针的缺失信息,禁止额外的优化。使用-O3 -fopt-info-missed,您会收到有关未知对齐和非连续访问的投诉。与我的结果相比,附加因子2应该源于这样一个事实,即您使用的是双精度,而我的代码是以单精度编写的......

答案 1 :(得分:0)

我接受斯特凡的回答是最好的答案。但我个人希望对讨论和我自己的问题做出结论。

  1. 根据Vladimir,FORTRAN指针与C指针不同。似乎FORTRAN标准旨在使数组指针成为&#34;子集&#34;对于阵列。因此&#34;指针数组&#34;在FORTRAN中,与C中的情况不同,几乎没有意义。请阅读Stefan的代码,了解使用FORTRAN指针的详细信息。此外,&#34;指针数组&#34;在FORTRAN中是可能的,但它的低性能绝对不是一个简单的解引用。

  2. 使用循环展开的直接访问可以提高计算的性能。请在Stefan的代码中找到详细信息。使用直接访问时,根据Stefan的评论,可以更好地完成编译器优化。我认为这就是为什么人们在不使用指针解决Stencil问题的情况下这样做的原因。

  3. 使用指针处理模板的想法是降低内存成本并使边界条件非常灵活。但目前不是性能选择。主要原因是&#34;非连续&#34;由于不了解指针模式,因此无法执行内存访问和编译器优化。

  4. 请参阅Stefan对FORTRAN指针的回答。