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

时间:2016-04-10 06:39:18

标签: python c++ numpy optimization numba

在我正在研究的程序中,我需要重复乘以两个矩阵。由于其中一个矩阵的大小,这个操作需要一些时间,我想看看哪种方法最有效。矩阵的维度为(m x n)*(n x p),其中m = n = 310^5 < p < 10^6

除了Numpy,我假设它使用优化算法,每个测试都包含matrix multiplication的简单实现:

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

numpy的

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

Numba

代码与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)是此类的友元函数。它使用来自QPF的{​​{1}}和QPC进行计时,并使用带有Windows.h命令的MinGW编译程序。同样,保留了从10次执行中获得的最佳时间。

结果

Results

正如所料,简单的Python代码速度较慢,但​​对于非常小的矩阵,它仍然胜过Numpy。对于最大的病例,Numba比Numpy快约30%。

我对C ++结果感到惊讶,其中乘法比Numba花费的时间多了几个数量级。事实上,我预计这些时间会相似。

这导致了我的主要问题:这是正常的,如果没有,为什么C ++比Numba慢?我刚开始学习C ++,所以我可能做错了。如果是这样,那么我的错误是什么,或者我可以做些什么来提高我的代码效率(除了选择更好的算法)?

编辑1

以下是g++类的标题。

mat2

修改2

许多人建议,使用优化标志是与Numba相匹配的缺失元素。以下是与之前的曲线相比的新曲线。标记为#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 的曲线是通过切换两个内环获得的,并显示另外30%至50%的改善。

Results v2

2 个答案:

答案 0 :(得分:6)

绝对使用-O3进行优化。这会打开vectorizations,这会显着加快您的代码速度。

Numba应该已经这样做了。

答案 1 :(得分:0)

您仍然可以通过改善内存访问来优化这些循环,您的功能可能看起来像(假设矩阵为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显然一起执行与以前相同的方式,对j和k保持相同,但是这次可以将A和B中CSxCS大小的区域保留在缓存中(我猜)并且可以再使用一次。

您可以玩CS和NCHUNKS。对我而言,CS = 10和NCHUNKS = 100效果很好。使用numba.jit时,它将代码从7s加速到850 ms(请注意,我使用1000x1000,上面的图形以3x3x10 ^ 5运行,因此有点另类)。