Источник: Nuances of Programming
Перечисления — важная часть каждого приложения, которое представляет собой чуть больше, чем “Hello World”. Они повсюду. И, на самом деле, перечисления очень полезны: они ограничивают ввод, позволяют сравнивать значения по ссылке, обеспечивают проверку во время компиляции и облегчают чтение кода. Однако применение перечислений приводит к некоторым проблемам с производительностью. И мы рассмотрим их в этом посте.
Рассматривайте весь приведенный ниже код с точки зрения производительности. Не зацикливайтесь на цифрах, это просто примеры для демонстрации необходимых утверждений.
Enum.valueOf
Одна из самых популярных функций в Blynk называется свойство виджета. Это когда вы можете изменить внешний вид визуального виджета в режиме реального времени, отправив с аппаратного обеспечения такую команду:
Blynk.setProperty(v1, “LABEL”, “My New Label”);
Давайте взглянем на существующее перечисление с нашего сервера:
А теперь взгляните на этот базовый код:
String inWidgetProperty;
...
WidgetProperty property = WidgetProperty.valueOf(inWidgetProperty)
Видите, что тут не так?
Давайте перепишем приведенный выше код и создадим наш собственный метод на основе оператора switch:
Этот метод не совсем такой же, так как он не выбрасывает исключение IllegalArgumentException в случае неизвестного значения. Однако это делается специально, потому что, как вам известно, выбрасывание и создание исключения (из-за трассировки стека) — дорогостоящая операция.
Давайте сделаем тест производительности:
Результаты (более низкий балл означает большее быстродействие):
Хм… Наш пользовательский метод с оператором switch работает в два раза быстрее Enum.valueOf. Что-то здесь не так. Надо нырнуть глубже.
Как вы, вероятно, знаете, классы enum не содержат метод valueOf(). Компилятор генерирует его во время компиляции, поэтому, чтобы разобраться в происходящем, нам нужно заглянуть в байт-код сгенерированного класса:
Похоже, что метод java.lang.Enum.valueOf вызывается внутри метода WidgetPtoperty.valueOf. К счастью, этот метод присутствует в коде Java. Давайте проверим это:
И enumConstantDirectory:
Ага, значит, все, что у нас здесь есть, — это обычная изменчивая хэш-карта HashMap с именами перечислений в качестве ключей. И похоже, что в этом конкретном случае оператор switch просто превосходит метод HashMap.get().
Добавим valueOfс собственной реализацией HashMap и тоже протестируем:
Результаты (более низкий балл означает большее быстродействие, результаты enumMapприкреплены к предыдущему тесту):
Похоже, собственная реализация HashMap работает быстрее,чем обычный Enum.valueOf(), но все же медленнее, чем оператор switch.
Перепишем тест, чтобы получить случайный ввод:
Результаты (более низкий балл = большее быстродействие):
Любопытно… Теперь, при случайном вводе, оператор switch работает в два раза медленнее. Похоже, что разница связана с предсказанием переходов. И самым быстрым вариантом будет наша собственная реализация хэша карт, так как она влечет за собой меньше проверок по сравнению с Enum.valueOf.
Enum.values()
Теперь рассмотрим другой пример. Он не так популярен, как приведенный первым, но все же присутствует во многих проектах:
for (WidgetProperty property : WidgetProperty.values()) {
...
}
Видите, что здесь не так?
Опять же, метод Enum.values() генерируется во время компиляции, поэтому, чтобы разобраться, что там не так, мы должны еще раз взглянуть на байт-код:
Не нужно быть экспертом по байт-коду, чтобы увидеть вызов метода .clone(). Таким образом, каждый раз, когда вы вызываете метод Enum.values(), всегда будет копироваться массив со значениями enum.
Решение довольно очевидно — мы можем кэшировать результат метода Enum.values() следующим образом:
public static final WidgetProperty[] values = values();
И взамен использовать поле кэша. Давайте проверим это с помощью теста производительности:
Я применил в этом тесте цикл, потому что обычно в цикле используется Enum.values().
Результаты (более низкий балл означает большее быстродействие):
Разница в производительности здесь около 30%. Это потому, что итерация цикла также требует кое-каких ресурсов ЦПУ. Однако, что более важно: после внесенного исправления никакого распределения не происходит, это даже более важно, чем улучшение показателей скорости.
Вы можете задаться вопросом: почемуEnum.values() реализован именно таким образом? Объяснение довольно простое. Без метода copy() любой желающий может изменить содержимое возвращаемого массива. А это может привести ко многим проблемам. Поэтому возвращать одну и ту же ссылку на массив со значениями, которые можно легко заменить, — не самая лучшая идея. Неизменяемые массивы могли бы решить эту проблему, но в Java их нет.
Да, такое изменение немного снижает читабельность, и вовсе не нужно применять его ко всякому перечислению. Это необходимо только для действительно горячих путей.
Для примера можете ознакомиться с этим пулл-реквестом jdbc-драйвера для Apache Cassandra или этим пулл-реквестом для HTTP-сервера.
Заключение
- Рассмотрите возможность воспользоваться собственным кэшем карт метода Enum.valueOf(). В качестве дополнительного бонуса можно избежать возникновения исключения при неправильном вводе. Это может изменить правила игры для некоторых потоков.
- Также подойдет Enum.values() с кэшированным полем.Это позволит вам уменьшить скорость распределения.
Вот исходный код тестов производительности — можете попробовать их самостоятельно.
Спасибо за внимание!
Читайте также:
Перевод статьи Dmytro Dumanskiy: “Micro optimizations in Java. Good, nice and slow Enum”