Почему не String hashCode() cache 0?

В исходном коде Java для String я заметил, что hashCode кэширует только значения, отличные от 0. Разница в производительности проявляется в следующем фрагменте:

public class Main{
   static void test(String s) {
      long start = System.currentTimeMillis();
      for (int i = 0; i < 10000000; i++) {
         s.hashCode();
      }
      System.out.format("Took %d ms.%n", System.currentTimeMillis() - start);
   }
   public static void main(String[] args) {
      String z = "Allocator redistricts; strict allocator redistricts strictly.";
      test(z);
      test(z.toUpperCase());
   }
}

Выполнение этого в ideone.com дает следующий результат:

Took 1470 ms.
Took 58 ms.

Итак, мои вопросы:

  • Почему не кеш файл hashCode() 0?
  • Какова вероятность того, что строка Java хеширует 0?
  • Каков наилучший способ избежать снижения производительности при пересчете хеш-значения каждый раз для строк с хешем в 0?
  • Это лучший способ кэширования ценностей? (т.е. кэшировать все, кроме одного?)

Для вашего развлечения каждая строка здесь представляет собой строку с хешем до 0:

pollinating sandboxes
amusement & hemophilias
schoolworks = perversive
electrolysissweeteners.net
constitutionalunstableness.net
grinnerslaphappier.org
BLEACHINGFEMININELY.NET
WWW.BUMRACEGOERS.ORG
WWW.RACCOONPRUDENTIALS.NET
Microcomputers: the unredeemed lollipop...
Incentively, my dear, I don't tessellate a derangement.
A person who never yodelled an apology, never preened vocalizing transsexuals.

Ответ 1

Вы ничего не беспокоитесь. Здесь можно подумать над этой проблемой.

Предположим, что у вас есть приложение, которое ничего не делает, кроме как сидеть вокруг хеширования строк в течение всего года. Скажем, это занимает тысячу строк, все в памяти, называет hashCode() на них неоднократно круговым способом, миллион раз, затем получает еще тысячу новых строк и делает это снова.

И предположим, что вероятность того, что строчный хэш-код равен нулю, на самом деле намного больше 1/2 ^ 32. Я уверен, что он несколько больше, чем 1/2 ^ 32, но скажите, что это намного хуже, чем 1/2 ^ 16 (квадратный корень! Теперь намного хуже!).

В этой ситуации у вас есть больше преимуществ от инженеров Oracle, которые улучшают, как эти хэш-коды этих строк кэшируются, чем кто-либо другой. Поэтому вы пишете им и просите их исправить это. И они работают своей магией, так что всякий раз, когда s.hashCode() равен нулю, он мгновенно возвращается (даже в первый раз! 100% -ное улучшение!). И пусть говорят, что они делают это без ухудшения производительности вообще для любого другого случая.

Ура! Теперь ваше приложение... пусть... 0.0015% быстрее!

То, что раньше занимало целый день, занимает всего 23 часа, 57 минут и 48 секунд!

И помните, мы создали сценарий, чтобы дать все возможные выгоды от сомнений, часто до смехотворной степени.

Вам это стоит того?

EDIT:, после публикации этого пару часов назад, я позволил одному из моих процессоров безумно искать двухсловные фразы с нулевыми хэш-кодами. До сих пор это придумали: bequirtle zorillo, хронограммический schtoff, обманчивый cloisterlike, creashaks organzine, drumwood boulderhead, электроаналитический, практичный, и favostely nonconstruible. Это составляет около 2 ^ 35 возможностей, поэтому с отличным распределением мы ожидаем увидеть только 8. Ясно, что к тому времени, когда это будет сделано, у нас будет несколько раз больше, но не слишком великодушно. Что еще более важно, так это то, что у меня появилось несколько интересных имен групп/альбомов! Нет справедливого воровства!

Ответ 2

Он использует 0, чтобы указать "Я еще не разработал хэш-код". Альтернативой было бы использование отдельного булевского флага, который занимал бы больше памяти. (Или, конечно, не кэшировать хэш-код вообще.)

Я не ожидаю, что много строк hash равно 0; возможно, было бы разумно, чтобы хэширующая программа сознательно избегала 0 (например, переводит хэш от 0 до 1 и кеширует это). Это увеличит количество столкновений, но не позволит перефразировать. Это слишком поздно, чтобы сделать это, хотя, поскольку алгоритм hashCode String явно документирован.

Что касается того, является ли это хорошей идеей в целом: это, безусловно, эффективный механизм кэширования, и может (см. править) быть еще лучше с изменением, чтобы избежать переименования значений, которые заканчиваются хешем 0. Лично я бы был заинтересованный, чтобы увидеть данные, которые заставили Sun поверить, что это стоило делать в первую очередь - он занимал дополнительные 4 байта для каждой строки, когда-либо созданной, однако часто или редко ее хэшировал, и единственное преимущество - для строк, которые хэшируются больше чем один раз.

РЕДАКТИРОВАТЬ: Как отмечает КевинБ в комментарии в другом месте, предложение "избегать 0" выше может иметь чистую стоимость, потому что оно помогает в очень редком случае, но требует дополнительного сравнения для каждого расчета хэша.

Ответ 3

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

Если у вас было две переменные, например, сам cachedHashCode и isHashCodeCalculated boolean, чтобы указать, был ли подсчитан cachedHashCode, вам понадобится синхронизация потоков для работы в многопоточной среде. И синхронизация будет плохой для производительности, тем более, что строки часто используются повторно в нескольких потоках.

Мое понимание модели памяти Java немного отрывочно, но здесь примерно то, что происходит:

  • Когда несколько потоков обращаются к переменной (например, кэшированный хэш-код), нет гарантии, что каждый поток увидит последнее значение. Если переменная начинается с нуля, тогда A обновляет ее (устанавливает ее на ненулевое значение), затем поток B читает ее вскоре после этого, поток B все еще может видеть нулевое значение.

  • Еще одна проблема с доступом к общим значениям из нескольких потоков (без синхронизации) - вы можете попытаться использовать объект, который был только частично инициализирован (построение объекта не является атомарным процессом). Многопоточные чтения и записи 64-битных примитивов, таких как longs и double, не обязательно являются атомарными, поэтому, если два потока попытаются прочитать и изменить значение длинного или двойного, один поток может в конечном итоге увидеть что-то странное и частично установленное, Или что-то в этом роде. Существуют аналогичные проблемы, если вы пытаетесь использовать две переменные вместе, например, cachedHashCode и isHashCodeCalculated - поток может легко прийти и увидеть последнюю версию одной из этих переменных, но более старую версию другой.

  • Обычный способ обойти эти проблемы с несколькими потоками - использовать синхронизацию. Например, вы можете поместить весь доступ к кэшированному хэш-коду внутри синхронизированного блока или использовать ключевое слово volatile (хотя будьте осторожны с этим, потому что семантика немного запутанна).

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

  • Java-примитивы, которые 32-битные или менее, например, int, являются особыми. В отличие от, скажем, длинного (64-битного значения), вы можете быть уверены, что никогда не будете читать частично инициализированное значение int (32 бита). Когда вы читаете int без синхронизации, вы не можете быть уверены, что получите последнее установленное значение, но можете быть уверены, что полученное вами значение является значением, которое явно было задано в какой-то момент вашим потоком или другой поток.

Механизм кэширования hashCode в java.lang.String настроен так, чтобы полагаться на пункт 5 выше. Вы можете понять это лучше, посмотрев на источник java.lang.String.hashCode(). В принципе, при одновременном вызове хэш-кода с несколькими потоками хэш-код может закончиться вычислением несколько раз (либо если вычисленное значение равно нулю, либо если несколько потоков сразу обращаются в hashCode и оба видят нулевое кэшированное значение), но вы можете быть уверены, что hashCode() всегда будет возвращать одно и то же значение. Таким образом, он надежный, и он тоже работает (потому что нет синхронизации для работы в качестве узкого места в многопоточных средах).

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

Ответ 4

0 не кэшируется, поскольку реализация интерпретирует кешированное значение 0 как "кешированное значение, еще не инициализированное". Альтернативой было бы использовать java.lang.Integer, где null подразумевал, что значение еще не кэшировано. Однако это означало бы дополнительные накладные расходы на хранение.

Что касается вероятности того, что хэш-код String вычисляется как 0, я бы сказал, что вероятность довольно низкая и может произойти в следующих случаях:

  • Строка пуста (хотя повторное вычисление этого хэш-кода каждый раз эффективно O (1)).
  • Переполнение происходит, когда конечный вычисленный хэш-код равен 0 (e.g. Integer.MAX_VALUE + h(c1) + h(c2) + ... h(cn) == 0).
  • Строка содержит только символ Unicode 0. Очень маловероятно, так как это управляющий символ без смысла, кроме как в "бумажной ленте мира" (!):

От Wikipedia:

Код 0 (кодовое имя ASCII NUL) является особый случай. В бумажной ленте это если нет отверстий. это удобно рассматривать это как заполнение символ без значения.

Ответ 5

Это хороший вопрос, связанный с уязвимостью .

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

Ответ 6

  • Почему не кеш файл hashCode() 0?

Значение нуля зарезервировано как означающее "хеш-код не кэшируется".

  • Какова вероятность того, что строка Java хеширует 0?

Согласно Javadoc, формула для хэш-кода String:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

с использованием int арифметики, где s[i] - i-й символ строки, а n - длина строки. (Хэш пустой строки определяется как нулевой как частный случай.)

Моя интуиция заключается в том, что функция hashcode, как указано выше, дает равномерный спред для хеш-значений String в диапазоне значений int. Равномерный спрэд, который будет означать, что вероятность случайного генерирования хеширования String равна нулю в 2 ^ 32.

  • Каков наилучший способ избежать снижения производительности при пересчете хеш-значения каждый раз для строк с хешем в 0?

Лучшая стратегия - игнорировать проблему. Если вы неоднократно хешируете одно и то же значение String, в вашем алгоритме есть что-то довольно странное.

  • Это лучший способ кэширования ценностей? (т.е. кэшировать все, кроме одного?)

Это компромисс между пространством и временем. AFAIK, альтернативы:

  • Добавьте флаг cached к каждому объекту String, сделав каждую строку Java еще одним словом.

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

  • Не кэшируйте хэш-коды на строках вообще.

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

(Обратите внимание, что существует два "общих" значения String, хэш которых равен нулю, пустая строка и строка, состоящая только из символа NUL. Однако стоимость вычисления хэш-кодов для этих значений мала по сравнению со стоимостью вычисления хэш-кода для типичного строкового значения.)

Ответ 7

Ну, ребята, он держит 0, потому что, если это нулевая длина, он все равно будет равен нулю.

И это не займет много времени, чтобы понять, что len равен нулю, и поэтому должен быть hashcode.

Итак, для вашего кода-reviewz! Вот он во всей его славе Java 8:

 public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

Как вы можете видеть, это всегда будет возвращать быстрый ноль, если строка пуста:

  if (h == 0 && value.length > 0) ...

Ответ 8

Рекомендация "избегать 0", по-видимому, рекомендуется рекомендовать в качестве лучшей практики, поскольку она помогает решить настоящую проблему (серьезно неожиданное ухудшение производительности в конструктивных случаях, которое может быть доступно злоумышленнику) для скудной стоимости операции ветки до записи. Существует некоторая оставшаяся "неожиданная деградация производительности", которая может быть реализована, если единственное, что входит в набор хэш, в специальное скорректированное значение. Но в худшем случае это ухудшение в 2 раза, а не без ограничений.

Конечно, реализация String не может быть изменена, но нет необходимости увековечивать проблему.