为什么犰狳矩阵计算比Fortran慢

时间:2017-12-21 07:10:03

标签: c++ fortran armadillo

我尝试使用Armadillo库中的矩阵实现将代码从Fortran重写为C ++。两个代码的结果相同,但C ++代码比Fortran慢(> 10x)。代码涉及小矩阵(2x2,4x4)逆,乘法和加法。我在这里放了一部分相似的代码进行测试。

============================

clang++ cplusplus.cc -o cplusplus --std=c++14 -larmadillo -O2

ifort fort.f90 -o fort -O2

C ++代码时间:0.39404s

Fortran代码时间:0.068s

============================

C ++代码:

#include <armadillo>
#include <iostream>

int main()
{
  const int niter = 1580000;
  const int ns = 3;
  arma::cx_cube m1(2, 2, ns), m2(2, 2, ns), m3(2, 2, ns);
  arma::wall_clock timer;
  timer.tic();
  for (auto i=0; i<niter; ++i) {
    for (auto j=0; j<ns; ++j)
      m1.slice(j) += m2.slice(j) * m3.slice(j);
  }
  double n = timer.toc();
  std::cout << "time: " << n << "s" << std::endl;
  return 0;
}

Fortran代码:

program main
  implicit none
  integer, parameter :: ns = 3, niter = 1580000
  complex*16 m1(2, 2, ns), m2(2, 2, ns), m3(2, 2, ns)
  integer i, j
  real :: start, finish
  call cpu_time(start)
  do i = 1, niter
     do j = 1, ns
        m1(1, 1, j) = m1(1, 1, j) + m2(1, 1, j) * m3(1, 1, j) + m2(1, 2, j) * m3(2, 1, j)
        m1(1, 2, j) = m1(1, 2, j) + m2(1, 1, j) * m3(1, 2, j) + m2(1, 2, j) * m3(2, 2, j)
        m1(2, 1, j) = m1(2, 1, j) + m2(2, 1, j) * m3(1, 1, j) + m2(2, 2, j) * m3(2, 1, j)
        m1(2, 2, j) = m1(2, 2, j) + m2(2, 1, j) * m3(1, 2, j) + m2(2, 2, j) * m3(2, 2, j)
     end do
  end do
  call cpu_time(finish)
  print *, "time: ", finish-start, " s"

end program main

=============================================== =====================

关注@ewcz @ user5713492建议

============================

clang++ cplusplus.cc -o cplusplus --std=c++14 -larmadillo -O2

ifort fort.f90 -o fort -O2

ifort fort2.f90 -o fort2 -O2

C ++代码(cplusplus.cc)时间:0.39650s

Fortran代码(fort.f90)(显式操作)时间:0.020s

Fortran代码(fort2.f90)(matmul)时间:0.064s

============================

C ++代码(cplusplus.cc):

#include <armadillo>
#include <iostream>
#include <complex>

int main()
{
  const int niter = 1580000;
  const int ns = 3;
  arma::cx_cube m1(2, 2, ns, arma::fill::ones),
    m2(2, 2, ns, arma::fill::ones),
    m3(2, 2, ns,arma::fill::ones);
  std::complex<double> result;
  arma::wall_clock timer;
  timer.tic();
  for (auto i=0; i<niter; ++i) {
    for (auto j=0; j<ns; ++j)
      m1.slice(j) += m2.slice(j) * m3.slice(j);
  }

  double n = timer.toc();
  std::cout << "time: " << n << "s" << std::endl;
  result = arma::accu(m1);
  std::cout << result << std::endl;
  return 0;
}

Fortran代码(fort.f90):

program main
  implicit none
  integer, parameter :: ns = 3, niter = 1580000
  complex*16 m1(2, 2, ns), m2(2, 2, ns), m3(2, 2, ns)
  integer i, j
  complex*16 result
  real :: start, finish
  m1 = 1
  m2 = 1
  m3 = 1
  call cpu_time(start)
  do i = 1, niter
     do j = 1, ns
        m1(1, 1, j) = m1(1, 1, j) + m2(1, 1, j) * m3(1, 1, j) + m2(1, 2, j) * m3(2, 1, j)
        m1(1, 2, j) = m1(1, 2, j) + m2(1, 1, j) * m3(1, 2, j) + m2(1, 2, j) * m3(2, 2, j)
        m1(2, 1, j) = m1(2, 1, j) + m2(2, 1, j) * m3(1, 1, j) + m2(2, 2, j) * m3(2, 1, j)
        m1(2, 2, j) = m1(2, 2, j) + m2(2, 1, j) * m3(1, 2, j) + m2(2, 2, j) * m3(2, 2, j)
     end do
  end do
  call cpu_time(finish)
  result = sum(m1)
  print *, "time: ", finish-start, " s"
  print *, result

end program main

Fortran代码(fort2.f90):

program main
  implicit none
  integer, parameter :: ns = 3, niter = 1580000
  complex*16 m1(2, 2, ns), m2(2, 2, ns), m3(2, 2, ns)
  integer i, j
  complex*16 result
  real :: start, finish
  m1 = 1
  m2 = 1
  m3 = 1
  call cpu_time(start)
  do i = 1, niter
     do j = 1, ns
        m1(:,:,j) = m1(:,:,j)+matmul(m2(:,:,j),m3(:,:,j))
     end do
  end do
  call cpu_time(finish)
  result = sum(m1)
  print *, "time: ", finish-start, " s"
  print *, result

end program main

=============================================== =======================

复数可能是犰狳如此缓慢的原因之一。如果我在C ++中使用arma::cube而不是arma::cx_cube并在Fortran中使用real*8,则时间是:

C ++代码时间:0.08s

Fortran代码(fort.f90)(显式操作)时间:0.012s

Fortran代码(fort2.f90)(matmul)时间:0.028s

但是,我的计算需要复数。这很奇怪 对于犰狳图书馆来说,计算时间增加得非常大,但对于Fortran而言则略有增加。

2 个答案:

答案 0 :(得分:3)

你不能在gfortran中计算任何东西。它可以在-O2级看到您不使用m1的值,因此它完全跳过计算。同样在Fortran中,你的数组是未初始化的,因此你可以使用NaN进行计算,这可能会大大减慢速度。

因此,您应该初始化数组并使用某种输入,如命令行,用户输入或文件内容,以便编译器无法预先计算结果。

然后您可以考虑将Fortran中的循环内容更改为

m1(:,:,j) = m1(:,:,j)+matmul(m2(:,:,j),m3(:,:,j))

以便与C ++的东西保持一致。 (gfortran在做这件事时似乎放慢了很多但是ifort对它很满意。)

然后你必须在最后打印出你的数组,这样编译器就不会得出结论,你正在计时的循环可以像gfortran那样被跳过。编辑修复程序,让我们了解新结果。

答案 1 :(得分:2)

我想说你的Fortran版本在这个特定的例子中从显式扩展到基本操作中获得了显着的利润。为了证明这一点,我们假设有以下修改:

  implicit none
  integer, parameter :: ns = 3, niter = 1580000
  complex*16 m1(2, 2, ns), m2(2, 2, ns), m3(2, 2, ns)
  integer i, j
  real :: start, finish
  call cpu_time(start)
  m2 = 1
  m3 = 1
  do i = 1, niter
     do j = 1, ns
        !m1(1, 1, j) = m1(1, 1, j) + m2(1, 1, j) * m3(1, 1, j) + m2(1, 2, j) * m3(2, 1, j)
        !m1(1, 2, j) = m1(1, 2, j) + m2(1, 1, j) * m3(1, 2, j) + m2(1, 2, j) * m3(2, 2, j)
        !m1(2, 1, j) = m1(2, 1, j) + m2(2, 1, j) * m3(1, 1, j) + m2(2, 2, j) * m3(2, 1, j)
        !m1(2, 2, j) = m1(2, 2, j) + m2(2, 1, j) * m3(1, 2, j) + m2(2, 2, j) * m3(2, 2, j)
        m1(:, :, j) = m1(:, :, j) + MATMUL(m2(:, :, j), m3(:, :, j))
     end do
  end do
  WRITE(*, *) SUM(m1)
  call cpu_time(finish)
  print *, "time: ", finish-start, " s"

这里,最后,程序打印m1的总和,以便至少部分确定整个循环没有被消除。使用显式乘法(和-O2),我得到大约0.05s的运行时间,而一般MATMUL它大约是0.2s,即类似于犰狳方法...

另外,即使Armadillo基于模板很多,所以很多函数调用关于通过slice()创建子多维数据集视图可能会被淘汰,你仍然原则上有一些开销与Fortran,你直接操纵连续的记忆。