我正在阅读“使用Fortran进行科学软件开发”一书,其中有一个练习,我觉得非常有趣:
“创建一个名为MatrixMultiplyModule的Fortran模块。添加三个子程序,称为LoopMatrixMultiply,IntrinsicMatrixMultiply和MixMatrixMultiply。每个例程应该将两个实矩阵作为参数,执行矩阵乘法,并通过第三个参数返回结果.LoopMatrixMultiply应该完全用do循环编写,没有数组操作或内部过程; IntrinsicMatrixMultiply应该使用matmul内部函数编写;而MixMatrixMultiply应该使用一些do循环和内部函数dot_product编写。编写一个小程序来测试这些函数的性能对不同大小的矩阵执行矩阵乘法的三种不同方法。“
我对两个秩2矩阵的乘法进行了一些测试,这里是不同优化标志下的结果:
compiler:ifort version 13.0.0 on Mac
这是我的问题:
为什么在-O0下它们具有相同的性能,但是当使用-O3时matmul具有巨大的性能提升,而显式循环和点产品的性能提升较少?另外,为什么dot_product与显式do循环相比似乎具有相同的性能?
我使用的代码如下:
module MatrixMultiplyModule
contains
subroutine LoopMatrixMultiply(mtx1,mtx2,mtx3)
real,intent(in) :: mtx1(:,:),mtx2(:,:)
real,intent(out),allocatable :: mtx3(:,:)
integer :: m,n
integer :: i,j
if(size(mtx1,dim=2) /= size(mtx2,dim=1)) stop "input array size not match"
m=size(mtx1,dim=1)
n=size(mtx2,dim=2)
allocate(mtx3(m,n))
mtx3=0.
do i=1,m
do j=1,n
do k=1,size(mtx1,dim=2)
mtx3(i,j)=mtx3(i,j)+mtx1(i,k)*mtx2(k,j)
end do
end do
end do
end subroutine
subroutine IntrinsicMatrixMultiply(mtx1,mtx2,mtx3)
real,intent(in) :: mtx1(:,:),mtx2(:,:)
real,intent(out),allocatable :: mtx3(:,:)
integer :: m,n
integer :: i,j
if(size(mtx1,dim=2) /= size(mtx2,dim=1)) stop "input array size not match"
m=size(mtx1,dim=1)
n=size(mtx2,dim=2)
allocate(mtx3(m,n))
mtx3=matmul(mtx1,mtx2)
end subroutine
subroutine MixMatrixMultiply(mtx1,mtx2,mtx3)
real,intent(in) :: mtx1(:,:),mtx2(:,:)
real,intent(out),allocatable :: mtx3(:,:)
integer :: m,n
integer :: i,j
if(size(mtx1,dim=2) /= size(mtx2,dim=1)) stop "input array size not match"
m=size(mtx1,dim=1)
n=size(mtx2,dim=2)
allocate(mtx3(m,n))
do i=1,m
do j=1,n
mtx3(i,j)=dot_product(mtx1(i,:),mtx2(:,j))
end do
end do
end subroutine
end module
program main
use MatrixMultiplyModule
implicit none
real,allocatable :: a(:,:),b(:,:)
real,allocatable :: c1(:,:),c2(:,:),c3(:,:)
integer :: n
integer :: count, rate
real :: timeAtStart, timeAtEnd
real :: time(3,10)
do n=100,1000,100
allocate(a(n,n),b(n,n))
call random_number(a)
call random_number(b)
call system_clock(count = count, count_rate = rate)
timeAtStart = count / real(rate)
call LoopMatrixMultiply(a,b,c1)
call system_clock(count = count, count_rate = rate)
timeAtEnd = count / real(rate)
time(1,n/100)=timeAtEnd-timeAtStart
call system_clock(count = count, count_rate = rate)
timeAtStart = count / real(rate)
call IntrinsicMatrixMultiply(a,b,c2)
call system_clock(count = count, count_rate = rate)
timeAtEnd = count / real(rate)
time(2,n/100)=timeAtEnd-timeAtStart
call system_clock(count = count, count_rate = rate)
timeAtStart = count / real(rate)
call MixMatrixMultiply(a,b,c3)
call system_clock(count = count, count_rate = rate)
timeAtEnd = count / real(rate)
time(3,n/100)=timeAtEnd-timeAtStart
deallocate(a,b)
end do
open(1,file="time.txt")
do n=1,10
write(1,*) time(:,n)
end do
close(1)
deallocate(c1,c2,c3)
end program
答案 0 :(得分:5)
循环数组元素时应注意以下几点:
确保内部循环超过内存中的连续元素。在当前的“循环”算法中,内部循环超过索引k。由于矩阵在内存中作为列布局(第一个索引在通过内存时变化最快),因此访问新值k可能需要将新页面加载到缓存中。在这种情况下,您可以通过将循环重新排序为:
来优化算法do j=1,n
do k=1,size(mtx1,dim=2)
do i=1,m
mtx3(i,j)=mtx3(i,j)+mtx1(i,k)*mtx2(k,j)
end do
end do
end do
现在,内部循环遍历内存中的连续元素(mtx2(k,j)
值可能在编译器内部循环之前只被提取一次,如果不是,则可以在循环之前将其存储在临时变量中)
确保整个循环可以放入缓存中,以避免过多的缓存未命中。这可以通过阻止算法来完成。在这种情况下,解决方案可以是例如:
l=size(mtx1,dim=2)
ichunk=512 ! I have a 3MB cache size (real*4)
do jj=1,n,ichunk
do kk=1,l,ichunk
do j=jj,min(jj+ichunk-1,n)
do k=kk,min(kk+ichunk-1,l)
do i=1,m
mtx3(i,j)=mtx3(i,j)+mtx1(i,k)*mtx2(k,j)
end do
end do
end do
end do
end do
在这种情况下,性能取决于ichunk
的大小,特别是对于足够大的矩阵(你甚至可以阻止内循环,这只是一个例子)。
确保执行循环所需的工作比循环内部的工作小得多。这可以通过'循环展开'来解决,即在循环的一次迭代中组合几个语句。通常,编译器可以通过提供标志-funroll-loops
。
如果我使用上面的代码并使用标记-O3 -funroll-loops
进行编译,那么性能会比使用matmul
稍微好一点。
要记住这三个重要的事情是关于循环排序的第一点,因为这会影响其他用例中的性能,并且编译器通常无法解决这个问题。循环展开,你可以留给编译器(但测试它,因为这并不总是提高性能)。至于第二点,由于这取决于硬件,你不应该(通常)尝试自己实现一个非常有效的矩阵乘法,而是考虑使用诸如atlas,可以优化缓存大小,或供应商库,如MKL或ACML。