Я провожу сравнительный анализ умножения матриц, как упоминалось ранее в статье Почему MATLAB так быстро справляется с умножением матриц?
Теперь у меня есть другая проблема: при умножении двух матриц 2048x2048 существует большая разница между C # и другими. Когда я пытаюсь перемножить только матрицы 2047x2047, это кажется нормальным. Также добавлены некоторые другие для сравнения.
1024x1024 - 10 секунд.
1027x1027 - 10 секунд.
2047x2047 - 90 секунд.
2048x2048 - 300 секунд.
2049x2049 - 91 секунда. (Обновить)
2500x2500 - 166 секунд
Это разница в три с половиной минуты для случая 2k на 2k.
с использованием массивов 2dim
//Array init like this
int rozmer = 2048;
float[,] matice = new float[rozmer, rozmer];
//Main multiply code
for(int j = 0; j < rozmer; j++)
{
for (int k = 0; k < rozmer; k++)
{
float temp = 0;
for (int m = 0; m < rozmer; m++)
{
temp = temp + matice1[j,m] * matice2[m,k];
}
matice3[j, k] = temp;
}
}
Ответы:
Вероятно, это связано с конфликтами в вашем кэше L2.
Промахи кеша на matice1 не являются проблемой, потому что к ним обращаются последовательно. Однако для matice2, если полный столбец помещается в L2 (т.е. когда вы обращаетесь к matice2 [0, 0], matice2 [1, 0], matice2 [2, 0] ... и т. Д., Ничего не выселяется), то проблем с Кеш промахов с matice2 тоже.
Теперь, чтобы глубже понять, как работает кеш, если байтовый адрес вашей переменной - X, то строка кеша для него будет (X >> 6) & (L - 1). Где L - общее количество строк кеша в вашем кеше. L всегда степень 2. Шесть исходит из того факта, что 2 ^ 6 == 64 байта - это стандартный размер строки кэша.
Что это значит? Что ж, это означает, что если у меня есть адрес X и адрес Y и (X >> 6) - (Y >> 6) делится на L (т.е. некоторая большая степень 2), они будут храниться в той же строке кеша.
Теперь, чтобы вернуться к вашей проблеме, в чем разница между 2048 и 2049 годами,
когда 2048 ваш размер:
если взять & matice2 [x, k] и & matice2 [y, k], разница (& matice2 [x, k] >> 6) - (& matice2 [y, k] >> 6) будет делиться на 2048 * 4 (размер поплавка). Итак, большая степень двойки.
Таким образом, в зависимости от размера вашего L2 у вас будет много конфликтов строк кеша, и вы будете использовать только небольшую часть вашего L2 для хранения столбца, поэтому вы фактически не сможете хранить полный столбец в своем кеше, поэтому вы получите плохую производительность. ,
Когда размер равен 2049, тогда разница составляет 2049 * 4, что не является степенью 2, поэтому у вас будет меньше конфликтов, и ваш столбец безопасно поместится в ваш кеш.
Теперь, чтобы проверить эту теорию, вы можете сделать несколько вещей:
Выделите свой массив matice2 array, как этот matice2 [razmor, 4096], и запустите с razmor = 1024, 1025 или любым размером, и вы увидите очень низкую производительность по сравнению с тем, что было у вас раньше. Это потому, что вы принудительно выравниваете все столбцы, чтобы они конфликтовали друг с другом.
Затем попробуйте matice2 [razmor, 4097] и запустите его с любым размером, и вы увидите гораздо лучшую производительность.
источник
Возможно эффект кеширования. С размерами матрицы, которые являются большими степенями двойки, и размером кэша, которые также являются степенью двойки, вы можете в конечном итоге использовать только небольшую часть вашего кеша L1, что сильно замедлит работу. Умножение простых матриц обычно ограничивается необходимостью извлечения данных в кеш. Оптимизированные алгоритмы, использующие тайлинг (или алгоритмы без учета кеша), ориентированы на более эффективное использование кеша L1.
Если вы рассчитываете время для других пар (2 ^ n-1,2 ^ n), я ожидаю, что вы увидите аналогичные эффекты.
Чтобы объяснить более полно, во внутреннем цикле, где вы обращаетесь к matice2 [m, k], вполне вероятно, что matice2 [m, k] и matice2 [m + 1, k] смещены друг от друга на 2048 * sizeof (float) и, таким образом, отображаются на тот же индекс в кэше L1. С N-сторонним ассоциативным кешем у вас обычно будет 1-8 ячеек кеша для всех этих мест. Таким образом, почти все эти обращения вызывают вытеснение кеша L1 и выборку данных из более медленного кеша или основной памяти.
источник
Возможно, это связано с размером кеша вашего процессора. Если 2 строки матрицы матрицы не умещаются, то вы потеряете время на подкачку элементов из ОЗУ. Дополнительных элементов 4095 может быть достаточно, чтобы ряды не умещались.
В вашем случае 2 строки для 2047 2d-матриц попадают в 16 КБ памяти (при условии 32-битных типов). Например, если у вас есть кэш L1 (ближайший к процессору на шине) размером 64 КБ, то вы можете разместить в кеш как минимум 4 строки (2047 * 32) одновременно. С более длинными строками, если требуется какое-либо заполнение, которое выталкивает пары строк за пределы 16 КБ, тогда все начинает становиться беспорядочным. Кроме того, каждый раз, когда вы «пропускаете» кеш, подкачка данных из другого кеша или основной памяти задерживает вещи.
Я предполагаю, что разница во времени выполнения, которую вы видите с матрицами разного размера, зависит от того, насколько эффективно операционная система может использовать доступный кеш (а некоторые комбинации просто проблематичны). Конечно, с моей стороны это грубое упрощение.
источник
Луи Брэнди написал два сообщения в блоге, анализирующих именно эту проблему:
Еще больше безумия кеша и вычислительной производительности - пример для начинающих с некоторой интересной статистикой и попытками более подробно объяснить поведение, оно действительно сводится к ограничению размера кеша.
источник
Учитывая, что время падает при больших размерах, не будет ли более вероятен конфликт кеша, особенно при степени 2 для проблемных размеров матрицы? Я не являюсь специалистом по вопросам кэширования, но отличная информация по вопросам производительности кэша , связанные здесь .
источник
Поскольку вы обращаетесь к
matice2
массиву по вертикали, он будет намного больше загружаться и выгружаться из кеша. Если вы отразите массив по диагонали, чтобы получить к нему доступ, используя[k,m]
вместо[m,k]
, код будет работать намного быстрее.Я тестировал это для матриц 1024x1024, и это примерно в два раза быстрее. Для матриц 2048x2048 это примерно в десять раз быстрее.
источник
Псевдоним кеша
Или кеширование , если можно назвать термин.
Кеши работают путем индексации битами младшего разряда и тегирования битами старшего разряда.
Представьте, что ваш кеш имеет 4 слова, а ваша матрица - 4 x 4. Когда осуществляется доступ к столбцу, а длина строки равна любой степени двойки, тогда каждый элемент столбца в памяти будет отображаться в один и тот же элемент кеша.
Степень двойки плюс один на самом деле оптимальна для этой задачи. Каждый новый элемент столбца будет отображаться в следующий слот кеша точно так же, как при доступе по строке.
В реальной жизни тег охватывает несколько последовательно увеличивающихся адресов, которые будут кэшировать несколько соседних элементов подряд. Смещая сегмент, которому сопоставляется каждая новая строка, обход столбца не заменяет предыдущую запись. При переходе к следующему столбцу весь кеш будет заполнен разными строками, и каждый раздел строки, который помещается в кеш, попадет в несколько столбцов.
Поскольку кеш намного быстрее, чем DRAM (в основном благодаря тому, что он находится на кристалле), скорость обращения - это все.
источник
Похоже, вы достигли предела размера кеша или, возможно, у вас есть проблемы с воспроизводимостью ваших таймингов.
В чем бы ни заключалась проблема, вам просто не следует писать умножение матриц самостоятельно на C #, а вместо этого использовать оптимизированную версию BLAS. Такой размер матрицы на любой современной машине должен быть увеличен менее чем за секунду.
источник
Очень важно эффективно использовать иерархию кеша. Вам необходимо убедиться, что многомерные массивы содержат данные в удобном порядке, что может быть достигнуто путем мозаичного размещения . Для этого вам нужно сохранить 2D-массив как 1D-массив вместе с механизмом индексации. Проблема с традиционным методом заключается в том, что хотя два соседних элемента массива, которые находятся в одной строке, находятся рядом друг с другом в памяти, два соседних элемента в одном столбце будут разделены W элементами в памяти, где W - количество столбцов. , Тайлинг может иметь разницу в производительности в десять раз.
источник
Я подозреваю, что это результат так называемого « последовательного затопления ». Дело в том, что вы пытаетесь перебрать список объектов, который немного превышает размер кеша, поэтому каждый отдельный запрос к списку (массиву) должен выполняться из оперативной памяти, и вы не получите ни одного кеша ударить.
В вашем случае вы просматриваете свои массивы 2048 индексов 2048 раз, но у вас есть место только для 2047 (возможно, из-за некоторых накладных расходов из структуры массива), поэтому каждый раз, когда вы получаете доступ к массиву pos, он должен получить этот массив pos от барана. Затем он сохраняется в кеше, но перед повторным использованием сбрасывается. Таким образом, кеш по существу бесполезен, что приводит к гораздо большему времени выполнения.
источник