Типичный сценарий использования ThreadLocal — делать его самого static.
Нестатичный ThreadLocal — экзотика, которая как раз и приводит к описанным проблемам.
Кстати, вместо getOrCreate() есть ThreadLocal.initialValue().
Мне нравится ход ваших мыслей, но насчет анонимных классов все равно не соглашусь.
Анонимный класс — вполне определенная конструкция языка, и лямбды таковой не являются.
Может, хотя бы это вас убедит:
Runnable anonymous = new Runnable() {
@Override
public void run() {
System.out.println("Hello from anonymous");
}
};
Runnable lambda = () -> System.out.println("Hello from lambda");
System.out.println(anonymous.getClass().isAnonymousClass()); // true
System.out.println(lambda.getClass().isAnonymousClass()); // false
JIT иногда генерирует nop в машинном коде исключительно в целях выравнивания, а именно:
a) выравнивания backward branch targets в горячих циклах;
b) выравнивания точки входа в метод (т.н. verified entry) для возможности атомарного патчинга, поскольку запись машинного слова в память на x86 гарантировано атомарна только по выровненному адресу.
Пожимаю руку: хороший анализ и правильные выводы. Многие после первого же теста сделали бы вывод, что байткод NOP выполняется N наносекунд :)
По вопросам:
1. Сложно представить, во что вообще JIT может скомпилировать NOP, кроме пустого места. Разве что в машинную инструкцию nop, на которую, в свою очередь, даже процессор не тратит ни такта.
2. Виртуальные методы ни при чем. Во-первых, в данном случае вызов будет девиртуализован. Во-вторых, размер таблицы виртуальных методов зависит только от количества методов, но не от их размера.
3. 8000 и 325 — размеры все-таки в байтах, а не в инструкциях. Почему именно такие, наверное, уже никто не вспомнит — за последние лет 7 они не менялись.
Кстати говоря, я на эти лимиты наталкивался в реальном коде. Исследуя, почему зверски тормозит GeoIP библиотека MaxMind, обнаружил, что в методе regionNameByCode есть огромный switch со всеми возможными вариантами регионов, в результате чего этот метод вообще никогда не компилировался.
Порадовал чем? Там же очередной лжебенчмарк, замеряющий время интерпретации + время JIT-компиляции. Небось, еще и на клиентской 32-битной JVM. Если измерять по-честному, разница получается мизерная, причем то в пользу одного, то в пользу другого варианта, в зависимости от опций JVM (UseCompressedOops).
[sarcasm]Чего уж там, давайте сразу на 10 итераций ручной loop unrolling сделаем — глядишь, будет еще эффективнее.[/sarcasm]
Ничего загадочного. В «оптимизированном» варианте просто-напросто больше операций. Гораздо больше.
Если в простом примере на одной итерации внутреннего цикла, грубо говоря, делается один store (не зависящий от предыдущих), то во втором примере на каждой итерации делается:
1. load указателя на j-ю строку;
2. load длинны массива;
3. bounds check;
4. и два store.
причем эти операции не могут выполняться конвейерно, т.к. зависят от результатов предыдущих операций.
Добавим ко всему этому, что обращение к памяти сильно разнесено, а, значит, кеши и префетчи не работают, куча TLB промахов и всё такое.
У Оракла много разных JVM, так что лучше уточнять, о которой речь.
Про Jazelle писал: раз, два. JVM с JIT ее не используют, т.к. от нее только хуже.
В основе Java SE Embedded лежит Hotspot JVM. Выпускается в нескольких сборках — в зависимости от версии архитектуры (ARM v5, v6, v7) и поддержки VFP. Кроме того, JVM в рантайме определяет поддерживаемый набор инструкций для более оптимальной JIT-компиляции. В частности, версия, собранная под ARM v6, может генерировать код с использованием ARM v7 инструкций.
Полноценно поддерживаются многоядерные CPU. Навороченный JIT-компилятор с промежуточным представлением, глубоким инлайнингом, спекулятивной девиртуализацией и т.д.
В основе Java ME лежит CLDC HI JVM. Тоже выпускается в разных сборках для разных платформ. Прежде всего заточена на маленькие объемы памяти. Поэтому большинство фич включаются/выключаются на этапе сборки под конкретную платформу. Также имеет JIT компилятор, заточенный под ARM, хотя и гораздо проще (можно сказать, однопроходный). Более того, есть сборки и без JIT-компилятора вовсе. Подозреваю, что рекорды по занимаемому месту, достигнуты именно на такой сборке.
1. Oracle Java SE Embedded (в отличие от OpenJDK), действительно, представляет собой оптимизированный под ARM порт JDK, включая С1 (aka Client) JIT компилятор. Тот же самый, что на «большой» Java, только с ARM бэкендом. Поэтому и производительность соответствующая.
Не знаю, как сейчас, но раньше он обгонял Dalvik VM в 2-3 раза на идентичном железе.
2. На слово «java» в cpu features даже не смотрите, оно ничего не значит. Вернее, оно означает, что процессор поддерживает технологию Jazelle DBX, которая безнадежно устарела еще лет 8 назад. Никакая JVM с JIT компилятором ее не использует.
И чего? :) Как нам это поможет отличить объект вложенного класса от enum константы?
enum E {
first,
second {
public String toString() {
return "not first";
}
};
static class Helper { }
public static final Helper helper = new Helper();
}
Я хотел сказать, что проверять, является ли объект enum константой, через obj.getClass().isEnum() — неправильно.
А правильно будет obj instanceof Enum.
Если не секрет, то, реально интересно, что послужило толчком к поиску именно такого, скажем прямо, нетрадиционного решения?
Любознательность :) На самом деле, острой необходимости и не было. Пару раз возникало желание включить/выключить на лету TraceClassLoading, TraceClassUnloading и PrintCompilation. Или еще один раз проверить эффект от смены hashCode под нагрузкой. Но в итоге на production серверы все равно такой хак решили не внедрять.
Проверил у себя — получилось успешно. На каком примере и на какой версии SIGSEGV валится?
С OutOfMemoryError и StackOverflowError есть особенность, что они преаллоцируются на старте JVM (иначе при попытке выполнения честного конструктора может не хватить памяти или стека). Поэтому, например, если запустили Java с -XX:-StackTraceInThrowable, а потом в рантайме выставили в true, то эти ошибки все равно вылетят без трейса.
Не выйдет.
-client / -server вообще не являются флагами JVM.
До JDK 7 эти параметры разбирал launcher (иначе говоря, запускаемый бинарник) и выбирал соответствующую версию libjvm.
Начиная с JDK 7 «серверная» библиотека libjvm включает в себя оба компилятора: C1 и C2. Есть возможность включить так называемую ступенчатую компиляцию ключиком -XX:+TieredCompilation. Но только из командной строки. Установка флага в runtime ни к чему не приведет, т.к. для поддержки ступенчатой компиляции требуется модифицированная версия интерпретатора байткода, которая генерируется лишь единожды при запуске JVM.
Разумеется, не все флаги получится менять на лету. Многие из них обрабатываются лишь один раз на старте JVM. В частности, все, что касается размеров Java Heap, выбора сборщика мусора и т.п. Эффект будет лишь от смены тех флагов, которые проверяются постоянно, например, различные опции трассировки. По-хорошему, все их надо сделать manageable, тогда и не придется плясать с бубном.
Да: когда очередной put() делал слишком много проб, таблица ресайзилась. Критерий «слишком много» каждый может определить сам — идея лишь в том, чтобы поддерживать производительность мапы на приемлемом уровне. Понятно, что с произвольными данными со стороны такой фокус не пройдет. Но в нашем случае, например, порог достигался при более чем 90% заполненности таблицы.
Заниматься мелочёвкой вроде перестановки условий и ручного разворачивания циклов я бы не стал, тем более, что JIT в определённой степени это сам делает. Много на этом не сэкономить: один единственный cache miss будет стоить сотни тактов и перекроет всю выгоду от микрооптимизаций.
Я в свое время экспериментировал с разными алгоритмами open-address hashing и остановился на одной хеш-функции с вариацией quadratic probing (с увеличивающимися шагами: 1, 2, 3...). Такой вариант на порядок лучше linear probing по среднему числу итераций, и, в отличие от двойного хеширования, обладает хоть каким-то cache locality.
К loadFactor'у у меня был другой подход: полное отсутствие loadFactor как такового :) Вместо него считалось число итераций, и, когда оно переваливало за некий порог (скажем, capacity ^ 1/3), таблица увеличивалась.
ThreadLocal— делать его самогоstatic.Нестатичный
ThreadLocal— экзотика, которая как раз и приводит к описанным проблемам.Кстати, вместо
getOrCreate()естьThreadLocal.initialValue().Анонимный класс — вполне определенная конструкция языка, и лямбды таковой не являются.
Может, хотя бы это вас убедит:
Во-первых, с помощью лямбд можно вполне себе элегантно реализовать функции высшего порядка:
Во-вторых, никаких анонимных классов нет.
Лямбды компилируются в
invokedynamicсо вспомогательными методами в том же классе.a) выравнивания backward branch targets в горячих циклах;
b) выравнивания точки входа в метод (т.н. verified entry) для возможности атомарного патчинга, поскольку запись машинного слова в память на x86 гарантировано атомарна только по выровненному адресу.
По вопросам:
1. Сложно представить, во что вообще JIT может скомпилировать NOP, кроме пустого места. Разве что в машинную инструкцию nop, на которую, в свою очередь, даже процессор не тратит ни такта.
2. Виртуальные методы ни при чем. Во-первых, в данном случае вызов будет девиртуализован. Во-вторых, размер таблицы виртуальных методов зависит только от количества методов, но не от их размера.
3. 8000 и 325 — размеры все-таки в байтах, а не в инструкциях. Почему именно такие, наверное, уже никто не вспомнит — за последние лет 7 они не менялись.
Кстати говоря, я на эти лимиты наталкивался в реальном коде. Исследуя, почему зверски тормозит GeoIP библиотека MaxMind, обнаружил, что в методе regionNameByCode есть огромный switch со всеми возможными вариантами регионов, в результате чего этот метод вообще никогда не компилировался.
[sarcasm]Чего уж там, давайте сразу на 10 итераций ручной loop unrolling сделаем — глядишь, будет еще эффективнее.[/sarcasm]
Если в простом примере на одной итерации внутреннего цикла, грубо говоря, делается один store (не зависящий от предыдущих), то во втором примере на каждой итерации делается:
1. load указателя на j-ю строку;
2. load длинны массива;
3. bounds check;
4. и два store.
причем эти операции не могут выполняться конвейерно, т.к. зависят от результатов предыдущих операций.
Добавим ко всему этому, что обращение к памяти сильно разнесено, а, значит, кеши и префетчи не работают, куча TLB промахов и всё такое.
Про Jazelle писал: раз, два. JVM с JIT ее не используют, т.к. от нее только хуже.
В основе Java SE Embedded лежит Hotspot JVM. Выпускается в нескольких сборках — в зависимости от версии архитектуры (ARM v5, v6, v7) и поддержки VFP. Кроме того, JVM в рантайме определяет поддерживаемый набор инструкций для более оптимальной JIT-компиляции. В частности, версия, собранная под ARM v6, может генерировать код с использованием ARM v7 инструкций.
Полноценно поддерживаются многоядерные CPU. Навороченный JIT-компилятор с промежуточным представлением, глубоким инлайнингом, спекулятивной девиртуализацией и т.д.
В основе Java ME лежит CLDC HI JVM. Тоже выпускается в разных сборках для разных платформ. Прежде всего заточена на маленькие объемы памяти. Поэтому большинство фич включаются/выключаются на этапе сборки под конкретную платформу. Также имеет JIT компилятор, заточенный под ARM, хотя и гораздо проще (можно сказать, однопроходный). Более того, есть сборки и без JIT-компилятора вовсе. Подозреваю, что рекорды по занимаемому месту, достигнуты именно на такой сборке.
Не знаю, как сейчас, но раньше он обгонял Dalvik VM в 2-3 раза на идентичном железе.
2. На слово «java» в cpu features даже не смотрите, оно ничего не значит. Вернее, оно означает, что процессор поддерживает технологию Jazelle DBX, которая безнадежно устарела еще лет 8 назад. Никакая JVM с JIT компилятором ее не использует.
Может, в первую очередь нужно зарепортить баг всё-таки на документацию?
E.second.getClass().getEnclosingClass().isEnum()вернет true;E.helper.getClass().getEnclosingClass().isEnum()вернет true.Я хотел сказать, что проверять, является ли объект enum константой, через
obj.getClass().isEnum()— неправильно.А правильно будет
obj instanceof Enum.E.first.getClass().isEnum()вернет true;E.second.getClass().isEnum()вернет false!Любознательность :) На самом деле, острой необходимости и не было. Пару раз возникало желание включить/выключить на лету TraceClassLoading, TraceClassUnloading и PrintCompilation. Или еще один раз проверить эффект от смены hashCode под нагрузкой. Но в итоге на production серверы все равно такой хак решили не внедрять.
С OutOfMemoryError и StackOverflowError есть особенность, что они преаллоцируются на старте JVM (иначе при попытке выполнения честного конструктора может не хватить памяти или стека). Поэтому, например, если запустили Java с -XX:-StackTraceInThrowable, а потом в рантайме выставили в true, то эти ошибки все равно вылетят без трейса.
-client / -server вообще не являются флагами JVM.
До JDK 7 эти параметры разбирал launcher (иначе говоря, запускаемый бинарник) и выбирал соответствующую версию libjvm.
Начиная с JDK 7 «серверная» библиотека libjvm включает в себя оба компилятора: C1 и C2. Есть возможность включить так называемую ступенчатую компиляцию ключиком -XX:+TieredCompilation. Но только из командной строки. Установка флага в runtime ни к чему не приведет, т.к. для поддержки ступенчатой компиляции требуется модифицированная версия интерпретатора байткода, которая генерируется лишь единожды при запуске JVM.
Я в свое время экспериментировал с разными алгоритмами open-address hashing и остановился на одной хеш-функции с вариацией quadratic probing (с увеличивающимися шагами: 1, 2, 3...). Такой вариант на порядок лучше linear probing по среднему числу итераций, и, в отличие от двойного хеширования, обладает хоть каким-то cache locality.
К loadFactor'у у меня был другой подход: полное отсутствие loadFactor как такового :) Вместо него считалось число итераций, и, когда оно переваливало за некий порог (скажем, capacity ^ 1/3), таблица увеличивалась.