在我正在编写的一个程序中,我需要重复地将两个矩阵相乘。由于其中一个矩阵的大小,这个操作需要一些时间,我想看看哪种方法最有效。矩阵的维数为(m x n)*(n x p)
,其中m = n = 3
和10^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
中的QPF
和QPC
进行计时,并使用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%的改进。
5条答案
按热度按时间pbwdgjma1#
使用
-O3
进行优化。这将打开vectorizations,这将显著提高代码的速度。Numba应该已经这样做了。
yk9xbfzb2#
我的建议是
如果你想要最大的效率,你应该使用一个专用的线性代数库,其中最经典的是BLAS/LAPACK库。有许多实现,例如。Intel MKL.你写的东西不会超过超优化的库。
矩阵矩阵乘法将是
dgemm
例程:d代表double,ge代表general,mm代表matrix multiply。如果你的问题有额外的结构,一个更具体的函数可能会被调用以获得额外的加速。注意,Numpy点ALREADY调用
dgemm
!你可能不会做得更好。为什么你的C++很慢
你的经典的,直观的矩阵-矩阵乘法算法与可能的算法相比是很慢的。编写代码,利用处理器如何缓存等。产生重要的性能增益。关键是,无数聪明人都致力于使矩阵矩阵乘法极快,你应该使用他们的工作,而不是重新发明轮子。
ioekq8ef3#
在当前的实现中,编译器很可能无法自动向量化最内层的循环,因为它的大小为3。
m2
也是以一种“跳跃”的方式访问的。交换循环,使p
的迭代在最内部的循环中进行,这将使它工作得更快(col
不会进行“跳跃”的数据访问),编译器应该能够做得更好(自动向量化)。在我的机器上,p=10^6元素的原始C++实现使用
g++ dot.cpp -std=c++11 -O3 -o dot
标志构建需要12ms
,而上面的实现使用交换循环需要7ms
。pftdvrlh4#
你仍然可以通过改善内存访问来优化这些循环,你的函数可能看起来像这样(假设矩阵是1000x1000):
说明:循环i和ii显然一起执行与i之前执行的相同方式,对于j和k也是如此,但是这次A和B中大小为CS × CS的区域可以保存在该高速缓存中(我猜),并且可以使用多于一次。
你可以玩CS和NCHUNKS。对于我来说,CS=10和NCHUNKS=100工作得很好。当使用numba.jit时,它将代码从7秒加速到850毫秒(注意我使用1000x1000,上面的图形是用3x3x10^5运行的,所以它有点像另一个场景)。
deyfvvtc5#
Numba默认使用多线程,将Numba与单线程C代码进行比较并不是一个公平的比较。我经常使用Numpy和Numba对科学应用程序进行快速原型设计,然后通过多线程将计算密集型部分移植到C,以获得更好的速度。有几个线程库- OpenMP、pthreads和MPI。我发现OpenMP简单有效地满足了我的需求。
在任何并行代码中,都必须注意竞争条件、错误共享以及关键或原子操作。一旦您理解了这些概念,利用OpenMP就非常简单了。