基准矩阵乘法性能:C++(eigen)比Python慢很多

Benchmarking matrix multiplication performance: C++ (eigen) is much slower than Python

我正在尝试估计 Python 性能与 C++ 相比有多好。

这是我的 Python 代码:

a=np.random.rand(1000,1000) #type is automaically float64
b=np.random.rand(1000,1000) 
c=np.empty((1000,1000),dtype='float64')

%timeit a.dot(b,out=c)

#15.5 ms ± 560 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

这是我在发布机制中使用 Xcode 编译的 C++ 代码:

#include <iostream>
#include <Dense>
#include <time.h>

using namespace Eigen;
using namespace std;

int main(int argc, const char * argv[]) {
    //RNG generator
    unsigned int seed = clock();
    srand(seed);

    int Msize=1000, Nloops=10;

    MatrixXd m1=MatrixXd::Random(Msize,Msize);
    MatrixXd m2=MatrixXd::Random(Msize,Msize);
    MatrixXd m3=MatrixXd::Random(Msize,Msize);

    cout << "Starting matrix multiplication test with " << Msize << 
    "matrices" << endl;
    clock_t start=clock();
    for (int i=0; i<Nloops; i++)
        m3=m1*m2;
    start = clock() - start;

    cout << "time elapsed for 1 multiplication: " << start / ((double) 
CLOCKS_PER_SEC * (double) Nloops) << " seconds" <<endl;
return 0;

}

结果是:

Starting matrix multiplication test with 1000matrices
time elapsed for 1 multiplication: 0.148856 seconds
Program ended with exit code: 0

这意味着 C++ 程序要慢 10 倍。

或者,我尝试在 MAC 终端中编译 cpp 代码:

g++ -std=c++11 -I/usr/local/Cellar/eigen/3.3.5/include/eigen3/eigen main.cpp -o my_exec -O3

./my_exec

Starting matrix multiplication test with 1000matrices
time elapsed for 1 multiplication: 0.150432 seconds

我知道非常相似 question,但是,问题似乎出在矩阵定义中。在我的示例中,我使用默认特征函数从均匀分布创建矩阵。

谢谢, 米哈伊尔

编辑:我发现,虽然 numpy 使用多线程,但 Eigen 默认不使用多线程(由 Eigen::nbThreads() 函数检查)。 正如建议的那样,我使用 -march=native 选项将计算时间减少了 3 倍。考虑到我的 MAC 上有 8 个线程可用,我相信多线程 numpy 的运行速度快了 3 倍。

经过漫长而痛苦的安装和编译,我已经在 Matlab、C++ 和 Python 中执行了基准测试。

我的电脑:MAC OS High Sierra 10.13.6 with Intel(R) Core(TM) i7-7920HQ CPU @ 3.10GHz(4 核,8 线程) .我有 Radeon Pro 560 4096 MB,因此这些测试中没有涉及 GPU(而且我从未配置过 openCL,也没有在 np.show_config() 中看到它)。

软件: Matlab 2018a,Python 3.6,C++ 编译器:Apple LLVM 版本 9.1.0 (clang-902.0.39.2),g++-8 (Homebrew GCC 8.2.0) 8.2.0

1) Matlab 性能:时间= (14.3 +- 0.7 ) ms,执行 10 次运行

a=rand(1000,1000);
b=rand(1000,1000);
c=rand(1000,1000);
tic
for i=1:100
    c=a*b;
end
toc/100

2) Python 表现 (%timeit a.dot(b,out=c)): 15.5 +- 0.8

我还为 python 安装了 mkl 库。使用 numpy linked against mkl: 14.4+-0.7 - 它有帮助,但非常小。

3) C++ 性能。应用了对原始(参见问题)代码的以下更改:

  • noalias 函数以避免创建不必要的时间矩阵。

  • 时间是用 c++11 chorno 库测量的

这里我使用了一堆不同的选项和两个不同的编译器:

3.1 clang++ -std=c++11 -I/usr/local/Cellar/eigen/3.3.5/include/eigen3/eigen main.cpp -O3

执行时间~ 146 毫秒

3.2 Added -march=native option:

执行时间~ 46 +-2 毫秒

3.3 Changed compiler to GNU g++ (in my mac it is called gpp by custom-defined alias):

gpp -std=c++11 -I/usr/local/Cellar/eigen/3.3.5/include/eigen3/eigen main.cpp -O3

执行时间 222 毫秒

3.4 Added - march=native option:

执行时间 ~ 45.5 +- 1 毫秒

到这里我才明白Eigen并没有使用多线程。我安装了 openmp 并添加了 -fopenmp 标志。请注意,在最新的 clang 版本上,openmp 不起作用,因此我必须从现在开始使用 g++。我还通过监视 Eigen::nbthreads() 的值和使用 MAC OS activity monitor.

确保我实际上使用了所有可用线程
3.5  gpp -std=c++11 -I/usr/local/Cellar/eigen/3.3.5/include/eigen3/eigen main.cpp -O3 -march=native -fopenmp

执行时间:16.5 +- 0.7 毫秒

3.6 最后,我安装了 Intel mkl 库。在代码中使用它们非常容易:我刚刚添加了 #define EIGEN_USE_MKL_ALL 宏,仅此而已。但是 link 所有的库都很难:

gpp -std=c++11 -DMKL_LP64 -m64 -I${MKLROOT}/include -I/usr/local/Cellar/eigen/3.3.5/include/eigen3/eigen -L${MKLROOT}/lib -Wl,-rpath,${MKLROOT}/lib -lmkl_intel_ilp64 -lmkl_intel_thread -lmkl_core -liomp5 -lpthread -lm -ldl   main.cpp -o my_exec_intel -O3 -fopenmp  -march=native

执行时间:14.33 +-0.26 毫秒。 (编者注:这个回答原来声称用了-DMKL_ILP64也就是not supported。可能曾经是,也可能是刚好起作用。)

结论:

  • Python/Matlab 中的矩阵乘法得到了高度优化。不可能(或者至少很难)做得更好(在 CPU)。

  • CPP 代码(至少在 MAC 平台上)只有在完全优化的情况下才能达到类似的性能,其中包括全套优化选项和 Intel mkl 库。 我本可以安装支持 openmp 的旧 clang 编译器,但由于单线程性能相似(~46 毫秒),看起来这无济于事。

  • 最好用原生英特尔编译器试试icc。不幸的是,这是专有软件,与英特尔 mkl 库不同。

感谢您的有益讨论,

米哈伊尔

编辑:为了进行比较,我还使用 cublasDgemm 函数对我的 GTX 980 GPU 进行了基准测试。计算时间 = 12.6 毫秒,与其他 results. 兼容 CUDA 几乎与 CPU 一样好的原因如下:我的 GPU 对双精度优化不佳。使用浮点数,GPU 时间 =0.43 毫秒,而 Matlab 的是 7.2 毫秒

编辑 2:为了获得显着的 GPU 加速,我需要对尺寸更大的矩阵进行基准测试,例如10k x 10k

编辑 3:将接口从 MKL_ILP64 更改为 MKL_LP64,因为 ILP64 是 not supported.