Почему HashMap resize () снова при указании точной емкости? - PullRequest
0 голосов
/ 05 октября 2018

Код говорит больше, чем слова, поэтому:

final int size = 100;
Map<Integer, String> m = new HashMap<>(size);
for (int i = 0; i < size; i++) m.put(i, String.valueOf(i));

Почему HashMap внутренне вызывает resize() 21 2раз! (Благодарим Андреаса за то, что он определил, что JVM использует HashMaps внутри, 19 из 21 запроса были из других процессов)

Два вызова resize() все еще неприемлемы для моего приложения.Мне нужно оптимизировать это.

Если я новый Java-разработчик, мое первое интуитивное предположение о том, что означает «емкость» в конструкторе HashMap, заключается в том, что это емкость для числа элементов, которые я (потребитель)HashMap) собираюсь положить в карту.Но это не так.

Если я хочу оптимизировать использование HashMap, чтобы ему вообще не нужно было изменять размер, тогда мне нужно достаточно глубоко знать внутреннее содержимое HashMap, чтобы точно знать, насколько разреженным являетсяМассив HashMap должен быть.Это странно, на мой взгляд.HashMap должен неявно сделать это для вас.В этом весь смысл инкапсуляции в ООП.

Примечание: я подтвердил, что resize () является узким местом для моего варианта использования приложений, поэтому моя цель - уменьшить количество вызововдля изменения размера ().

Вопрос:

Если я знаю точное количество записей, которые я собираюсь поместить на карту заранее.Какую емкость я выбрал, чтобы предотвратить любые дополнительные вызовы resize() операции?Что-то вроде size * 10?Я также хотел бы получить некоторые сведения о том, почему HashMap спроектирован таким образом.

Редактировать: меня часто спрашивают, почему необходима такая оптимизация.Мое приложение тратит нетривиальное количество процессорного времени в hashmap.resize ().Хэш-карты, которые использует мое приложение, инициализируются с емкостью, равной количеству элементов, которые мы вставили в него.Поэтому, если мы сможем уменьшить вызовы resize () (выбрав лучшую начальную емкость), производительность моего приложения улучшится.

Ответы [ 5 ]

0 голосов
/ 06 октября 2018

Здесь много чудесных ответов.Я очень признателен за вклад.

Я решил не изобретать это колесо заново, потому что, похоже, Google уже решил эту проблему.

Я собираюсь использовать служебный метод Maps.newHashMapWithExpectedSize(int)от Библиотека гуавы Google

0 голосов
/ 05 октября 2018
  • Изменение размера является неотъемлемой частью работы хэш-карты по поддержанию низкого коэффициента загрузки.

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


Однако, в вашем конкретном случае, коллизия не является проблемой, только изменение размера hashmapis.

Размер хеш-карты обычно изменяется при коэффициенте нагрузки 0,75 (= 3/4 во фракции).Используя эту информацию, вы можете настроить хэш-карту в 4/3 раз больше записей, которые необходимо сохранить.


Относительно вашего несогласия с нарушением инкапсуляции:

Я согласен с вами это спорно.

1024 * Вы можете сказать, что было бы лучше, если бы capacity представлял количество записей, до которой изменения размера не бывает, а неколичество максимально возможных записей, которые могут быть сохранены в хэш-карте - и я склонен с вами согласиться.

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

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

0 голосов
/ 05 октября 2018

В случае сомнений прочитайте документацию.Документы для HashMap достаточно хорошо объясняют компромиссы initial capacity и load-factor.

Согласно документации, если initCapacity = (maxEntries / loadFactor) + 1, то при добавлении записей операции перефразирования не выполняются.Где в этом случае maxEntries - это 100, как вы укажете, а loadFactor будет коэффициентом загрузки по умолчанию .75.

Но, кроме установки начального размера, чтобы избежать перефразировки(resize()) вам следует внимательно прочитать документацию HashMap, чтобы правильно настроить ее, принимая во внимание как начальную емкость, так и коэффициент загрузки.

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

final float loadFactor = 0.5;
final int maxEntries   = 100;
final int initCapacity = (int) maxEntries / loadFactor + 1;
new HashMap<>(initCapacity, loadFactor);

(выделение мое)

Экземпляр HashMap имеет два параметра, которые влияют на егопроизводительность: начальная емкость и коэффициент загрузки.Емкость - это количество сегментов в хэш-таблице, а начальная емкость - это просто емкость на момент создания хеш-таблицы.Коэффициент загрузки - это мера того, насколько полной хеш-таблице разрешено получать до того, как ее емкость будет автоматически увеличена.Когда количество записей в хеш-таблице превышает произведение коэффициента загрузки и текущей емкости, хеш-таблица перефразируется (то есть внутренние структуры данных перестраиваются), так что хеш-таблица имеет примерно вдвое больше сегментов.
...
Как правило, коэффициент загрузки по умолчанию (.75) предлагает хороший компромисс между временными и пространственными затратами.Более высокие значения уменьшают затраты пространства, но увеличивают стоимость поиска (отражается в большинстве операций класса HashMap, включая get и put).Ожидаемое количество записей на карте и коэффициент загрузки должны учитываться при настройке начальной емкости, чтобы минимизировать количество операций перефразировки. Если начальная емкость больше, чем максимальное количество записей, деленное на коэффициент загрузки, операции перефразировки никогда не будут выполняться.

0 голосов
/ 05 октября 2018

Это легко доказать:

private static <K, V> void debugResize(Map<K, V> map, K key, V value) throws Throwable {

    Field table = map.getClass().getDeclaredField("table");
    AccessibleObject.setAccessible(new Field[] { table }, true);
    Object[] nodes = ((Object[]) table.get(map));

    // first put
    if (nodes == null) {
        map.put(key, value);
        return;
    }

    map.put(key, value);

    Field field = map.getClass().getDeclaredField("table");
    AccessibleObject.setAccessible(new Field[] { field }, true);
    int x = ((Object[]) field.get(map)).length;
    if (nodes.length != x) {
        ++currentResizeCalls;
    }
}

И какое-то использование:

static int currentResizeCalls = 0;

public static void main(String[] args) throws Throwable {

    int size = 100;
    Map<Integer, String> m = new HashMap<>(size);
    for (int i = 0; i < size; i++) {
        DeleteMe.debugResize(m, i, String.valueOf(i));
    }

    System.out.println(DeleteMe.currentResizeCalls);
}     

Я регистрирую только время, которое требуется, когда resize на самом деле изменяет размер, потому что первый вызов инициализируется;как указано в документации:

Инициализирует или удваивает размер таблицы


Второй из ваших пунктов гораздо интереснее. A HashMap определяет capacity, теперь какая емкость?И это не так очевидно:

Для HashMap, capacity - это число buckets до изменения размера, для ConcurrentHashMap это количество записей до изменения размера.

Таким образом, чтобы не вызывать resize внутри, в случае HashMap используйте формулу:

(int)(1.0 + (long)initialCapacity / LOAD_FACTOR)

Но это далеко не идеал, скажем, вы хотите 1024 записей без изменения размера,используя эту формулу, вы получаете 1367 сегментов, которые внутренне округляются до степени двойки, таким образом, 2048 - ну, намного больше, чем вы просили.

Для CHM укажите размер напрямую .Легко доказать, используя одну единственную модификацию в предыдущем коде:

 // use CHM instead of HashMap
 Map<Integer, String> m = new ConcurrentHashMap<>(size);

Это приведет к zero изменениям размера, которые фактически удваивают массив.Но иногда даже CHM внутренний код сбивает с толку и требует небольшого количества исправлений.

0 голосов
/ 05 октября 2018

Коэффициент загрузки по умолчанию - 0.75, то есть 3/4, что означает, что размер внутренней хеш-таблицы будет изменен после добавления 75 из 100 значений.

FYI: resize() вызывается только дважды .Один раз, когда добавляется первое значение, и один раз, когда он заполняется на 75%.

Чтобы предотвратить изменение размера, необходимо убедиться, что сотое значение не приведет к изменению размера, т. Е. size <= capacity * 0.75 aka size <= capacity * 3/4 akasize * 4/3 <= capacity, чтобы быть уверенным:

capacity = size * 4/3 + 1

С size = 100, что означает capacity = 134.

...