Почему MATLAB так быстро умножает матрицы?

Я делаю некоторые тесты с CUDA, C++, C# и Java и использую MATLAB для проверки и генерации матриц. Но когда я умножаю с MATLAB, 2048x2048 и даже большие матрицы почти мгновенно умножаются.

             1024x1024   2048x2048   4096x4096
             ---------   ---------   ---------
CUDA C (ms)      43.11      391.05     3407.99
C++ (ms)       6137.10    64369.29   551390.93
C# (ms)       10509.00   300684.00  2527250.00
Java (ms)      9149.90    92562.28   838357.94
MATLAB (ms)      75.01      423.10     3133.90

только CUDA конкурентоспособна, но я думал, что по крайней мере C++ будет несколько ближе, а не 60x медленнее.

Итак, мой вопрос : как MATLAB делает это так быстро?

C++ Код:

float temp = 0;
timer.start();
for(int j = 0; j < rozmer; j++)
{
    for (int k = 0; k < rozmer; k++)
    {
        temp = 0;
        for (int m = 0; m < rozmer; m++)
        {
            temp = temp + matice1[j][m] * matice2[m][k];
        }
        matice3[j][k] = temp;
    }
}
timer.stop();

изменить: Я тоже не знаю, что думать о результатах C#. Алгоритм такой же, как C++ и Java, но есть гигантский скачок 2048 из 1024?

Edit2: Обновлено MATLAB и 4096x4096 результаты

14 ответов


вот мои результаты с использованием MATLAB R2011a + Parallel Computing Toolbox на машине с Tesla C2070:

>> A = rand(1024); gA = gpuArray(A);
% warm up by executing the operations a couple of times, and then:
>> tic, C = A * A; toc
Elapsed time is 0.075396 seconds.
>> tic, gC = gA * gA; toc
Elapsed time is 0.008621 seconds.

MATLAB использует высоко оптимизированные библиотеки для матричного умножения, поэтому простое матричное умножение MATLAB так быстро. The gpuArray версия использует магма.

обновление с помощью R2014a на машине с Tesla K20c и новый timeit и gputimeit функции:

>> A = rand(1024); gA = gpuArray(A);
>> timeit(@()A*A)
ans =
    0.0324
>> gputimeit(@()gA*gA)
ans =
    0.0022

этот вопрос повторяется и должен быть дан более четкий ответ, чем "Matlab использует высоко оптимизированные библиотеки" или "Matlab использует MKL" для StackOverflow.

история:

матричное умножение (вместе с матричным вектором, векторным умножением и многими матричными разложениями) является (являются) наиболее важными проблемами в линейной альгребре. Инженеры решают эти проблемы с компьютерами с самого начала дни.

Я не эксперт по истории, но, по-видимому, тогда все просто переписали свою версию Fortran с помощью простых циклов. Затем появилась некоторая стандартизация с идентификацией "ядер" (основных подпрограмм), которые необходимы для решения большинства задач линейной алгебры. Эти основные операции были затем стандартизированы в спецификации, называемой: основные подпрограммы линейной алгебры (BLAS). Затем инженеры могли бы назвать эти стандартные, хорошо проверенные процедуры BLAS в своем коде, сделать их работу намного проще.

Блас:

BLAS эволюционировал от уровня 1 (первая версия, которая определяла скалярно-векторные и векторно-векторные операции) до уровня 2 (векторно-матричные операции) до уровня 3 (матрично-матричные операции) и предоставлял все больше и больше "ядер", поэтому стандартизировал все больше и больше фундаментальных операций линейной алгебры. Оригинальные реализации Fortran 77 по-прежнему доступны на библиотека netlib по сайт.

к лучшей производительности:

таким образом, на протяжении многих лет (особенно между выпусками BLAS level 1 и level 2: начало 80-х годов), аппаратное обеспечение изменилось с появлением векторных операций и иерархий кэша. Эти изменения позволили существенно повысить производительность подпрограмм BLAS. Затем различные поставщики пришли вместе с их реализацией процедур BLAS, которые были все более и более эффективными.

I не знаю всех исторических реализаций (я тогда не родился и не был ребенком), но два из самых заметных вышли в начале 2000-х годов: Intel MKL и GotoBLAS. Ваш Matlab использует Intel MKL, который является очень хорошим, оптимизированным BLAS, и это объясняет отличную производительность, которую вы видите.

технические данные по умножению матрицы:

так почему же Matlab (MKL) так быстро в dgemm (общая матрица с двойной точностью-матричное умножение)? В простыми словами: потому что он использует векторизацию и хорошее кэширование данных. В более сложных терминах: см. статьи предоставлено Джонатаном Муром.

в основном, когда вы выполняете умножение в коде C++, который вы предоставили, вы совсем не дружественны к кэшу. Поскольку я подозреваю, что вы создали массив указателей на массивы строк, ваш доступ во внутреннем цикле к k-му столбцу "matice2":matice2[m][k] очень медленно. Действительно, когда вы получаете доступ matice2[0][k], вы должны получить K-й элемент массива 0 вашей матрицы. Затем на следующей итерации вы должны получить доступ к matice2[1][k], который является k-м элементом другого массива (массив 1). Затем на следующей итерации вы получаете доступ к еще одному массиву и так далее... Так как вся матрица matice2 не может поместиться в самые высокие кэши (это 8*1024*1024 байт большой), программа должна извлечь нужный элемент из основной памяти, потеряв много времени.

если вы просто транспонировать матрицу, так что доступ будет в непрерывной памяти адреса, ваш код уже будет работать намного быстрее, потому что теперь компилятор может загружать целые строки в кэш одновременно. Просто попробуйте эту измененную версию:

timer.start();
float temp = 0;
//transpose matice2
for (int p = 0; p < rozmer; p++)
{
    for (int q = 0; q < rozmer; q++)
    {
        tempmat[p][q] = matice2[q][p];
    }
}
for(int j = 0; j < rozmer; j++)
{
    for (int k = 0; k < rozmer; k++)
    {
        temp = 0;
        for (int m = 0; m < rozmer; m++)
        {
            temp = temp + matice1[j][m] * tempmat[k][m];
        }
        matice3[j][k] = temp;
    }
}
timer.stop();

таким образом, вы можете видеть, как просто локальность кэша значительно повысила производительность вашего кода. Теперь настоящий dgemm реализации используют это на очень обширном уровне: они выполняют умножение на блоках матрицы, определенной размером TLB (буфер lookaside перевода, короче говоря: что можно эффективно кэшировать), чтобы они передавали процессору именно тот объем данных, который он может обрабатывать. Другой аспект-векторизация, они используют векторизованные инструкции процессора для оптимальной пропускной способности команд, что вы не можете сделать из своего кросс-платформенного кода C++.

наконец, люди, утверждающие, что это из-за алгоритма Штрассена или Coppersmith-Winograd, ошибаются, оба этих алгоритма не реализуемы на практике из-за аппаратного обеспечения соображения, упомянутые выше.


вот почему. MATLAB не выполняет наивное умножение матрицы, зацикливаясь на каждом отдельном элементе, как это было в коде C++.

конечно, я предполагаю, что вы просто использовать C=A*B вместо написания функции умножения себя.


Matlab incorporated LAPACK некоторое время назад, поэтому я предполагаю, что их матричное умножение использует что-то, по крайней мере, так быстро. Исходный код LAPACK и документация легко доступны.

вы также можете посмотреть на Гото и Ван де Geijn газете "Анатомия высокопроизводительная Матрица Умножение " at http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.140.1785&rep=rep1&type=pdf


при умножении матрицы вы используете наивный метод умножения, который занимает время O(n^3).

существует алгоритм умножения матрицы, который принимает O(n^2.4). Это означает, что на n=2000 ваш алгоритм требует ~100 раз больше вычислений, чем лучший алгоритм.
Вы действительно должны проверить страницу Википедии для умножения матрицы для получения дополнительной информации об эффективных способах ее реализации.


вам нужно быть осторожным в проведении справедливых сравнений с C++. Можете ли вы опубликовать код C++, который показывает основные внутренние циклы, которые вы используете для умножения матрицы? В основном, меня волнует расположение вашей памяти и то, делаете ли вы что-то расточительно.

Я написал умножение матрицы C++, которое так же быстро, как Matlab, но оно было осторожным. (EDIT: до того, как Matlab использовал графические процессоры для этого.)

вы можете быть практически уверены, что Matlab тратит впустую очень немногие циклы на этих "встроенных" функциях. Мой вопрос в том, где вы тратите циклов? (Без обид)


ответ LAPACK и Блас библиотеки делают MATLAB ослепительно быстрым в матричных операциях, а не какой-либо собственный код людьми в MATLAB.

использовать LAPACK и/или Блас библиотеки в коде C++ для матричных операций, и вы должны получить аналогичную производительность, как MATLAB. Эти библиотеки должны быть свободно доступны в любой современной системе, а их части десятилетиями разрабатывались в академических кругах. Обратите внимание, что есть несколько реализаций, включая некоторые закрытые источники, такие как Intel MKL.

обсуждение того, как BLAS получает высокую производительность здесь.


кстати, это серьезная боль в моем опыте, чтобы вызвать библиотеки LAPACK непосредственно из c (но стоит того). Вы должны прочитать документацию очень точно.


в зависимости от вашей версии Matlab, я считаю, что он уже может использовать ваш GPU.

другое дело; Matlab отслеживает многие свойства вашей матрицы; ее диагональ, герметичность и т. д., и специализируется на ее алгоритмах на основе. Может быть, его специализация основана на нулевой матрице, которую вы передаете, или что-то в этом роде? Может быть, это кэширование повторяющихся вызовов функций, которое портит ваши тайминги? Возможно, он оптимизирует повторяющуюся неиспользуемую матрицу продукты?

чтобы защитить от таких вещей, используйте матрицу случайных чисел и убедитесь, что вы принудительно выполняете, печатая результат на экран или диск или что-то еще.


использование двойников и одного сплошного массива вместо трех отдельных приводит мой код c# почти к тем же результатам, что и C++/Java (с вашим кодом: 1024 - было немного быстрее, 2048-было около 140s и 4096 - было около 22 минут)

                1024x1024   2048x2048   4096x4096
                ---------   ---------   ---------
your C++ (ms)   6137.10     64369.29     551390.93
my C# (ms)      9730.00     90875.00    1062156.00

вот мой код:

    const int rozmer = 1024;
    double[][] matice1 = new double[rozmer * 3][];
    Random rnd = new Random();

    public Form1()
    {
        InitializeComponent();

        System.Threading.Thread thr = new System.Threading.Thread(new System.Threading.ThreadStart(() =>
        {

            string res = "";
            Stopwatch timer = new Stopwatch();
            timer.Start();

            double temp = 0;
            int r2 = rozmer * 2;

            for (int i = 0; i < rozmer*3; i++)
            {
                if (matice1[i] == null)
                {
                    matice1[i] = new double[rozmer];
                    {
                        for (int e = 0; e < rozmer; e++)
                        {
                            matice1[i][e] = rnd.NextDouble();
                        }
                    }
                }
            }
            timer.Stop();
            res += timer.ElapsedMilliseconds.ToString();

            int j = 0; int k = 0; int m = 0;

            timer.Reset();
            timer.Start();
            for (j = 0; j < rozmer; j++)
            {
                for (k = 0; k < rozmer; k++)
                {
                    temp = 0;
                    for (m = 0; m < rozmer; m++)
                    {
                        temp = temp + matice1[j][m] * matice1[m + rozmer][k];
                    }
                    matice1[j + r2][k] = temp;
                }
            }
            timer.Stop();
            this.Invoke((Action)delegate
            {
                this.Text = res + " : " + timer.ElapsedMilliseconds.ToString();
            });
        }));
        thr.Start();
    }

вы проверили, что все реализации использовали многопоточные оптимизации для алгоритма ? И они использовали один и тот же алгоритм умножения ?

Я действительно сомневаюсь в этом.

Matlab не является по своей сути быстрым, вы, вероятно, использовали медленные реализации.

алгоритмы эффективного умножения матриц


общий ответ на вопрос "почему matlab быстрее делает xxx, чем другие программы" заключается в том, что matlab имеет много встроенных оптимизированных функций.

другие программы, которые используются часто не имеют этих функций, поэтому люди применяют свои собственные творческие решения, которые удивительно медленнее, чем профессионально оптимизированный код.

Это можно интерпретировать двумя способами:

1) общий / теоретический путь: Matlab значительно не быстрее, вы просто делаете эталоном не так

2) реалистичный способ: для этого Matlab быстрее на практике, потому что языки, такие как c++, Слишком легко используются неэффективными способами.


MATLAB использует высоко оптимизированную реализацию LAPACK от Intel, известную как Библиотека Ядра Intel Math (Intel MKL) - в частности,dgemm функция. Скорость эта библиотека использует преимущества функций процессора, включая инструкции SIMD и многоядерные процессоры. Они не документируют, какой конкретный алгоритм они используют. Если вы должны были вызвать Intel MKL с C++ , вы должны увидеть аналогичную производительность.

Я не уверен, что библиотека MATLAB использует для Умножение GPU, но, вероятно, что-то вроде nVidia CUBLAS.


это медленно в C++, потому что вы не используете многопоточность. По сути, если A = B C, где все они являются матрицами, первая строка A может быть вычислена независимо от второй строки и т. д. Если A, B и C все N на n матриц, вы можете ускорить умножение на коэффициент N^2, как

a_{i, j} = sum_{k} b_{i,k} c_{k, j}

Если вы используете, скажем, Eigen [ http://eigen.tuxfamily.org/dox/GettingStarted.html ], многопоточность встроена и количество нитей регулируется.


резкий контраст обусловлен не только удивительной оптимизацией Matlab (как уже обсуждалось многими другими ответами), но и тем, как вы сформулировали матрицу как объект.

похоже, вы сделали матрицу списком списков? Список списков содержит указатели на списки, которые затем содержат элементы матрицы. Места расположения содержащихся списков назначаются произвольно. Как вы зацикливаетесь на своем первом индексе (номер строки?), время доступа к памяти очень значительно. В сравнение, почему бы вам не попробовать реализовать матрицу как один список/вектор, используя следующий метод?

#include <vector>

struct matrix {
    matrix(int x, int y) : n_row(x), n_col(y), M(x * y) {}
    int n_row;
    int n_col;
    std::vector<double> M;
    double &operator()(int i, int j);
};

и

double &matrix::operator()(int i, int j) {
    return M[n_col * i + j];
}

тот же алгоритм умножения должен использоваться так, чтобы число флопа было одинаковым. (н^3 для квадратных матриц размера n)

Я прошу вас, сделайте так, чтобы результат сопоставим с тем, что у вас было раньше (на той же машине). С помощью сравнения вы покажете, насколько значительное время доступа к памяти может быть!