数组乘法和求和的聪明/快速方式

时间:2017-07-11 21:47:54

标签: arrays fortran integration multiplication do-loops

我必须解决双积分

enter image description here

在我的程序中,可以在以下最小工作示例中转换为i-,j-循环:

program test

  implicit none

  integer :: i, j, n
  double precision, allocatable :: y(:), res(:), C(:,:,:)

  n=200

  allocate(y(n), res(n), C(n,n,n))

  call random_number(y)
  call random_number(C)

  res = 0.d0
  do i=1, n
    do j=1, n
      res(:) = res(:) + y(i) * y(j) * C(:, j, i)
    end do
  end do

  deallocate(y, res, C)

end program test

我必须每次执行多次解决这个积分,并且分析告诉我,它是我计算的瓶颈,耗费了超过95%的执行时间。

我想知道是否有可能以更聪明,即快速的方式解决这个问题,并且可能会摆脱一个或两个循环。

我的问题不是用编译器标志或并行化来优化代码,而是双循环是否是解决给定问题的最佳实践。通常循环很慢,我试图避免它们。我在想通过重塑或扩展数组来避免循环是可能的。但我只是没有看到它。

1 个答案:

答案 0 :(得分:2)

如果以Matrix表示法编写双循环,y(i)*y(j)将成为二元YY^t,Y为n x 1矩阵。有了这个,你可以重新编写循环到(伪代码)

do n=1,size(C,1)
  res(n) = sum( YY^t * C_n )
enddo

其中C_n = C(n,:,:)*是逐元素乘法。除了你已经完成的元素计算之外,这还有两种计算结果的方法:

  1. res(n) = sum( (YY^t) * C_n )
  2. res(n) = sum( Y * (Y^t C_n) )
  3. 在这两种情况下,拥有连续数据并重新排序数组C是有益的:

      do i=1,n
        C2(:,:,i) = C(i,:,:)
      enddo !i
    

    浮点运算的数量与两种方法相同,略低于原始方法。所以让我们衡量所有人的时间......

    以下是使用LAPACK进行矩阵运算的实现(并在适用的情况下使用点积):

    <强> 1。 sum( (YY^t) * C_n )

      call system_clock(iTime1)
      call dgemm('N','N',n,n,1,1.d0,y,n,y,1,0.d0,mat,n)
    
      nn=n*n
      do i=1,n
        res(i) = ddot( nn, mat, 1, C2(:,:,i), 1 ) 
      enddo !i
    

    <强> 2。 sum( Y * (Y^t C_n) )

      do i=1,n
        call dgemm('N','N',1,n,n,1.d0,y,1,C2(:,:,i),n,0.d0,vec,1)
        res(i) = ddot( n, y, 1, vec, 1 ) 
      enddo !i
    

    结果如下:

     Orig:   0.111000001    
     sum((YY^t)C):   0.116999999    
     sum(Y(Y^tC)):   0.187000006   
    

    您原来的实施速度最快!为什么?最可能是由于CPU上缓存的理想使用。 Fortran编译器通常在优化循环方面非常聪明,并且在元素计算中,您只需添加和缩放向量,而无需任何矩阵运算。这可以非常有效地利用。

    那么,还有改进的余地吗?当然:)你在循环中执行的操作通常称为axpy:y = a*x + y。这是一个常用的BLAS子程序 - 通常是高度优化的。 利用这个导致

      res = 0.d0
      do i=1, n
        do j=1, n
          call daxpy(n, y(i)*y(j), C(:,j,i), 1, res, 1)
        end do
      end do
    

    并采取

     Orig (DAXPY):   0.101000004
    

    大约快10%。

    以下是完整的代码,所有测量都是使用OpenBLAS和n=500进行的(以便更好地了解影响)

    program test
    
      implicit none
    
      integer :: i, j, n, nn
      double precision, allocatable, target :: y(:), res(:), resC(:), C(:,:,:), C2(:,:,:), mat(:,:), vec(:)
      integer           :: count_rate, iTime1, iTime2
    
      double precision :: ddot
      n=500
    
      allocate(y(n), res(n), resC(n), C(n,n,n), C2(n,n,n), mat(n,n), vec(n))
    
      call random_number(y)
      call random_number(C)
    
      ! Get the count rate
      call system_clock(count_rate=count_rate)
    
      ! Original Aproach
      call system_clock(iTime1)
      res = 0.d0
      do i=1, n
        do j=1, n
          res(:) = res(:) + y(i) * y(j) * C(:, j, i)
        end do
      end do
      call system_clock(iTime2)
      print *,'Orig: ',real(iTime2-iTime1)/real(count_rate)
    
      ! Original Aproach, DAXPY
      call system_clock(iTime1)
      resC = 0.d0
      do i=1, n
        do j=1, n
          call daxpy(n, y(i)*y(j), C(:,j,i), 1, resC, 1)
        end do
      end do
      call system_clock(iTime2)
      print *,'Orig (DAXPY): ',real(iTime2-iTime1)/real(count_rate)
    !  print *,maxval( abs(resC-res) )
    
      ! Re-order
      do i=1,n
        C2(:,:,i) = C(i,:,:)
      enddo !i
    
      ! sum((YY^t)C)
      call system_clock(iTime1)
      call dgemm('N','N',n,n,1,1.d0,y,n,y,1,0.d0,mat,n)
    
      nn=n*n
      do i=1,n
        resC(i) = ddot( nn, mat, 1, C2(:,:,i), 1 ) 
      enddo !i
      call system_clock(iTime2)
      print *,'sum((YY^t)C): ',real(iTime2-iTime1)/real(count_rate)
    !  print *,maxval( abs(resC-res) )
    
      ! sum(Y(Y^tC))
      call system_clock(iTime1)
      do i=1,n
        call dgemm('N','N',1,n,n,1.d0,y,1,C2(:,:,i),n,0.d0,vec,1)
        resC(i) = ddot( n, y, 1, vec, 1 ) 
      enddo !i
      call system_clock(iTime2)
      print *,'sum(Y(Y^tC)): ',real(iTime2-iTime1)/real(count_rate)
    !  print *,maxval( abs(resC-res) )
    end program test