Сравнение Python, Numpy, Numba и C++ для умножения матриц

в программе, над которой я работаю, мне нужно многократно умножить две матрицы. Из-за размера одной из матриц эта операция занимает некоторое время, и я хотел посмотреть, какой метод будет наиболее эффективным. Матрицы имеют размеры (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

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 казней хранившийся.

результаты

Results

как и ожидалось, простой код 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%.

Results v2

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.