Таинственный FrontCache

    Всё началось с того, что я в очередной раз ковырял в Eclipse Memory Analyzer дамп памяти Java-приложения и увидел такую интересную вещь:

    С кодом HashMap я знаком весьма неплохо, но вложенного класса FrontCache никогда там не видел. Может, с последним обновлением JDK мне прислали обновлённый HashMap? Я заглянул в исходники, но слова «front» там не обнаружилось. Стало интересно, откуда же этот класс берётся и что он делает.

    Порывшись в JRE (у меня 1.7u10, но и в последних 1.6 тоже это есть), я нашёл любопытный джарик: alt-rt.jar, в котором и обнаружился HashMap$FrontCache.class, а также несколько других классов (LinkedHashMap, TreeMap, BigDecimal, BigInteger, MutableBigInteger и их вложенные классы). Обычно эти классы подключаются из rt.jar. Почему же они стали грузиться из этого загадочного джарика?

    Я вспомнил, что недавно экспериментировал с опциями Java-машины и, в частности, включил -XX:+AggressiveOpts. На сайте Оракла про этот ключ написано скупо:
    Turn on point performance compiler optimizations that are expected to be default in upcoming releases.

    На Хабре была попытка объяснить эту опцию подробнее, мол, это комбинация других ключиков. Порывшись в исходниках OpenJDK 7-й версии я понял, что ключиками дело не ограничивается. Вот что мы видим в hotspot/src/share/vm/runtime/arguments.cpp:
    jint Arguments::parse_vm_init_args(const JavaVMInitArgs* args) {
    ...
      if (AggressiveOpts) {
        // Insert alt-rt.jar between user-specified bootclasspath
        // prefix and the default bootclasspath.  os::set_boot_path()
        // uses meta_index_dir as the default bootclasspath directory.
        const char* altclasses_jar = "alt-rt.jar";
        size_t altclasses_path_len = strlen(get_meta_index_dir()) + 1 +
                                     strlen(altclasses_jar);
        char* altclasses_path = NEW_C_HEAP_ARRAY(char, altclasses_path_len);
        strcpy(altclasses_path, get_meta_index_dir());
        strcat(altclasses_path, altclasses_jar);
        scp.add_suffix_to_prefix(altclasses_path);
        scp_assembly_required = true;
        FREE_C_HEAP_ARRAY(char, altclasses_path);
      }
    

    Ага! С этой опцией действительно помимо всего прочего добавляется alt-rt.jar. В этом можно убедиться и из своего приложения, воспользовавшись System.getProperty("sun.boot.class.path"). Таким образом, для упомянутых классов при включенных AggressiveOpts реализация меняется.

    Но в чём различие в реализации HashMap? Я стал искать изменённый исходник, но тут меня постигла неудача. Выяснилось, что этот jar собирается с помощью jdk/make/altclasses/Makefile, а каталог с исходниками обозначен как
    ALTCLASSES_SRCDIR = $(CLOSED_SRC)/share/altclasses

    Это запахло не очень хорошо, и файл jdk/make/common/Defs.gmk подтвердил мои опасения:
    # Files that cannot be included in the OpenJDK distribution are
    # collected under a parent directory which contains just those files.
    ifndef CLOSED_SRC
      CLOSED_SRC  = $(BUILDDIR)/../src/closed
    endif

    Разумеется, указанного каталога в комплекте не идёт. На всякий случай я выкачал JDK 8, но там ситуация была не лучше. Oracle прячет альтернативный HashMap.

    Порывшись в интернете, я напоролся на проект, который обнадёжил меня, но напрасно. На главной написано, что там есть исходники классов из jre\lib\alt-rt.jar, по факту же там стандартная реализация HashMap и остальных классов. Видимо, автор не разобрался, что есть два варианта.

    Остался один способ — дизассемблировать байткод (javap -c -private) и почитать его так. Чтобы было проще, я дизассемблировал обычный и альтернативный HashMap, парой регекспов выкинул несущественные вещи и сравнил diff'ом. Сперва всё выглядело довольно страшно, но потом я догадался, что код обычного и альтернативного HashMap эволюционировали независимо, поэтому сравнивать альтернативный HashMap надо с общим предком, коим оказался HashMap из последних апдейтов 6-й JDK. Тут картина стала гораздо понятней, не потребовалось даже специальных инструментов для декомпиляции в Java-код. В HashMap действительно появилось HashMap.FrontCache frontCache, которое инициализируется в конструкторе. Конструктор FrontCache принимает capacity — количество элементов в основной хэш-таблице. В метод get(Object key) добавлен примерно такой код:
    if(frontCache != null) {
    	V value = frontCache.get(key);
    	if(value != null) return value;
    }

    В метод put(K key, V value) добавлено следующее:
    if(frontCache != null) {
    	frontCache.put(key, value);
    }

    Есть технические изменения и в других методах, но суть здесь. Ясно, что frontCache — какое-то альтернативное (видимо, более быстрое) хранилище данных. При запросе элемента он в первую очередь ищется в frontCache, а при занесении нового заносится и в frontCache, и в обычную хэш-таблицу. Что же такого ускоряет класс FrontCache? Вот как выглядят самые важные методы из него:
    private class FrontCache {
    	private Object[] cache;
    	private int bitMask;
    	
    	public FrontCache(int capacity) {
    		this.cache = new Object[capacity];
    		this.bitMask = makeBitMask(capacity);
    	}
    
    	public int makeBitMask(int capacity) {
    		return -1 << (32 - Integer.numberOfLeadingZeros(capacity - 1));
    	}
    	
    	public boolean inRange(int value) {
    		return (value & bitMask) == 0;
    	}
    	
    	public V get(Object key) {
    		if(key instanceof Integer) {
    			int intKey = ((Integer)key).intValue();
    			if(inRange(intKey)) {
    				return (V) cache[intKey];
    			}
    		}
    		return null;
    	}
    	
    	private void put(K key, V value) {
    		if(key instanceof Integer) {
    			int intKey = ((Integer)key).intValue();
    			if(inRange(intKey)) {
    				cache[intKey] = value;
    			}
    		}
    	}
    }
    Прочие методы служат для удаления элементов, изменения размера кэша и т. д. Но идея из приведённого кода понятна: если ключ — Integer и он попадает в диапазон от 0 до capacity-1 (проверка оригинально соптимизирована), то значение просто заносится в массив по индексу с соответствующим порядковым номером без всяких преобразований и хэш-функций.

    Если же ключи не являются целыми числами, то FrontCache просто бесполезен. Однако массив всё равно выделяется и проверки делаются при каждой операции с HashMap.

    На самом деле теряется даже больше памяти, так как HashMap.Entry имеет метод setValue, который теперь должен обновлять и значение в FrontCache при необходимости. Поэтому в каждый Entry добавлена ссылка на сам HashMap, что может добавлять до 8 лишних байт на запись.

    Открытие несколько шокировало меня. Последнее время мы во славу Trove почти не пользуемся ключами вроде Integer, поэтому получилось, что оптимизация только напрасно ест время и память. В общем, я решил, что AggressiveOpts лучше отключить. Конечно, я не разбирался, что там изменилось в TreeMap и математических классах (в LinkedHashMap изменения косметические, связанные как раз с изменениями в HashMap.Entry для поддержки FrontCache). Будьте осторожнее и не используйте опций, смысла которых вы до конца не понимаете.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 23

      +29
      Так вот почему они скрывают их реализацию! Им СТЫДНО :)
        0
        Можно попробовать заюзать декомпилятор, чтобы проще было листинги анализировать.
        jad геренегил вполне удобоваримые листинги.
            0
            Спасибо! С интересом глянул.
            Кстати, в jre есть оптимизация для Integer'ов — для объектов, хранящих значения из интервала -127..127 используется объектный пул.
              +1
              это есть ещё со времён java 5 — и более того, в sun/oracle jre можно расширять верхнюю границу при помощи ключа
              -Djava.lang.Integer.IntegerCache.high=max

              и ещё говориться о
              /**
                   * Cache to support the object identity semantics of autoboxing for values between 
                   * -128 and 127 (inclusive) as required by JLS.
                   *
                   * The cache is initialized on first usage. During VM initialization the
                   * getAndRemoveCacheProperties method may be used to get and remove any system
                   * properites that configure the cache size. At this time, the size of the
                   * cache may be controlled by the vm option -XX:AutoBoxCacheMax=<size>.
                   */
              
              0
              Ага, спасибо. Я использовал когда-то jad, но лень было его устанавливать. Пару десятков строчек проще и надёжнее декомпилировать по ассемблеру в уме :-) Похоже, jad генерики совсем не любит и скобки ставит скупо, лишь по мере необходимости. Такое я воспринимаю хуже, чем ассемблер, мне всё время кажется, что у сдвига приоритет выше:
               return -1 << 32 - Integer.numberOfLeadingZeros(i - 1);
                0
                Есть ещё jd и fernflower — они лучше понимают байт-код новых версий java.
            0
            По поводу AggressiveOpts
            if (AggressiveOpts)

            таких вот условий по коду не мало. Поэтому для упрощения я не включал их в пост.
              +1
              На мой взгляд, это неправильный подход. Честно говоря, я думал, что вы добросовестно заблуждаетесь. В вашем посте написано:
              Не в том плане, что она резко увеличивает производительность Вашего приложения, а в том смысле, что она всего лишь изменяет значения других опций.
              Выражение «всего лишь изменяет значения других опций» — это не упрощение, это либо заблуждение, либо ложь. Может, добавите какой-нибудь минимальный комментарий про альтернативную реализацию некоторых классов? Или хотя бы допишете, что не только в перечисленных опциях дело. А то ваш пост хорошо ищется в гугле, люди читают и верят вам :-)
                +1
                Ну в целом, Вы правы. Добавил ремарку.
              0
              А есть где-нибудь сравнение быстродействия этих коллекций?
                +3
                Власти Оракл скрывает!!!
                  0
                  Вот, кстати, вы спросили бы начальство, зачем они это делают :-)
                    0
                    А зачем спрашивать. Мы знаем, но если молчать и надувать щеки — то можно тешить свое ЧСВ. :)))
                • UFO just landed and posted this here
                    0
                    Этот фильм до сих пор популярный…
                      0
                      Шутки-шутками, но всё же было бы интересно услышать предысторию к чему это делалось и зачем.

                      Я понимаю, что это некоторый эксперимент, ибо AggressiveOpts толсто намекают на это. Собственно какой аспект хотелось обкатать, или в каких случаях это должно работать лучше?
                      • UFO just landed and posted this here
                          0
                          То что nvidia подкручивает драйвера для тестов все уже привыкли, а как Oracle крутит что-то, да еще при использовании специальных опций, так низя.
                      +2
                      Про java.math.* скажу, что это было временное хранение. Новый убыстренный java.math.* уже давно в java8.
                        +2
                        а вот еще кусочек мозаики.
                          –1
                          Скорей всего эта реализация была взята из JRockit-а, ибо Оракл давно собиралась перенести в JVM «лучшие черты JRockit».
                          А вот чисто теоретиццки, возникала ли когда-нибудь идея сделать native имплементацию части классов rt.jar? Всякие байткод компиляторы типа excelcior не в счет. Именно ручками на сях.
                            0
                            Месье просто обязан для себя открыть intrinsic в java, не говоря уже о jit.

                          Only users with full accounts can post comments. Log in, please.