比较Python,Numpy,Numba和C++的矩阵乘法

4nkexdtk  于 2023-10-19  发布在  Python
关注(0)|答案(5)|浏览(140)

在我正在编写的一个程序中,我需要重复地将两个矩阵相乘。由于其中一个矩阵的大小,这个操作需要一些时间,我想看看哪种方法最有效。矩阵的维数为(m x n)*(n x p),其中m = n = 310^5 < p < 10^6
除了Numpy,我假设它使用优化算法,每个测试都包含matrix multiplication的简单实现:

下面是我的各种实现:

Python

def dot_py(A,B):
    m, n = A.shape
    p = B.shape[1]

    C = np.zeros((m,p))

    for i in range(0,m):
        for j in range(0,p):
            for k in range(0,n):
                C[i,j] += A[i,k]*B[k,j] 
    return C

麻木

def dot_np(A,B):
    C = np.dot(A,B)
    return C

农巴

代码与Python代码相同,但在使用之前及时编译:

dot_nb = nb.jit(nb.float64[:,:](nb.float64[:,:], nb.float64[:,:]), nopython = True)(dot_py)

到目前为止,每个方法调用都使用timeit模块计时了10次。保持最佳结果。矩阵是使用np.random.rand(n,m)创建的。

C++

mat2 dot(const mat2& m1, const mat2& m2)
{
    int m = m1.rows_;
    int n = m1.cols_;
    int p = m2.cols_;

    mat2 m3(m,p);

    for (int row = 0; row < m; row++) {
        for (int col = 0; col < p; col++) {
            for (int k = 0; k < n; k++) {
                m3.data_[p*row + col] += m1.data_[n*row + k]*m2.data_[p*k + col];
            }
        }
    }

    return m3;
}

这里,mat2是我定义的一个自定义类,dot(const mat2& m1, const mat2& m2)是该类的友元函数。它使用Windows.h中的QPFQPC进行计时,并使用MinGW和g++命令编译程序。同样,从10次执行中获得的最佳时间被保留。

结果

正如预期的那样,简单的Python代码较慢,但对于非常小的矩阵,它仍然优于Numpy。在最大的情况下,Numba比Numpy快30%。
我对C的结果感到惊讶,乘法比Numba多花了几乎一个数量级的时间。事实上,我预计这些需要类似的时间。
这就引出了我的主要问题:这是正常的吗?如果不是,为什么C
比Numba慢?我刚开始学习C++,所以我可能做错了什么。如果是这样,我的错误是什么,或者我可以做些什么来提高代码的效率(除了选择一个更好的算法)?

编辑1

下面是mat2类的头文件。

#ifndef MAT2_H
#define MAT2_H

#include <iostream>

class mat2
{
private:
    int rows_, cols_;
    float* data_;

public: 
    mat2() {}                                   // (default) constructor
    mat2(int rows, int cols, float value = 0);  // constructor
    mat2(const mat2& other);                    // copy constructor
    ~mat2();                                    // destructor

    // Operators
    mat2& operator=(mat2 other);                // assignment operator

    float operator()(int row, int col) const;
    float& operator() (int row, int col);

    mat2 operator*(const mat2& other);

    // Operations
    friend mat2 dot(const mat2& m1, const mat2& m2);

    // Other
    friend void swap(mat2& first, mat2& second);
    friend std::ostream& operator<<(std::ostream& os, const mat2& M);
};

#endif

编辑2

正如许多人所建议的那样,使用优化标志是匹配Numba的缺失元素。下面是与以前的曲线相比的新曲线。标记为v2的曲线是通过切换两个内部循环获得的,并且显示出另外30%到50%的改进。

pbwdgjma

pbwdgjma1#

使用-O3进行优化。这将打开vectorizations,这将显著提高代码的速度。
Numba应该已经这样做了。

yk9xbfzb

yk9xbfzb2#

我的建议是

如果你想要最大的效率,你应该使用一个专用的线性代数库,其中最经典的是BLAS/LAPACK库。有许多实现,例如。Intel MKL.你写的东西不会超过超优化的库。
矩阵矩阵乘法将是dgemm例程:d代表double,ge代表general,mm代表matrix multiply。如果你的问题有额外的结构,一个更具体的函数可能会被调用以获得额外的加速。
注意,Numpy点ALREADY调用dgemm!你可能不会做得更好。

为什么你的C++很慢

你的经典的,直观的矩阵-矩阵乘法算法与可能的算法相比是很慢的。编写代码,利用处理器如何缓存等。产生重要的性能增益。关键是,无数聪明人都致力于使矩阵矩阵乘法极快,你应该使用他们的工作,而不是重新发明轮子。

ioekq8ef

ioekq8ef3#

在当前的实现中,编译器很可能无法自动向量化最内层的循环,因为它的大小为3。m2也是以一种“跳跃”的方式访问的。交换循环,使p的迭代在最内部的循环中进行,这将使它工作得更快(col不会进行“跳跃”的数据访问),编译器应该能够做得更好(自动向量化)。

for (int row = 0; row < m; row++) {
    for (int k = 0; k < n; k++) {
        for (int col = 0; col < p; col++) {
            m3.data_[p*row + col] += m1.data_[n*row + k] * m2.data_[p*k + col];
        }
    }
}

在我的机器上,p=10^6元素的原始C++实现使用g++ dot.cpp -std=c++11 -O3 -o dot标志构建需要12ms,而上面的实现使用交换循环需要7ms

pftdvrlh

pftdvrlh4#

你仍然可以通过改善内存访问来优化这些循环,你的函数可能看起来像这样(假设矩阵是1000x1000):

CS = 10
NCHUNKS = 100

def dot_chunked(A,B):
    C = np.zeros(1000,1000)

    for i in range(NCHUNKS):
        for j in range(NCHUNKS):
            for k in range(NCHUNKS):
                for ii in range(i*CS,(i+1)*CS):
                    for jj in range(j*CS,(j+1)*CS):
                        for kk in range(k*CS,(k+1)*CS):
                            C[ii,jj] += A[ii,kk]*B[kk,jj] 
    return C

说明:循环i和ii显然一起执行与i之前执行的相同方式,对于j和k也是如此,但是这次A和B中大小为CS × CS的区域可以保存在该高速缓存中(我猜),并且可以使用多于一次。
你可以玩CS和NCHUNKS。对于我来说,CS=10和NCHUNKS=100工作得很好。当使用numba.jit时,它将代码从7秒加速到850毫秒(注意我使用1000x1000,上面的图形是用3x3x10^5运行的,所以它有点像另一个场景)。

deyfvvtc

deyfvvtc5#

Numba默认使用多线程,将Numba与单线程C代码进行比较并不是一个公平的比较。我经常使用Numpy和Numba对科学应用程序进行快速原型设计,然后通过多线程将计算密集型部分移植到C,以获得更好的速度。有几个线程库- OpenMP、pthreads和MPI。我发现OpenMP简单有效地满足了我的需求。
在任何并行代码中,都必须注意竞争条件、错误共享以及关键或原子操作。一旦您理解了这些概念,利用OpenMP就非常简单了。

相关问题