Прежде всего, я съеживаюсь всякий раз, когда что-то квалифицируется как «очевидно».Это слово часто используется, чтобы скрыть недостатки в своих вычетах.
Но очевидно, что подобный подход очень медленный для построчного преобразования из-за всех ошибок кеша.
Я не уверен, что должно быть очевидным: будет показываться строковое преобразование или что оно медленное из-за пропадания кэша.В любом случае, я нахожу это не очевидным.В конце концов, здесь есть два аспекта кэширования, не так ли?Один для чтения и один для письма?Давайте посмотрим на код с точки зрения чтения:
row_major_naive
for (size_t i = 0; i < n_col; ++i)
for (size_t j = 0; j < n_row; ++j)
out_vec[j * n_col + i] = vec[i][j];
Последовательные чтения из vec
- это чтения непрерывной памяти: vec[i][0]
, за которыми следует vec[i][1]
и т. Д. Очень хорошо для кеширования.Итак ... кеш отсутствует?Медленный?:) Может быть, не так очевидно.
Тем не менее, есть кое-что, что можно почерпнуть из этого.Требование только неправильно, утверждая "очевидно".Есть не локальные проблемы, но они возникают в конце письма.(Последовательные записи компенсируются пробелом для значений 50 double
.) Эмпирическое тестирование подтверждает медлительность.Таким образом, может быть, решение состоит в том, чтобы переключить то, что считается «очевидным»?
перевернутая мажорная строка
for (size_t j = 0; j < n_row; ++j)
for (size_t i = 0; i < n_col; ++i)
out_vec[j * n_col + i] = vec[i][j];
Все, что я здесь сделал, - это повернул петли.Буквально поменяйте местами порядок этих двух строк кода, затем измените отступ.Теперь последовательные чтения потенциально повсюду, так как они читают из разных векторов.Тем не менее, последовательные записи теперь в смежные блоки памяти.В каком-то смысле мы находимся в той же ситуации, что и раньше.Но так же, как и раньше, нужно измерить производительность, прежде чем принять «быстрое» или «медленное».
NaiveColumnMajor: 3,4 секунды
NaiveRowMajor: 7,7 секунды
FlippedRowMajor: 4,2 секунды
BlockingRowMajor: 4,4 секунды
BlockingColumnMajor: 3,9 секунды
Все еще медленнее, чем основная конверсия наивного столбца.Однако этот подход не только быстрее наивного майора строк, но и быстрее, чем блокирует мажор строк.По крайней мере, на моем компьютере (используя gcc -O3
и , очевидно, : P, повторяясь тысячи раз).Пробег может отличаться.Я не знаю, что скажут необычные инструменты профилирования.Дело в том, что иногда проще, тем лучше.
Для забавы я сделал тест, в котором размеры меняются местами (изменяясь от 50 векторов 4000 элементов до 4000 векторов 50 элементов).Все методы пострадали таким образом, но "NaiveRowMajor" получил самый большой удар.Стоит отметить, что «перевернутая мажорная строка» отстала от версии блокировки.Таким образом, как и следовало ожидать, лучший инструмент для работы зависит от того, что именно работа.секунд
BlockingRowMajor: 4,9 секунды
BlockingColumnMajor: 4,5 секунды
(Кстати, я также попробовал трюк с переворотом на версии блокировки. Изменение было небольшим - около 0,2 -и противоположность отклонению наивной версии. То есть «блокировка с переворотом» была медленнее, чем «блокировка» для векторов вопроса 50 на 4000, но быстрее для моего варианта 4000 на 50. Точная настройка может улучшить результаты.)
Обновление: Я провел еще немного тестирования с помощью трюка с переворотом на блокирующей версии.Эта версия имеет четыре цикла, поэтому «переворот» не так прост, как в случае только двух циклов.Похоже, что замена порядка двух внешних циклов плохо влияет на производительность, а замена внутренних двух циклов - это хорошо.(Первоначально я сделал оба и получил смешанные результаты.) Когда я поменял местами только внутренние петли, я измерил 3,8 секунды (и 4,1 секунды в сценарии 4000-из-50), делая это лучшим рядом-много вариант в моих тестах.
рядный основной гибрид
for (size_t l = 0; l < n_col; l += block_side)
for (size_t i = 0; i < n_row; ++i)
for (size_t j = l; j < l + block_side && j < n_col; ++j)
out_vec[i * n_col + j] = vec[j][i];
(После замены внутренних циклов я объединил средние циклы.)
Что касается теории, стоящей за этим, я бы предположил, что это равносильно попытке записать один блок кэша за раз.Как только блок записан, попробуйте повторно использовать векторы (vec[j]
), прежде чем они будут извлечены из кэша.После того, как вы исчерпали эти исходные векторы, переходите к новой группе исходных векторов, снова записывая полные блоки за раз.