LINUX.ORG.RU

Partially ordered unordered_map

 ,


0

2

Это самый первый раз когда я хоть что-то спрашиваю на ЛОРе, так что просьба гнилыми помидорами не закидывать :)

Чего хочется: ассоциативного контейнера похожего на unordered_map, но с частично определённым ordering.

Более конкретно. Допустим у нас есть ключ

struct Key {
   int field1;
   int field2;
   int field3;
};

Хотелось бы unordered_map, такого чтобы hash определялся field1 и field2 (collisions будут крайне редки, и макс длина внутри bucket - 5-6, и очень очень редко, при размере таблички ~200k buckets и заполнении меньше 0.5). Но при обходе внутри «группы» (same field1 + field2) очень бы хотелось strict ordering by the last one (field3). Порядок обхода (field1 + field2) не волнует вообще.

Понятно что для любых chained nodes имплементаций это довольно легко делается.

Собственно вопрос: есть чего готового на что можно глянуть, или только самим писать?

@fsb4000, @Siborgium, сильно расчитываю на Вашу помощь, господа.

В лоб: хеш-таблица со значениями - упорядоченными (ассоциативными) массивами

unordered_map«f1,f2>, ordered_map<f3, T»

anonymous
()

Просится какая то двухуровневая шня, у которой по первым двум полям хэш таблица, а в таблице лежит упорядоченный список пар третий ключ:значение или обычная мапа

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

Почему?

Дорого. По многим причинам. Мы на самом деле соревнуемся с std::map<Key, …>, и по факту (не смотря на все извращения) оно таки стоит лишних микросекунд, и в самый неподходящий момент…

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

Я глубоко не копал, но емнип unordered_map<size_t, …> при неограниченном числе бакетов выигрывает у такой же мапы на порядок. Я не очень понимаю за счет чего, мб какая то оптимизация за счет того что ключ целочисленный. Наверное это воспроизводимо и для мапы?

А насколько плотно лежат field3 в рамках одной пары field1,2 и какой у них диапазон?

И насколько важна оптимизация записи в контейнер?

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

Если значений c key3 в группе (key1, key2) мало, то map можно реализовать как сортированный вектор.

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

при неограниченном числе бакетов

Мечта поэта ;)

выигрывает у такой же мапы на порядок

Изначально мне нужна была стабильность итераторов при вставке. Мы на самом деле в этой map никогда не ищем, только обходим. Элементы никогда не удаляются. При вставке я заранее знаю что ключа там ещё нет. Казалось бы - живи да радуйся. Ан нет. По факту все эти 200k ключей прилетают в случайном порядке в течении секунды - двух, и в этот момент мы «заикаться» начинаем.

А насколько плотно лежат field3 в рамках одной пары field1,2 и какой у них диапазон

Field3 это на самом деле значение из enum. В «тяжелом» случае для 99% пар - единственное и заранее предсказуемое. Но захардкодить не выйдет. Field2 тоже по сути константа меньше 2k. В «тяжелом» случае - единственная. В общем случае их с десяток. Во всех случаях набор допустимых значений (вот этот вот десяток) известен заранее. Но захардкодить тоже не выйдет. Field1 это некий sequence num сгенерённый «вне». Непредсказуем. В принципе можно ожидать числа из первых 100 миллионов, но лучше на это не закладываться.

И насколько важна оптимизация записи в контейнер?

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

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

Field3 это на самом деле значение из enum.

битовое поле?

В «тяжелом» случае для 99% пар - единственное и заранее предсказуемое.

сортированный массив (99% из одного элемента - все операции O(1)).

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

сортированный массив (99% из одного элемента - все операции O(1)).

Это понятно. Но это лишний malloc. И не очень хорошая reference locality при обеге. Да и плодить вектора в подавляющем большинстве которых всего один элемент не самый эффективный способ решения проблемы. Тут просто напрашивается custom hash map, но очень уж его писать не хотелось.

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

Дорого. По многим причинам.

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

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

Тут просто напрашивается custom hash map

Оверкилл. Лучше свой вектор/массив. Который не дергает malloc когда элементов меньше некоторой константы, например, 1, раз в 99% случаев этого хватает (то есть просто поле в классе-массиве).

Сортированный вектор быстрее хешмапа при маленьком количестве элементов.

anonymous
()

Я что-то не понял, вы просто хешировать по двум полям хотите ? В чём проблема переопределить функции хеширования и равенства ? Или использовать unordered_multimap, опять же переопределив хешфункцию

template<class Key,
           class T,
           class Hash = hash<Key>,
           class Pred = equal_to<Key>,
           class Alloc = allocator<pair<const Key, T>>>
    class unordered_map;
 
template<class Key,
           class T,
           class Hash = hash<Key>,
           class Pred = equal_to<Key>,
           class Alloc = allocator<pair<const Key, T>>>
    class unordered_multimap;

третий и четвёртый шаблонные параметры. Если цель поддерживать конкретный порядок(куча, дерево), то просто тип для значения выберите подходящий, в чём проблема ?

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

В чём проблема переопределить функции хеширования и равенства?

Хочется обходить field3 в строго определённом порядке. Одним равенством здесь не отделаешься.

Или использовать unordered_multimap, опять же переопределив хешфункцию

И это рассматривалось. Если бы она позволяла втыкаться в конкретное место так бы наверное и сделали.

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

Хочется контроля над порядком элементов внутри bucket’а. В случае multimap - в пределах последовательности «эквивалентных» ключей.

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

Изначально мне нужна была стабильность итераторов при вставке. Мы на самом деле в этой map никогда не ищем, только обходим

А можно тогда полностью задачу сформулировать? По процитированному советую посмотреть plf::colony

UPD: дочитал до конца, colony это вообще не туда.

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

Field3 это на самом деле значение из enum

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

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

Ну не надо в лоб так уж это делать, кмк ты сильно лучше меня умеешь писать и хорошую косвенную адресацию и менеджер памяти:-)

Вообще мы такие вещи (однократная набивка в случ порядке + многократный быстрый обход) делаем в два этапа. На первом собираем данные так что бы они собирались быстро и можно было понять скока памяти выделить. Потом переносим данные в другой контейнер, оптимизированный именно на поиск/обход, с хорошей локальностью и вот этим всем.

Универсальный контейнер делающий и то и то очевидно и то и то будет делать фигово. Нужно два узкоспециализированных.

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

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

А точно нужен упорядоченный (в любой момент) контейнер? Может достаточно сортировки после сбора данных?

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

смещений размера енум

Enum хитрый - 16ти битный, sparse, порезанный на «блоки» значений обладающих определенными свойствами с некоторыми зависимостями между этими значениями. По факту сейчас конкретно в этой map может мелькать с десяток из этих значений в принципе, и с пяток одновременно (зависит от конфигурации модуля). Преаллоцировать даже на эти 5 когда чаще всего используется только 1 - довольно большая роскошь: мы найдем на что память на этих машинках потратить, да и вообще - много памяти не бывает ;) Хотя идея конечно интересная (спасибо!) - надо внимательно всё посчитать (учитывая все overheads), может там всё на так плохо по памяти как кажется на первый взгляд.

Пока я всё таки больше склоняюсь к мысли что custom hash map будет наиболее оптимальным решением - там и итерация простая (беги себе по списку нод), и вставка довольно быстрая (в подавляющем числе случаев придётся смотреть только на одну ноду). Будем думать дальше ;)

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

Насчёт сортированного вектора в роли малого ассоциативного контейнера - такое есть со стандартным интерфейсом в виде boost::flat_map https://www.boost.org/doc/libs/1_78_0/doc/html/container/non_standard_containers.html#container.non_standard_containers.flat_xxx

GPFault
()
Ответ на: комментарий от Elyas

А точно нужен упорядоченный (в любой момент) контейнер?

По field3 - да. Ну, если быть совсем точным то нужно гарантировать что определенные пары значений всегда идут подряд. Иначе может произойти большая большая бяка, в худшем случае с привлечением legals и тому подобными неприятностями.

Может достаточно сортировки после сбора данных?

В «тяжелом» случае 99% значений прилетают сразу, но остальное «долетает» в случайные моменты позже. Это тоже нужно правильно обрабатывать, хотя там уже не так стрессово потому как размазано по времени а не «все сразу». Следующее что я приделаю это сбор статистики насколько часто map меняется после начального заполнения. От этого и будем плясать - может так случиться что его иногда разворачивать в вектор на «медленных» обходах не так и дорого (чем то пересекается со второй идеей которую @AntonI подкинул).

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

такое есть со стандартным интерфейсом в виде boost::flat_map

Эт мы знаем ;) Да, в целом полезняшка, но здесь хотелось бы избавиться от лишних аллокаций по максимуму.

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

остальное «долетает» в случайные моменты позже.

Остальное оформлять в виде патча прицепленного сбоку к отсортированному контейнеру?

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

Преаллоцировать даже на эти 5 когда чаще всего используется только 1 - довольно большая роскошь

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

Банальность скажу - эпизодическое копирование данных с перетасовкой (сиречь дефграментация) в итоге дешевле чем плохая локальность;-)

AntonI ☕☕☕
()

Господа, я в принципе услышал всё что мне нужно было, закрываю тему. Всем откликнувшимся (включая анонимов) - большое спасибо ;)

bugfixer
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.