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
()
Ответ на: комментарий от bugfixer

Других готовых решений я не знаю, к сожалению.

Siborgium ★★★★★
()

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

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 ★★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.