Найти в Дзене
Nuances of programming

Микрооптимизации в Java. Enum - хороший, красивый и медленный

Оглавление

Источник: 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() с кэшированным полем.Это позволит вам уменьшить скорость распределения.

Вот исходный код тестов производительности  —  можете попробовать их самостоятельно.

Спасибо за внимание!

Читайте также:

Читайте нас в Telegram, VK

Перевод статьи Dmytro Dumanskiy: “Micro optimizations in Java. Good, nice and slow Enum”