最有效的方法来对Fortran中的一些矩阵进行加权和求和

时间:2013-04-20 18:53:20

标签: performance fortran

我正在尝试在Fortran中编写一个函数,该函数将多个矩阵与不同的权重相乘,然后将它们相加以形成单个矩阵。我已经确定这个过程是我程序中的瓶颈(对于单个程序运行,这个加权将是很多次,具有不同的权重)。现在我正试图通过从Matlab切换到Fortran来使其运行得更快。我是Fortran的新手,所以我很感激所有帮助。

在Matlab中,我发现进行这种计算的最快方法如下:

function B = weight_matrices()
n = 46;
m = 1800;
A = rand(n,m,m);
w = rand(n,1);
tic;
B = squeeze(sum(bsxfun(@times,w,A),1));
toc;

分配B的行在我的机器上运行约0.9秒(Matlab R2012b,MacBook Pro 13“视网膜,2.5 GHz Intel Core i5,8 GB 1600 MHz DDR3)。应该注意的是我的问题,张量A对于整个程序运行(初始化后)将是相同的(常量),但是w可以取任何值。此外,n和{{1的典型值这里使用的,意味着张量m在内存中的大小约为1 GB。

我能想到在Fortran中写这个的最清晰的方式是这样的:

A

当使用gfortran 4.7.2编译时,此函数运行大约1.4秒,使用-O3(函数调用时间为“call cpu_time(t)”)。如果我手动打开循环

pure function weight_matrices(w,A) result(B)
    implicit none
    integer, parameter :: n = 46
    integer, parameter :: m = 1800
    double precision, dimension(num_sizes), intent(in) :: w
    double precision, dimension(num_sizes,msize,msize), intent(in) :: A
    double precision, dimension(msize,msize) :: B
    integer :: i
    B = 0
    do i = 1,n
        B = B + w(i)*A(i,:,:)
    end do
end function weight_matrices

该函数大约需要0.11秒才能运行。这很棒,意味着与Matlab版本相比,我获得了大约8倍的加速。但是,我仍然对可读性和性能有一些疑问。

首先,我想知道是否有更快的方法来执行矩阵的加权和求和。我查看过BLAS和LAPACK,但找不到任何合适的功能。我还尝试将维度放在B = w(1)*A(1,:,:)+w(2)*A(2,:,:)+ ... + w(46)*A(46,:,:) 中,将矩阵枚举为最后一个维度(即从元素的A切换到(i,j,k)),但这会导致代码变慢。< / p>

其次,这个快速版本不是很灵活,实际上看起来很难看,因为它是如此简单的计算文本。对于我正在运行的测试,我想尝试使用不同数量的权重,以便w的长度会有所不同,以了解它如何影响我的算法的其余部分。但是,这意味着我每次都很难重写(k,i,j)的任务。有没有办法让这个更灵活,同时保持性能相同(或更好)?

第三,如前所述,张量B将在程序运行期间保持不变。我使用自己模块中的“parameter”属性在程序中设置了常量标量值,并使用“use”表达式将它们导入到需要它们的函数/子程序中。为张量A做同等事情的最佳方法是什么?我想告诉编译器这个张量在init之后将是常量,这样就可以完成任何相应的优化。请注意,A的大小通常约为1 GB,因此直接在源文件中输入它是不切实际的。

提前感谢您的任何输入! :)

4 个答案:

答案 0 :(得分:3)

也许你可以试试像

这样的东西
    do k=1,m
       do j=1,m
          B(j,k)=sum( [ ( (w(i)*A(i,j,k)), i=1,n) ])
       enddo
    enddo

方括号是(/ /)的新形式,1d矩阵(向量)。 sum中的字词是维度(n)的矩阵,sum汇总了所有这些元素。这正是您的解包代码所做的(并且不完全等于您拥有的do循环)。

答案 1 :(得分:1)

我不会隐藏任何循环,因为这通常较慢。您可以明确地编写它,然后您将看到内部循环访问超过最后一个索引,从而使其效率低下。因此,您应该通过存储A n来确保您的A(m,m,n)维度是最后一个维度:

B = 0
do i = 1,n
    w_tmp = w(i)
    do j = 1,m
        do k = 1,m
            B(k,j) = B(k,j) + w_tmp*A(k,j,i)
        end do
    end do
end do

这应该更有效率,因为您现在正在内循环中访问内存中的连续元素。

另一个解决方案是使用1级BLAS子程序_AXPY(y = a * x + y):

B = 0
do i = 1,n
    CALL DAXPY(m*m, w(i), A(1,1,i), 1, B(1,1), 1)
end do

使用英特尔MKL,这应该更有效率,但是你应该确保最后一个索引是在外部循环中改变的索引(在这种情况下是你正在编写的循环)。您可以在此处找到此调用的必要参数:MKL

编辑:您可能还想使用一些并行化? (我不知道Matlab是否利用了这一点)

EDIT2:在Kyle的回答中,内部循环超过了w的不同值,这比n次重新加载B更有效,因为w可以保存在缓存中(使用A(n,m,m)):

B = 0
do i = 1,m
    do j = 1,m
        B(j,i)=0.0d0
        do k = 1,n
            B(j,i) = B(j,i) + w(k)*A(k,j,i)
        end do
    end do
end do

这个显式循环比使用全数组操作的Kyle代码好大约10%。带ifort -O3 -xHost的带宽约为6600 MB / s,gfortran -O3为~6000 MB / s,带有任一编译器的全数组版本也约为6000 MB / s。

答案 2 :(得分:1)

我试图改进Kyle Vanos的解决方案。

因此我决定使用sum和Fortran的矢量功能。

我不知道,如果结果是正确的,因为我只是寻找时间!

版本1:(用于比较)

B = 0
do i = 1,n
    B = B + w(i)*A(i,:,:)
end do

版本2:(来自Kyle Vanos)

do k=1,m
   do j=1,m
      B(j,k)=sum( [ ( (w(i)*A(i,j,k)), i=1,n) ])
   enddo
enddo

版本3:(混合索引,一次只能处理一行/列)

do j = 1, m
    B(:,j)=sum( [ ( (w(i)*A(:,i,j)), i=1,n) ], dim=1)
enddo

版本4:(完整矩阵)

B=sum( [ ( (w(i)*A(:,:,i)), i=1,n) ], dim=1)

<强>时序

正如您所看到的,我不得不混淆索引以获得更快的执行时间。第三个解决方案真的很奇怪,因为矩阵的数量是中间索引,但这对于内存顺序原因是必要的。

V1: 1.30s
V2: 0.16s
V3: 0.02s
V4: 0.03s

总而言之,我想说,如果你有可能以任意顺序改变矩阵指数的顺序,你可以获得大规模的加速。

答案 3 :(得分:0)

我知道这是一篇很老的帖子,但是我会很高兴在我发布大部分已发布的解决方案时带来我的贡献。

通过为权重循环添加本地展开(来自​​Steabert的答案),与完整的展开版本相比,给我一点加速(从不同大小的矩阵的10%到80%)。部分展开可以帮助编译器在一次SSE调用中对4个操作进行矢量化。

pure function weight_matrices_partial_unroll_4(w,A) result(B)
  implicit none
  integer, parameter  :: n = 46
  integer, parameter  :: m = 1800
  real(8), intent(in) :: w(n)
  real(8), intent(in) :: A(n,m,m)
  real(8)             :: B(m,m)
  real(8)             :: Btemp(4)
  integer             :: i, j, k, l, ndiv, nmod, roll
  !==================================================
  roll = 4
  ndiv = n / roll
  nmod = mod( n, roll )

  do i = 1,m
    do j = 1,m
        B(j,i)=0.0d0
        k = 1
        do l = 1,ndiv
          Btemp(1) = w(k  )*A(k  ,j,i)
          Btemp(2) = w(k+1)*A(k+1,j,i)
          Btemp(3) = w(k+2)*A(k+2,j,i)
          Btemp(4) = w(k+3)*A(k+3,j,i)
          k = k + roll
          B(j,i) = B(j,i) + sum( Btemp )
        end do

        do l = 1,nmod !---- process the rest of the loop
          B(j,i) = B(j,i) + w(k)*A(k,j,i)
          k = k + 1
        enddo
    end do
  end do
end function