Этот вопрос повторяется и на него следует ответить более четко, чем «MATLAB использует высоко оптимизированные библиотеки» или «MATLAB использует MKL» один раз в переполнении стека.
История:
Матричное умножение (вместе с матрично-векторным, векторно-векторным умножением и многими матричными разложениями) является (являются) наиболее важными проблемами линейной алгебры. Инженеры решали эти проблемы с компьютерами с первых дней.
Я не специалист по истории, но, видимо, тогда все просто переписали его версию на Фортране с помощью простых циклов. Затем пришла некоторая стандартизация с идентификацией «ядер» (базовых процедур), которые необходимы для решения большинства задач линейной алгебры. Эти основные операции были затем стандартизированы в спецификации под названием: «Основные подпрограммы линейной алгебры (BLAS)». Инженеры могли бы тогда назвать эти стандартные, хорошо проверенные подпрограммы BLAS в своем коде, делая их работу намного проще.
BLAS:
BLAS эволюционировал от уровня 1 (первая версия, которая определяла скалярно-векторные и векторно-векторные операции) до уровня 2 (операции вектор-матрица) до уровня 3 (операции матрица-матрица) и предоставлял все больше и больше «ядер», настолько стандартизированных и другие основные операции линейной алгебры. Оригинальные реализации FORTRAN 77 по-прежнему доступны на веб-сайте Netlib .
На пути к лучшей производительности:
Таким образом, за прошедшие годы (особенно между выпусками уровня 1 и уровня 2: в начале 80-х годов) аппаратное обеспечение изменилось с появлением векторных операций и иерархий кэша. Эти изменения позволили существенно повысить производительность подпрограмм BLAS. Различные поставщики затем пришли с их реализацией подпрограмм BLAS, которые были все более и более эффективными.
Я не знаю всех исторических реализаций (тогда я не родился и не был ребенком), но в начале 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 (буфер с переводом в сторону, короче говоря: что может эффективно кэшироваться), чтобы они передавались в процессор именно тот объем данных, который он может обработать. Другой аспект - векторизация, они используют векторизованные инструкции процессора для оптимальной пропускной способности команд, чего вы не можете сделать из своего кроссплатформенного кода C ++.
Наконец, люди, утверждающие, что это из-за алгоритма Штрассена или Копперсмита-Винограда, ошибочны, оба эти алгоритма не реализуемы на практике из-за аппаратных соображений, упомянутых выше.