我正在尝试在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,因此直接在源文件中输入它是不切实际的。
提前感谢您的任何输入! :)
答案 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