Я решил попытаться реализовать специальный набор классов на основе хеш-функции, который использует линейное зондирование для обработки коллизий:
- Резервное хранилище представляет собой простой массив
long
s
- Размер массива больше ожидаемого количества элементов, которые будут сохранены.
- Для хэш-кода значения используйте наименее значащий 31 бит.
Поиск позиции значения в резервном хранилище выполняется с использованием базового линейного зонда, например:
int FindIndex(long value)
{
var index = ((int)(value & 0x7FFFFFFF) % _storage.Length;
var slotValue = _storage[index];
if(slotValue == 0x0 || slotValue == value) return index;
for(++index; ; index++)
{
if (index == _storage.Length) index = 0;
slotValue = _storage[index];
if(slotValue == 0x0 || slotValue == value) return index;
}
}
(Я смог определить, что сохраняемые данные никогда не будут содержать 0, поэтому это число безопасно использовать для пустых слотов.)
Массив должен быть больше, чем количество хранимых элементов. (Коэффициент загрузки меньше 1.) Если набор когда-либо будет полностью заполнен, то FindIndex()
перейдет в бесконечный цикл, если будет использоваться для поиска значения, которого еще нет в наборе. На самом деле, он захочет иметь достаточно много пустого пространства, иначе поиск и извлечение могут пострадать, когда данные начнут образовывать большие скопления.
Я уверен, что еще есть место для оптимизации, и я могу застрять, используя какой-то BigArray<T>
или шард для бэк-магазина на больших наборах. Но первые результаты многообещающие. Он работает в два раза быстрее HashSet<T>
при коэффициенте нагрузки 0,5, почти в два раза быстрее при коэффициенте нагрузки 0,8, и даже при 0,9 он все еще работает на 40% быстрее в моих тестах.
Накладные расходы составляют 1 / load factor
, поэтому, если эти показатели производительности сохранятся в реальном мире, то я считаю, что это также будет более эффективным с точки зрения памяти, чем HashSet<T>
. Я не проводил формальный анализ, но, судя по внутренней структуре HashSet<T>
, я почти уверен, что его накладные расходы значительно превышают 10%.
-
Так что я очень доволен этим решением, но мне все еще интересно, есть ли другие возможности. Может быть, какая-то трия?
-
Эпилог: Наконец-то дошло время провести несколько конкурентных тестов по сравнению с HashSet<T>
на живых данных. (До того, как я использовал синтетические тестовые наборы.) Это даже превзошло мои оптимистические ожидания от предыдущих. Реальная производительность оказывается в 6 раз выше, чем HashSet<T>
, в зависимости от размера коллекции.