LINUX.ORG.RU

Плохо параллелится код с std::unordered_map

 ,


0

4

Сабж. Есть плюсовый код, цикл параллелится при помощи OpenMP. Цикл по вектору, в процессе работы используется таблица std::unordered_map<int, T>, sizeof(T)=16 байт, в таблицу идет как запись так и чтение (изначально таблица пустая).

Что бы исключить гонку данных каждый тред работает со своей таблицей.

Число итераций в цикле до 4млн, каждая итерация занимает порядка 20мкс.

Эффективность распараллеливания около плинтуса, какие то десятки процентов на 2-4 тредах, дальше хуже.

Гонки данных нет (ну я не вижу, код специально писался так что бы ее исключать). Я бы понял если бы в std::unordered_map было какое то статическое поле, тогда OpenMP ведет себя похожим образом. Что просиходит вообще?

Проблема с плохой параллелизацией типичная, но по моему опыту такие достаточно нагруженные циклы параллелятся хорошо. Памяти задача ест относительно немного (1-2 Гб), правда адресуется она достаточно хаотично.

Основная разница в том что тут юзается std::unordered_map c которым я толком дела не имел в параллельных задачах.

Еще один вопрос - есть ли какие то рекомендации по выбору числа бакетов? Я выбрал порядка числа элементов (на два порядка ниже чем диапазон ключей), стало лучше чем по умолчанию. Сейчас еще поиграюсь конечно, но боюсь дело совсем не в этом…

cast @bugfixer

★★★★★

Последнее исправление: AntonI (всего исправлений: 3)
Ответ на: комментарий от imatveev13

Предлагаю заменить вектор на std::deque.

Практически не помогло, разница по сравнению с вектором в десятки процентов, максимальный профит с распараллеливания - стало быстрее на 20% на 4х тредах.

AntonI ★★★★★
() автор топика
Ответ на: комментарий от rumgot

Нет, колизии повышают сложность. В вырожденном случае (у всех элементов один хеш) сложность хеш таблицы O(N). В идеальном случае, где у всех элементов разные хеши O(1). В реальности где-то посередине в зависимости от данных. При этом надо учитывать, что используется не хеш целиком, а последние N бит, потому что хештаблица не будет содержать SIZE_MAX элементов, а всегда меньше. Так что коллизии случаются чаще, чем кажется. Конечно, с этим борятся авторы хешфункций и сама хештаблица (load factor), но это не панацея.

Если упираешься в перф, бенчить хештаблица vs std::map (и аналоги) стандартная практика. Потому что хештаблица не всегда быстрее и не серебряная пуля. Зависит от конкретной задачи и только бенчи знают правду наверняка.

Ну и само собой бенчить хеш таблицы с открытой адресацией и обычные тоже надо между собой, потому что они тоже по-разному ведут себя на разных наборах данных. Хотя на бумаге обе обещают сложность где-то между O(1) и O(N).

KivApple ★★★★★
()
Последнее исправление: KivApple (всего исправлений: 5)
Ответ на: комментарий от KivApple

Ну так можно установить заведомо бОльший чем требуется размер хэш таблицы и низкий max_load_factor, коллизии будут редки, перехэширования не будет, константное время будет не в среднем случае, а в большинстве.

rumgot ★★★★★
()
Ответ на: комментарий от KivApple

Он может кушать меньше ОЗУ, чем хештаблица с низким load factor.

Это абсолютно irrelevant. А вот то что при любом поиске в std::map Вы всегда будете трогать log(N) нод разбросанных по памяти случайным образом - это медицинский факт. И я ещё не видел чтобы хеш табличка с «хорошей» хеш функцией и малым числом коллизий была медленней.

bugfixer ★★★★★
()
Последнее исправление: bugfixer (всего исправлений: 1)
Ответ на: комментарий от bugfixer

Если мы равномерно запрашиваем все элементы, то в обоих случаях лучше, чтобы в кеше была вся структура целиком. std::map будет дольше влезать в кеш целиком, чем хештаблица с низким load factor.

Так что опять же, зависит от данных (в том числе их размера) и алгоритма.

KivApple ★★★★★
()
Ответ на: комментарий от KivApple

std::map будет дольше влезать в кеш целиком, чем хештаблица с низким load factor.

С точки зрения кеширования нас будут интересовать только непустые buckets, я думаю при прочих равных «рабочий» dataset у хеш таблички всё равно будет меньше. Ну, или я совсем чего-то не понимаю.

Я не говорю что std::map - абсолютное зло, и есть целый класс задач / алгоритмов где она просто незаменима. Но утверждать «std::map более cache-friendly» - так себе идея.

bugfixer ★★★★★
()
Последнее исправление: bugfixer (всего исправлений: 1)
Ответ на: комментарий от AntonI

Я бы рискнул предположить что это зависит от того как память для map аллоцирована. Если там ноды по отдельности, то фигово будет.

Это не так важно: ни в одном из случаев говорить о последовательном доступе не приходится - никакие «prefetch» не помогут. В этом контексте роль играет исключительно уместились мы хотя бы в L3, или нет.

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

Я про то, что в кэш оно же приходит строками, т.е. при случайном размещении отдельных элементов будет оверхед который убьёт весь профит (проигрыш от пустых бакетов).

Вот если мапу как то компактненько уложить тогда да. Но это нужен какой то продвинутый аллокатор.

AntonI ★★★★★
() автор топика
Последнее исправление: AntonI (всего исправлений: 1)
Ответ на: комментарий от AntonI

Я сварщик не настоящий, но тем не менее спрошу: а оно (std::vector, std::map, кто угодно) там вообще в принципе параллелизуется от OpenMP? А то озвученные ускорения в десятки процентов никак на это не похожи. Я в основном с массивами (и, будете смеяться, на фортране), тем не менее ускорение было кратное; top при этом показывал использование процессора в 1000% процентов.

В интырнетах пишут что с векторы очень даже параллелизуется, тем не менее…

sshestov ★★
()
Ответ на: комментарий от sshestov

Смотря в каком смысле параллелизуется. На чтение должно параллелиться все (ну вектор так точно), на запись с переаалокацией не параллелится ничего:-)

Ускорение зависит от задачи тащем то, на него много факторов влияет. Есть например такая roofline model…

AntonI ★★★★★
() автор топика
Ответ на: комментарий от AntonI

А может его можно переделать в массив, поперезаписывать и назад в map? Если озвученные в начале тайминги я понял правильно - 4 млн элементов х 20 мс = 1.3e+3 min (~20 часов), и могут быть ускорены, то как бы может стоит выделки.

sshestov ★★
()
Ответ на: комментарий от AntonI

Если переделывание в массив и OpenMP массива не помогает, то, очевидно проблема в алгоритме и переделывание мап в хэш или в любые другие структуры тоже не помогут. Тогда вопрос что ж происходит в цикле, раз он не параллелится? Какие-то хитрые перекрестные ссылки?

sshestov ★★
()

у вас число элементов как-то сверху ограничено?

если применима хештаблица для данного случая, и надо выжимать скорость по максимуму, то стоит свою хештаблицу написать с преаллокированными баскетами, чтобы не иметь аллокации во время заполнения, если они там есть.

вообще все зависит от числа элементов, стратегии добавления/ удаления элементов, обьема памяти.

как реализован unordered_map не знаю, но наверняка там общее решение, которое будет хуже заточенного под задачу.

короче, если стоит задача максимальной производительности, вряд ли стоит пользовать стандартные классы.

alysnix ★★★
()
Ответ на: комментарий от AntonI

Я переделывал, не помогает.

значит дело не в контейнере, а каких-то еще коллизиях на общих обьектах. используемых например аллокаторами или еще что.

alysnix ★★★
()

Сабж. Есть плюсовый код, цикл параллелится при помощи OpenMP…

А есть смысл ускорять вычисления? Для проверки работоспособности модели можно и подождать 80 секунд.

Если это «боевая» прошивка блока управления сверхзвукового снаряда, то тут кодогенератор в чистый Си использовать следует ради скорости исполнения расчета и чтобы исключить человеческие ошибки программирования.

Если же просто маяться ерундой в ВУЗе и нет огромной базы унаследованного исходного кода промышленного ПО, то зачем использовать Си++? Фортран считает гораздо быстрее и писать на нем проще.

Enthusiast ★★★
()
Ответ на: комментарий от Enthusiast

А есть смысл ускорять вычисления?

Есть.

кодогенератор в чистый Си использовать следует

Не следует.

Фортран считает гораздо быстрее и писать на нем проще.

Не считает быстрее и писать на нем не проще.

AntonI ★★★★★
() автор топика
17 сентября 2024 г.

Все оказалось банально:

sizeof(std::unordered_map<K, V>) == 56

(грррррр! 8 байт сэкономили, молодцы че). Дальше вот такая конструкция

std::vector<std::unordered_map<K, V> > table(omp_get_num_threads());

при совместном доступе из разных потоков встает колом потому что разные потоки ломятся в одну строку кэша.

@bugfixer, спасибо!;-)

AntonI ★★★★★
() автор топика
Последнее исправление: AntonI (всего исправлений: 1)