微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

Eigen 在使用显式循环的矩阵乘法中比 Fortran 慢得多

如何解决Eigen 在使用显式循环的矩阵乘法中比 Fortran 慢得多

我尝试通过 Eigen 库使用 2000*2000 矩阵乘法实现将代码从 Fortran 重写为 C++。我发现 Eigen 中的 for 循环比 Fortran 中的 do 循环慢得多(> 3 倍)。代码如下:

test.f90

program main
implicit none
integer :: n,i,j,k
integer :: tic,toc
real(8),ALLOCATABLE ::a(:,:),b(:,c(:,:)
real(8) :: s

n = 2000
allocate(a(n,n),b(n,c(n,n))
do i=1,n
    do j =1,n
        a(j,i) = i * 1.0
        b(j,i) = i * 1.0
    enddo
enddo

call system_clock(tic)
do j=1,n
    do i=1,n
        s = 0.0
        do k=1,n
            s = s + a(i,k) * b(k,j)
        enddo
        c(i,j) = s
    enddo
enddo
call system_clock(toc)
print*,'Fortran with loop:',(toc - tic) / 1000.0

call system_clock(tic)
c = matmul(a,b)
call system_clock(toc)
print*,'Fortran with matmul:',(toc - tic) / 1000.0


DEALLOCATE(a,b,c)
end

test.cpp

#include<Eigen/Core>
#include<time.h>
#include<iostream>
using Eigen::MatrixXd;

int main(){
    int n = 2000;
    MatrixXd a(n,n);
    for(int i=0;i<n;i++){
    for(int j=0;j<n;j++){
            a(i,j) = i * 1.0;
            b(i,j) = j * 1.0;
        }
    }
    clock_t tic,toc;
    tic = clock();
    for(int j=0;j<n;j++){
        for(int i=0;i<n;i++){
            double s= 0.0;
            for(int k=0;k<n;k++){
                s += a(i,j);
            }
            c(i,j) = s;
        }
    }
    toc = clock();
    std::cout << (double)((toc - tic)) / CLOCKS_PER_SEC << std::endl;

    tic = clock();
    c=  a * b;
    toc = clock();
    std::cout << (double)((toc - tic)) / CLOCKS_PER_SEC << std::endl;
}

编译者(使用 gcc-8.4,在 ubuntu-18.04 中)

gfortran test.f90 -O3 -march=native -o testf
g++ test.cpp -O3 -march=native -I/path/to/eigen -o testcpp 

我得到了结果:

Fortran with loop:   10.9700003
Fortran with matmul:   0.834999979
Eigen with loop: 38.2188
Eigen with *: 0.40625

内部实现的速度相当,但为什么 Eigen 对于循环实现要慢得多?

解决方法

循环的最大问题是它们没有按照 C++(应该是行优先)或 Fortran(应该是列优先)的正确顺序完成。这会给您带来很大的性能损失,尤其是对于大型矩阵。

John Alexiou 的 nativemul 实现(使用 dot_product)也有同样的问题,所以我很惊讶他声称它更快。 (我发现它不是;见下文。也许他的(英特尔?)编译器重写了代码以在内部使用 matmul。)

这是 Fortran 的正确循环顺序:

    c = 0
    do j=1,n
        do k=1,n
            do i=1,n
                c(i,j) = c(i,j) + a(i,k) * b(k,j)
            enddo
        enddo
    enddo

使用 gfortran 10.2.0 版,并使用 -O3 编译,我得到

 Fortran with original OP's loop:   53.5190010    
 Fortran with John Alexiou's nativemul:   53.4309998    
 Fortran with correct loop:   11.0679998    
 Fortran with matmul:   2.36999989    

一个正确的 C++ 循环应该会给你类似的性能。

显然 matmul/BLAS 对于大型矩阵要快得多。

,

在 Fortran 代码中,我看到了同样的问题,但后来我在子程序中移动了矩阵乘法,结果速度几乎与 matmul 一样好。我还和 BLAS Level 3 功能进行了比较。

Fortran with loop:   9.220000
Fortran with matmul:   8.450000
Fortran with blas3:   2.050000

以及生成它的代码

program ConsoleMatMul
use BLAS95
implicit none
integer :: n,i,j
integer :: tic,toc
real(8),ALLOCATABLE :: a(:,:),b(:,c(:,xe(:,:)

n = 2000
allocate(a(n,n),b(n,c(n,xe(n,n))
do i=1,n
    do j =1,n
        a(j,i) = i * 1.0
        b(j,i) = i * 1.0
    enddo
enddo

call system_clock(tic)
call nativemul(a,b,c)
call system_clock(toc)
print*,'Fortran with loop:',(toc - tic) / 1000.0

call system_clock(tic)
c = matmul(a,b)
call system_clock(toc)
print*,'Fortran with matmul:',(toc - tic) / 1000.0
c = b
xe = 0d0
call system_clock(tic)
call gemm(a,c,xe) ! BLAS MATRIX/MATRIX MUL
call system_clock(toc)
print*,'Fortran with blas3:',(toc - tic) / 1000.0

DEALLOCATE(a,c)

contains

pure subroutine nativemul(a,c)
real(8),intent(in),allocatable :: a(:,:)
real(8),intent(out),allocatable :: c(:,:)
real(8) :: s
integer :: n,j,k
    n = size(a,1)
    if (.not. allocated(c)) allocate(c(n,n))
    do j=1,n
        do i=1,n
            s = 0.0d0
            do k=1,n
                s = s + a(i,j)
            end do
            c(i,j) = s
        end do
    end do
end subroutine    

end program ConsoleMatMul

在我将代码移入子程序之前,我得到了

Fortran with loop:   85.450000

更新当内循环被 matmul 替换时,本机乘法达到 dot_product() 级(或超过它)。

pure subroutine nativemul(a,intent(in) :: a(:,intent(out) :: c(:,:)
integer :: n,j
    n = size(a,1)
    do j=1,n
            c(i,j) = dot_product(a(i,j))
            ! or  = sum(a(i,:)*b(:,j))
        end do
    end do
end subroutine    
,

C++ 前增量比后增量快...

for(int j=0;j<n;++j){
        for(int i=0;i<n;++i){
            double s= 0.0;
            for(int k=0;k<n;++k){
                s += a(i,j);
            }
            c(i,j) = s;
        }
    }

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。