Цей тип запитань повторюється, і на нього слід відповісти чіткіше, ніж "MATLAB використовує високооптимізовані бібліотеки" або "MATLAB використовує MKL" для одного разу на переповнення стека.
Історія:
Матричне множення (разом з матричним вектором, множенням векторів та багатьма матричними розкладами) є (є) найбільш важливими проблемами лінійної алгебри. Ці проблеми з комп’ютерами інженери вирішували з перших днів.
Я не знавець історії, але, мабуть, тоді всі просто переписали свою версію FORTRAN простими петлями. Потім з'явилася деяка стандартизація, з ідентифікацією "ядер" (основних процедур), які потребують більшості лінійних проблем з алгебрами для їх вирішення. Ці основні операції були потім стандартизовані в специфікації, що називається: Основні лінійні алгебри підпрограми (BLAS). Потім інженери могли назвати ці стандартні, добре перевірені процедури BLAS у своєму коді, що значно полегшує їх роботу.
BLAS:
BLAS еволюціонував від рівня 1 (перша версія, яка визначала скалярний вектор і вектор-векторні операції) до рівня 2 (операції вектор-матриця) до рівня 3 (операції з матрицею-матрицею), і забезпечувала все більше і більше "ядер", настільки стандартизованих більше і більше основних операцій лінійної алгебри. Оригінальні реалізації FORTRAN 77 все ще доступні на веб-сайті Netlib .
На шляху до кращих показників:
Таким чином, з роками (особливо між релізами рівня 1 та 2 рівня BLAS: початок 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 (буфер перекладу для перегляду, короткий опис: що можна ефективно кешувати), щоб вони перетікали в процесор саме кількість даних, які вона може обробити. Інший аспект - векторизація, вони використовують векторизовані інструкції процесора для оптимальної пропускної здатності інструкцій, чого ви не можете реально зробити зі свого кросплатформенного коду С ++.
Нарешті, люди, які стверджують, що це з-за алгоритму Страссена або Копперсміта – Винограда, помиляються, обидва ці алгоритми не реалізовані на практиці через згадані вище апаратні міркування.