【问题标题】:Performance: Matlab vs C++ Matrix vector multiplication性能:Matlab vs C++ 矩阵向量乘法
【发布时间】:2018-03-24 15:56:46
【问题描述】:

序言

前段时间我问了一个关于 Matlab 与 Python 性能的问题 (Performance: Matlab vs Python)。我很惊讶 Matlab 比 Python 快,尤其是在 meshgrid 中。在讨论该问题时,有人指出我应该在 Python 中使用包装器来调用我的 C++ 代码,因为我也可以使用 C++ 代码。我在 C++、Matlab 和 Python 中有相同的代码。

在这样做的同时,我再次惊讶地发现,Matlab 在矩阵汇编计算方面比 C++ 更快。我有一个稍大的代码,我正在研究一段矩阵-向量乘法。较大的代码在多个实例中执行这样的乘法。总体而言,C++ 中的代码比 Matlab 快得多(因为在 Matlab 中调用函数有开销等),但在矩阵向量乘法方面,Matlab 似乎优于 C++(底部的代码 sn-p)。

结果

下表显示了组装内核矩阵所需的时间以及将矩阵与向量相乘所需的时间的比较。结果针对矩阵大小 NxN 进行编译,其中 N 从 10,000 到 40,000 不等。这不是那么大。但有趣的是,N 越大,Matlab 的性能就优于 C++。 Matlab 总时间快 3.8 - 5.8 倍。此外,它在矩阵组装计算方面也更快。

 ___________________________________________
|N=10,000   Assembly    Computation  Total  |
|MATLAB     0.3387      0.031        0.3697 |
|C++        1.15        0.24         1.4    |
|Times faster                        3.8    |
 ___________________________________________ 
|N=20,000   Assembly    Computation  Total  |
|MATLAB     1.089       0.0977       1.187  |
|C++        5.1         1.03         6.13   |
|Times faster                        5.2    |
 ___________________________________________
|N=40,000   Assembly    Computation  Total  |
|MATLAB     4.31        0.348        4.655  |
|C++        23.25       3.91         27.16  |
|Times faster                        5.8    |
 -------------------------------------------

问题

在 C++ 中有更快的方法吗?我错过了什么吗?我知道 C++ 正在使用 for 循环,但我的理解是 Matlab 也会在 meshgrid 中做类似的事情。

代码片段

Matlab 代码:

%% GET INPUT DATA FROM DATA FILES ------------------------------------------- %
% Read data from input file
Data       = load('Input/input.txt');
location   = Data(:,1:2);           
charges    = Data(:,3:end);         
N          = length(location);      
m          = size(charges,2);       

%% EXACT MATRIX VECTOR PRODUCT ---------------------------------------------- %
kex1=ex1; 
tic
Q = kex1.kernel_2D(location , location);
fprintf('\n Assembly time: %f ', toc);

tic
potential_exact = Q * charges;
fprintf('\n Computation time: %f \n', toc);

类(使用网格):

classdef ex1
    methods 
        function [kernel] = kernel_2D(obj, x,y) 
            [i1,j1] = meshgrid(y(:,1),x(:,1));
            [i2,j2] = meshgrid(y(:,2),x(:,2));
            kernel = sqrt( (i1 - j1) .^ 2 + (i2 - j2) .^2 );
        end
    end       
end

C++ 代码:

编辑

使用带有以下标志的 make 文件编译:

CC=g++ 
CFLAGS=-c -fopenmp -w -Wall -DNDEBUG -O3 -march=native -ffast-math -ffinite-math-only -I header/ -I /usr/include 
LDFLAGS= -g -fopenmp  
LIB_PATH= 

SOURCESTEXT= src/read_Location_Charges.cpp 
SOURCESF=examples/matvec.cpp
OBJECTSF= $(SOURCESF:.cpp=.o) $(SOURCESTEXT:.cpp=.o)
EXECUTABLEF=./exec/mykernel
mykernel: $(SOURCESF) $(SOURCESTEXT) $(EXECUTABLEF)
$(EXECUTABLEF): $(OBJECTSF)
    $(CC) $(LDFLAGS) $(KERNEL) $(INDEX) $(OBJECTSF) -o $@ $(LIB_PATH)
.cpp.o:
    $(CC) $(CFLAGS) $(KERNEL) $(INDEX) $< -o $@

`

# include"environment.hpp"
using namespace std;
using namespace Eigen;

class ex1 
{
public:
    void kernel_2D(const unsigned long M, double*& x, const unsigned long N,  double*&  y, MatrixXd& kernel)    {   
        kernel  =   MatrixXd::Zero(M,N);
        for(unsigned long i=0;i<M;++i)  {
            for(unsigned long j=0;j<N;++j)  {
                        double X =   (x[0*N+i] - y[0*N+j]) ;
                        double Y =   (x[1*N+i] - y[1*N+j]) ;
                        kernel(i,j) = sqrt((X*X) + (Y*Y));
            }
        }
    }
};

int main()
{
    /* Input ----------------------------------------------------------------------------- */
    unsigned long N = 40000;          unsigned m=1;                   
    double* charges;                  double* location;
    charges =   new double[N * m]();  location =    new double[N * 2]();
    clock_t start;                    clock_t end;
    double exactAssemblyTime;         double exactComputationTime;

    read_Location_Charges ("input/test_input.txt", N, location, m, charges);

    MatrixXd charges_           =   Map<MatrixXd>(charges, N, m);
    MatrixXd Q;
    ex1 Kex1;

    /* Process ------------------------------------------------------------------------ */
    // Matrix assembly
    start = clock();
        Kex1.kernel_2D(N, location, N, location, Q);
    end = clock();
    exactAssemblyTime = double(end-start)/double(CLOCKS_PER_SEC);

    //Computation
    start = clock();
        MatrixXd QH = Q * charges_;
    end = clock();
    exactComputationTime = double(end-start)/double(CLOCKS_PER_SEC);

    cout << endl << "Assembly     time: " << exactAssemblyTime << endl;
    cout << endl << "Computation time: " << exactComputationTime << endl;


    // Clean up
    delete []charges;
    delete []location;
    return 0;
}

【问题讨论】:

  • 您应该放置用于编译 C++ 代码的标志
  • 您初始化矩阵的方式可以明显得到改进。首先,不要调用 ::Zero,你是在浪费时间初始化一切。其次,尝试查看矩阵是按行优先还是列优先顺序存储的。如果它是列主要的,则内部循环应该在每一行上进行迭代!
  • MATLAB 使用BLAS/LAPACK 可能在Intel MKL 中实现。请参阅here 了解一堆有用的背景信息。类似的性能在 C++ 中并非不可能实现,但肯定不是微不足道的。
  • 另请注意:-ffast-math"accurate-math",MATLAB 不会做出妥协
  • 如前所述,Matlab 在 BLAS 下使用专门的子程序,这些子程序针对矩阵/向量运算进行了高度优化。要真正比较使用 C++ 的性能,您可以尝试使用 Armadillo,它是用 C++ 编写并使用 BLAS。

标签: c++ matlab performance matrix eigen


【解决方案1】:

正如 cmets 中所说,MatLab 依赖英特尔的 MKL 库来处理矩阵产品,这是此类操作最快的库。尽管如此,仅 Eigen 就应该能够提供类似的性能。为此,请确保使用最新的 Eigen(例如 3.4)和正确的编译标志以启用 AVX/FMA(如果可用)和多线程:

-O3 -DNDEBUG -march=native

由于charges_ 是一个向量,最好使用VectorXd,Eigen 知道您需要矩阵向量乘积而不是矩阵矩阵乘积。

如果你有 Intel 的 MKL,那么你也可以让 Eigenuses it 获得与 MatLab 完全相同的性能来进行这种精确操作。

关于程序集,最好反转两个循环以启用矢量化,然后使用 OpenMP 启用多线程(添加 -fopenmp 作为编译器标志)以使最外层循环并行运行,最后您可以使用 Eigen 简化代码:

void kernel_2D(const unsigned long M, double* x, const unsigned long N,  double*  y, MatrixXd& kernel)    {
    kernel.resize(M,N);
    auto x0 = ArrayXd::Map(x,M);
    auto x1 = ArrayXd::Map(x+M,M);
    auto y0 = ArrayXd::Map(y,N);
    auto y1 = ArrayXd::Map(y+N,N);
    #pragma omp parallel for
    for(unsigned long j=0;j<N;++j)
      kernel.col(j) = sqrt((x0-y0(j)).abs2() + (x1-y1(j)).abs2());
}

使用多线程,您需要测量挂钟时间。在这里(Haswell 有 4 个物理内核以 2.6GHz 运行)对于 N=20000,组装时间下降到 0.36 秒,矩阵向量乘积需要 0.24 秒,因此总共需要 0.6 秒,这比 MatLab 快,而我的 CPU 似乎更慢比你的。

【讨论】:

  • 感谢您的回答。添加march=native 不会做任何事情。关于VectorXd 的建议使它更快,但仍然比Matlab 慢。我尝试了你建议的代码 sn-p,但我收到了错误。拳头是auto,我把它改成了ArrayXd。但是我在编译时遇到的主要错误是undefined reference to omp_get_num_threads 我用我的源文件做了#include omp.h 并添加了标志-fopenmp。还是不行你能帮我实现吗?谢谢
  • 没关系,经过一番谷歌搜索后,我也通过添加LDFLAGS=-g -fopenmp 使其工作。我已将部分制作文件放入问题中。它现在正在工作,但我看不到时间上有任何变化。不幸的是,它们是一样的。还有其他建议吗?
  • 不,您不能将 auto 替换为 ArrayXd,因为这将执行深层复制(昂贵)。您应该通过在 CFLAGS 中添加 -std=c++11 来启用 c++11 支持。然后,正如我所说,您需要测量 wall-time(不是 CPU 时间),例如使用 c++11 std::chrono
  • 感谢您的 cmets。我已经做出了改变,它对装配时间产生了巨大的影响。至于计算时间,还是比 Matlab 慢。对于 N=20k,计算时间 =~0.30,对于 N=40k,它是~1.0 秒。有趣的是,在这些修改之前,使用 VectorXd 使计算速度更快,但现在看来 MatrixXd 使计算速度更快。至少整体计算速度更快。然而,Matlab 的计算仍然要快得多......
  • 是的,对于计算步骤,Matllab 在后台使用 Intel 的 MKL,它具有多线程矩阵向量产品,而 Eigen 中只有矩阵矩阵产品是多线程的。如果你有 openblas,你可以尝试用-DEIGEN_USE_BLAS 编译并链接到-lblas,看看他们的矩阵向量实现是否与 MKL 竞争(如果你有 MKL,你也可以用同样的方式将 Eigen 链接到 MKL 的 blas) .
【解决方案2】:

您可能有兴趣查看 MATLAB Central 的贡献 mtimesx

Mtimesx 是一个 mex 函数,它使用 BLAS 库、openMP 和其他方法优化矩阵乘法。以我的经验,当它最初发布时,在某些情况下它可能会比现有的 MATLAB 高出 3 个数量级。 (我想 MATHWORKS 有点尴尬。)这些天来,MATLAB 改进了自己的方法(我怀疑是从这里借来的。)并且差异不那么严重。 MATLAB 有时会胜过它。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-01-02
    • 2021-12-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-07-30
    • 2015-10-12
    • 2020-10-29
    相关资源
    最近更新 更多