Сравнение Python, Numpy, Numba и C++ для умножения матриц
в программе, над которой я работаю, мне нужно многократно умножить две матрицы. Из-за размера одной из матриц эта операция занимает некоторое время, и я хотел посмотреть, какой метод будет наиболее эффективным. Матрицы имеют размеры (m x n)*(n x p)
здесь m = n = 3
и 10^5 < p < 10^6
.
за исключением Numpy, который, как я предполагаю, работает с оптимизированным алгоритмом, каждый тест состоит из простой реализации матрица умножение:
Ниже приведены мои различные реализации:
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
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
и QPC
С Windows.h
и программа компилируется с помощью MinGW с . Опять же, лучшее время, полученное от 10 казней хранившийся.
результаты
как и ожидалось, простой код Python медленнее, но он все еще бьет Numpy для очень маленьких матриц. Numba оказывается примерно на 30% быстрее, чем Numpy для самых больших случаев.
я удивлен результатами 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
было получено путем переключать 2 внутренних петли и показывает другое улучшение 30% до 50%.
3 ответов
использовать -O3
для оптимизации. Это получается векторизациям on, что должно значительно ускорить ваш код.
Numba должен сделать это уже.
что бы я рекомендовал
если вы хотите максимальную эффективность, вы должны использовать специальную библиотеку линейной алгебры,классический из которых Блас/LAPACK библиотеки. Существует ряд реализаций, например. Intel MKL. То что вы пишите не собираюсь outpeform гипер-оптимизированные библиотеки.
Матрица Матрица умножить будет dgemm
режим: D означает double, ge-general, и mm для матрицы матрицы умножить. Если ваша проблема имеет дополнительную структуру, для дополнительного ускорения может быть вызвана более конкретная функция.
обратите внимание, что Numpy dot уже вызывает dgemm
! Вы, вероятно, не сделаете лучше.
почему ваш c++ медленный
ваш классический, интуитивно понятный алгоритм умножения матриц-матриц оказывается медленным по сравнению с тем, что возможно. Написание кода, который использует преимущества кэширования процессоров и т. д... дает важные прирост производительности. Дело в том, что множество умных людей посвятили свою жизнь тому, чтобы Матрица умножалась очень быстро, и вы должны использовать их работу, а не изобретать колесо.
в вашей текущей реализации, скорее всего, компилятор не может автоматически векторизовать самый внутренний цикл, потому что его размер равен 3. Также осуществляется "нервным" способом. Замена циклов так, что итерация по p
в самый внутренний цикл будет работать быстрее (col
не будет делать "нервный" доступ к данным), и компилятор должен быть в состоянии сделать лучшую работу (autovectorize).
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];
}
}
}
на моей машине оригинальная реализация C++ для P=10^6 элементов сборки с g++ dot.cpp -std=c++11 -O3 -o dot
флаги принимает 12ms
и выше реализация с замененными циклами занимает 7ms
.