Почему умножение матрицы Штрассена происходит медленно?
Я написал две программы умножения матриц на C++: Regular MM (источник), и Штрассен мм (источник), оба из которых работают на квадратных матрицах размеров 2^k x 2^k(другими словами, квадратные матрицы четного размера).
результаты просто ужасные. Для матрицы 1024 x 1024 обычный мм принимает 46.381 sec
, в то время как ШТРАССЕН MM принимает 1484.303 sec
(25 minutes
!!!!).
Я попытался сохранить код как можно более простым. Другие примеры MM Штрассена найденные в Интернете не сильно отличаются от моего кода. Одна проблема с кодом Штрассен очевиден - у меня нет отсечки, что переключается в обычный мм.
какие еще проблемы имеет MM-код моего Штрассена ???
спасибо !
прямые ссылки на sources
http://pastebin.com/HqHtFpq9
http://pastebin.com/USRQ5tuy
Edit1. Кулак, много хороших советов. Спасибо, что уделили время и поделились знание.
я реализовал изменения (сохранил весь мой код), добавил точку отсечения. Мм матрицы 2048x2048, с отсечкой 512 уже дает хорошие результаты. Обычный мм: 191.49 s Мм Штрассена: 112.179 с Значительное улучшение. Результаты были получены на доисторическом Lenovo x61 TabletPC с процессором Intel Centrino, используя Visual Studio 2012. Я сделаю больше проверок (чтобы убедиться, что я получил правильные результаты) и опубликую результаты.
2 ответов
одна проблема с кодом Штрассена очевидна - у меня нет точки отсечения, что переключается в обычный мм
справедливо сказать, что рекурсия до 1 точки является основной (если не всей) проблемой. Попытка угадать другие узкие места производительности без решения этой проблемы почти спорна из-за массивного хита производительности, который он приносит. (Другими словами, вы сравниваете яблоки с апельсинами.)
как описано в комментариях, выравнивание кэша может иметь эффект, но не такого масштаба. Кроме того, выравнивание кэша, вероятно, повредит регулярному алгоритму больше, чем алгоритм Штрассена, так как последний не учитывает кэш.
void strassen(int **a, int **b, int **c, int tam) {
// trivial case: when the matrix is 1 X 1:
if (tam == 1) {
c[0][0] = a[0][0] * b[0][0];
return;
}
это слишком мало. Хотя алгоритм Штрассена имеет меньшую сложность, он имеет гораздо большую константу Big-O. Во-первых, у вас есть накладные расходы на вызов функции вплоть до 1 элемента.
это аналогично использованию слияния или быстрой сортировки и рекурсии вплоть до одного элемент. Чтобы быть эффективным, вам нужно остановить рекурсию, когда размер становится маленьким и вернуться к классическому алгоритму.
в быстрой сортировке / слиянии вы вернетесь к низким накладным расходам O(n^2)
вставка или сортировка выбора. Здесь вы бы вернулись к нормальному O(n^3)
матрицы умножать.
порог, который вы отбрасываете классический алгоритм, должен быть настраиваемым порогом, который, вероятно, будет варьироваться в зависимости от оборудования и способности компилятора оптимизировать код.
для чего-то вроде умножения Штрассена, где преимущество только O(2.8074)
по сравнению с классическим O(n^3)
, Не удивляйтесь, если этот порог окажется очень высоким. (тысячи элементов?)
в некоторых приложениях может быть много алгоритмов с уменьшающейся сложностью, но увеличивающейся большой-O. В результате несколько алгоритмов становятся оптимальными при разных размерах.
большое целочисленное умножение является пресловутым пример:
- умножение начальной школы:O (N^2) оптимальный для
- Умножение Карацубы: O (N^1.585) быстрее, чем выше на ~100 цифр*
- Toom-Cook 3-way: O (N^1.465) быстрее, чем Карацуба на ~3000 цифр*
- БПФ с плавающей точкой: O (>N log (N)) быстрее, чем Karatsuba / Toom-3 at ~700 знаков*
- Schönhage–Штрассен (ССА): O (N log (n) loglog (n)) быстрее, чем БПФ на ~ миллиард цифр*
- теоретико-числовое преобразование фиксированной ширины: O (N log (n) быстрее, чем SSA на ~ несколько миллиардов цифр?*
*обратите внимание, что эти примеры порогов являются приблизительными и могут сильно отличаться - часто более чем в 10 раз.
таким образом, может быть больше проблем, но ваша первая проблема заключается в том, что вы используете массивы указателей на массивы. И так как вы используете размеры массива, которые являются степенями 2, это особенно большой удар по производительности, выделяя элементы смежно и используя целочисленное деление, чтобы сложить длинный массив чисел в строки.
в любом случае, это моя первая догадка о проблеме. Как я уже сказал, Может быть больше, и я добавлю к этому ответу, как я узнаю их.
Edit: это, вероятно, только вносит небольшой вклад в проблему. Проблема, скорее всего, одна Лукиан Григоре относится к участием проблемы с кеш-линией с полномочиями двух.
Я проверил, что мое беспокойство справедливо для наивного алгоритма. Время для наивного алгоритма уменьшается почти на 50%, если массив является непрерывным. Вот код для этого (используя класс SquareMatrix, который C++11 зависимый) от pastebin.