ImmutableCollections SetN детали реализации

Мне сложно понять детали реализации из java-9 ImmutableCollections.SetN; в частности, зачем нужно увеличивать внутренний массив вдвое.

Предположим, вы делаете это:

Set.of(1,2,3,4) // 4 elements, but internal array is 8

Точнее, я прекрасно понимаю, почему это делается (двойное расширение) в случае HashMap - когда вы никогда (почти) не хотите, чтобы load_factor был одним. Значение !=1 сокращает время поиска, так как, например, записи лучше распределяются по сегментам.

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

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

 int idx = Math.floorMod(pe.hashCode() ^ SALT, elements.length);

pe - это фактическое значение, которое мы добавляем в набор. SALT - это всего лишь 32 бита, генерируемые при запуске, один раз за JVM (это фактическая рандомизация, если хотите). elements.length для нашего примера - это 8 (4 элемента, но 8 здесь - вдвое больше).

Это выражение похоже на безопасную отрицательную операцию по модулю. Обратите внимание, что та же самая логическая вещь выполняется, например, в HashMap ((n - 1) & hash), когда выбирается сегмент.

Итак, если для нашего случая elements.length is 8, то это выражение вернет любое положительное значение, которое меньше 8 (0, 1, 2, 3, 4, 5, 6, 7).

Теперь остальная часть метода:

 while (true) {
        E ee = elements[idx];
        if (ee == null) {
            return -idx - 1;
        } else if (pe.equals(ee)) {
            return idx;
        } else if (++idx == elements.length) {
            idx = 0;
        }
    }

Давайте разберемся:

if (ee == null) {
    return -idx - 1;

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

} else if (pe.equals(ee)) {
    return idx;

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

 else if (++idx == elements.length) {
      idx = 0;
 }

Это означает, что этот слот занят (хеш-коллизия), но элементы не равны. В HashMap эта запись будет помещена в ту же корзину, что и LinkedNode или TreeNode, но не здесь.

Таким образом, index увеличивается, и пробуется следующая позиция (с небольшой оговоркой, что она перемещается по кругу, когда достигает последней позиции).

И вот вопрос: если при поиске по индексу ничего особенного (если я что-то не упускаю) не делается, зачем нужен массив в два раза больше? Или почему функция не была написана так:

int idx = Math.floorMod(pe.hashCode() ^ SALT, input.length);

// notice the diff elements.length (8) and not input.length (4)

person Eugene    schedule 27.07.2017    source источник
comment
@GhostCat github.com/netroby/jdk9-dev/blob/master/jdk/src/java.base/share/ (метод probe)   -  person Dioxin    schedule 27.07.2017
comment
@VinceEmigh Подводя итог другому обсуждению: часто вещи не являются черными или белыми. Я видел множество ответов людей с шестизначными цифрами, которые давали еще меньше содержания или больше неопределенности, но набрали много голосов за несколько часов. И я видел, как хороший контент долго сидит без дела ... и ничего не происходит. И вы видите - ноль других ответов, даже без комментариев по этому вопросу. Так что, возможно, список возможных объяснений не дает отличного ответа, но он может дать пищу для размышлений. И кроме того: разве голосование не для этого? Я воспринимаю отрицательные голоса как прямую обратную связь ... и хорошо: я стараюсь   -  person GhostCat    schedule 27.07.2017
comment
@VinceEmigh, чтобы в следующий раз придумать что-нибудь получше.   -  person GhostCat    schedule 27.07.2017


Ответы (1)


Текущая реализация SetN представляет собой довольно простую схему замкнутого хеширования, в отличие от подхода с раздельной цепочкой, используемого HashMap. («Закрытое хеширование» также сбивает с толку как «открытая адресация».) В схеме закрытого хеширования , элементы хранятся в самой таблице, а не в списке или дереве элементов, которые связаны из каждого слота таблицы, что является отдельной цепочкой.

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

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

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

При обсуждении реализации мы провели несколько тестов с разными коэффициентами расширения. (Я использовал термин EXPAND_FACTOR в коде, тогда как в большей части литературы используется коэффициент загрузки. Причина в том, что коэффициент расширения является обратной величиной коэффициента загрузки, используемого в HashMap, и использование «коэффициента нагрузки» для обоих значений может сбить с толку.) Когда коэффициент расширения был близок к 1,0, работа датчика, как и ожидалось, была довольно медленной. Он значительно улучшился по мере увеличения коэффициента расширения. Улучшение действительно нарастало к тому времени, когда оно добралось до 3.0 или 4.0. Мы выбрали версию 2.0, так как она дает наибольшее улучшение производительности (время, близкое к O (1)), обеспечивая при этом значительную экономию места по сравнению с HashSet. (К сожалению, мы нигде не публиковали эти контрольные цифры.)

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

Хорошее обсуждение открытой адресации и компромиссов производительности с факторами нагрузки можно найти в разделе 3.4.

Седжвик, Роберт и Кевин Уэйн. Алгоритмы, четвертое издание. Аддисон-Уэсли, 2011.

Сайт онлайн-книги находится здесь, но обратите внимание, что печатное издание содержит гораздо больше деталей.

person Stuart Marks    schedule 27.07.2017
comment
имеет ли смысл добавить простой комментарий в код используемого алгоритма? эта единственная строка могла бы сэкономить мне время, пока я копался в почему. Кстати, у HashMap есть куча этих комментариев в деталях реализации ... - person Eugene; 28.07.2017
comment
когда ты сказал обыск, я сразу сделал сердитое лицо. Я явно торопился с вопросом и не думал о других аспектах. Спасибо - person Eugene; 28.07.2017
comment
Когда мы находимся ... намеренно ли в этих неизменяемых коллекциях нет оптимизированных forEach(…) или spliterator() методов? - person Holger; 28.07.2017
comment
@Stuart Вы довольны производительностью линейного зондирования и компромиссом между памятью и временем? Я предполагаю, что для неизменяемых наборов с несколькими элементами линейное зондирование совсем не повредит, но что показывают ваши тесты для неизменяемых наборов, например, 1k, 10k и 100k элементов? Ожидаете ли вы, что эти неизменяемые коллекции будут использоваться для такого большого количества элементов? Если да, то, возможно, вам может помочь схема динамического зондирования ... - person fps; 31.07.2017
comment
@FedericoPeraltaSchaffner Я считаю, что текущие компромиссы между производительностью и пространством являются удовлетворительными, но их, безусловно, можно улучшить. Центр проектирования - это небольшое количество элементов, но производительность ожидаемо масштабируется даже при большом количестве элементов, по-прежнему O (1), хотя и медленнее, чем HashMap. Было бы неплохо провести более тщательное исследование, но меня больше беспокоит плохая производительность при наличии плохих hashCode реализаций. - person Stuart Marks; 01.08.2017