Оценка скорости / использования памяти для различных структур данных

Я пытаюсь решить, какую структуру данных использовать для следующего.

Допустим, у меня есть около 10 миллионов ключей, которые содержат указатели на уникальные объекты, содержащие некоторые данные.

Ключи UUID представляют собой 16-байтовые двоичные массивы. UUID генерируются с использованием генератора случайных чисел хорошего качества.

Я думал о следующем, но хотел бы знать, какие плюсы и минусы с точки зрения скорости и потребления памяти будут для каждого. Некоторые справедливые оценки, лучший / худший / средний случай на 64-битной платформе были бы хорошими.

Мне нужно иметь возможность вставлять практически неограниченное количество предметов.

Двоичное дерево хеш-таблиц Radix Tree (на основе битов или 2-битный многоканальный)

Мне нужно выполнить следующие операции: вставить, удалить, найти

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

4 ответа

Решение
  • Вы не заботитесь о заказе
  • Ваш ключ уже случайный
  • 10 миллионов предметов

Краткий ответ

Хеш-таблица, вероятно, будет лучшим для вашего случая.

скорость

Хеш-таблица (std::unordered_map) будет O(1), если хеширование постоянное. В вашем случае O(1) выполняется, потому что вам даже не нужно хэшировать - достаточно просто использовать младшие 32 бита случайного UUID. Стоимость поиска будет аналогична одной или двум указателям.

Бинарное дерево (std::map) будет O(log2 n), поэтому для 10 миллионов элементов у вас будет 24 сравнения и 24 потенциальных пропуска кэша. Даже при n = 4000 он будет использовать 12 сравнений, поэтому он очень быстро станет значительно хуже, чем хеш-таблица.

Основное дерево будет O(k), поэтому у вас будет максимум k сравнений и k потенциальных пропусков кэша. Очень маловероятно, что основополагающее дерево будет таким же быстрым, как хеш-таблица. В худшем случае (при условии k = несколько разумных 16 для дерева с 256 путями) он будет работать лучше, чем двоичное дерево, но гораздо хуже, чем хеш-таблица.

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

накладные расходы

Типичная хеш-таблица будет содержать около 1–3 указателей накладных расходов на элемент, если они заполнены. Если не заполнен, вы, вероятно, будете тратить 1 указатель пространства на пустой слот. Вы должны быть в состоянии поддерживать его почти полным, но при этом быть быстрее, чем двоичное дерево, потому что у вас очень случайный ключ, но для максимально возможной скорости вы, конечно, захотите дать ему достаточно места. Для 10 миллионов элементов на 32-разрядной машине ожидайте 38–114 МБ служебных данных для полной таблицы. Для наполовину заполненного стола ожидайте 76–153MiB.

Красно-черное дерево, самое распространенное std::map реализация, будет иметь 3 указателя + 1 бул на элемент. Некоторые реализации используют выравнивание указателей для объединения bool с одним из указателей. В зависимости от реализаций и степени заполнения хеш-таблицы, красно-черное дерево может иметь немного меньшие накладные расходы. Ожидайте 114–153MiB.

Основное дерево будет иметь 1 указатель на элемент и 1 указатель на пустой слот. К сожалению, я думаю, что такие большие случайные ключи приведут к тому, что у вас будет очень много пустых слотов к краю дерева, поэтому он, вероятно, будет использовать больше памяти, чем любой из перечисленных выше. Уменьшение k может снизить эти издержки, но также снизит производительность.

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

Обратите внимание, что std::unordered_map не позволяет вам контролировать, когда он изменит размер, поэтому получить один полный будет сложно. Boost Intrusive имеет очень хороший unordered_map реализация, которая позволит вам непосредственно контролировать это и многое другое.

Я только что сделал быстрый расчет, и я думаю, что вы можете быть в порядке со стандартным деревом 10 миллионов ключей - разумное число. При сбалансированном дереве это будет глубина только 23 узлов для проверки. С основополагающим деревом у вас фактически есть длина ключа 128 битов для проверки.

Ваш ключ также может быть представлен и сравнительно дешево. Используйте кортеж (boost или 0x) из двух 64-битных значений, чтобы получить тот же 128-битный ключ. Порядка кортежа будет достаточно для использования на карте. Копирование ключей, таким образом, дешево, как и сравнение. Сравнение целых чисел как есть, вероятно, дешевле, чем маскирование и сравнение на основе битов для поиска по глубине.

Так что в этом случае карта, скорее всего, будет работать нормально.

* Я бы избежал unordered_map здесь, поскольку UUID, как правило, структурированные данные. Это означает, что стандартная процедура хеширования (для хэш-карты) может быть очень плохой по производительности. *

Обновить:

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

Кроме того, учитывая совершенно случайные UUID, основание, скорее всего, будет иметь такую ​​же балансировку, что и дерево (поскольку распределение ключей абсолютно равномерное). Таким образом, вы не можете сохранить даже шаги и все равно понести накладные расходы на битовые операции. Но существует так много способов специализироваться и оптимизировать основополагающее дерево, что трудно точно сказать, может ли оно быть быстрее или всегда медленнее.

Я бы попробовал std::map или же std::unordered_map первый.

У них было много умных людей, которые развивали и совершенствовали их в течение многих лет.

Есть ли причина, по которой вы не можете использовать std::map или же std::unordered_map?

Основное дерево IMO не сложно реализовать. Тем не менее, простой хэш-таблицы будет достаточно. Просто выделите массив из 2^16 списков объектов и используйте первые 2 байта UUID для индексации списка, куда вставить объект. Тогда вы можете искать список примерно с 160 элементами.

Или выделите массив из 20M указателей. Чтобы сохранить объект, просто сделайте хеш UUID в диапазоне 0-20M, найдите первый свободный (NULL) указатель и сохраните его там. Поиск означает переход от значения хеша к первому значению NULL. Удаление также просто.... попробуйте прочитать http://en.wikipedia.org/wiki/Hash_function

Другие вопросы по тегам