Fortran矩阵乘法在不同优化中的表现

时间:2013-03-22 22:03:32

标签: performance fortran matrix-multiplication

我正在阅读“使用Fortran进行科学软件开发”一书,其中有一个练习,我觉得非常有趣:

“创建一个名为MatrixMultiplyModule的Fortran模块。添加三个子程序,称为LoopMatrixMultiply,IntrinsicMatrixMultiply和MixMatrixMultiply。每个例程应该将两个实矩阵作为参数,执行矩阵乘法,并通过第三个参数返回结果.LoopMatrixMultiply应该完全用do循环编写,没有数组操作或内部过程; IntrinsicMatrixMultiply应该使用matmul内部函数编写;而MixMatrixMultiply应该使用一些do循环和内部函数dot_product编写。编写一个小程序来测试这些函数的性能对不同大小的矩阵执行矩阵乘法的三种不同方法。“

我对两个秩2矩阵的乘法进行了一些测试,这里是不同优化标志下的结果:

enter image description here enter image description here enter image description here

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

1 个答案:

答案 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。