Перечисления — важная часть каждого приложения, которое представляет собой чуть больше, чем “Hello World”. Они повсюду. И, на самом деле, перечисления очень полезны: они ограничивают ввод, позволяют сравнивать значения по ссылке, обеспечивают проверку во время компиляции и облегчают чтение кода. Однако применение перечислений приводит к некоторым проблемам с производительностью. И мы рассмотрим их в этом посте.
Рассматривайте весь приведенный ниже код с точки зрения производительности. Не зацикливайтесь на цифрах, это просто примеры для демонстрации необходимых утверждений.
Enum.valueOf
Одна из самых популярных функций в Blynk называется свойство виджета. Это когда вы можете изменить внешний вид визуального виджета в режиме реального времени, отправив с аппаратного обеспечения такую команду:
Blynk.setProperty(v1, “LABEL”, “My New Label”);
Давайте взглянем на существующее перечисление с нашего сервера:
enum WidgetProperty {
LABEL,
COLOR,
ON_LABEL,
OFF_LABEL,
MIN,
MAX
}
А теперь взгляните на этот базовый код:
String inWidgetProperty;
...
WidgetProperty property = WidgetProperty.valueOf(inWidgetProperty)
Видите, что тут не так?
Давайте перепишем приведенный выше код и создадим наш собственный метод на основе оператора switch
:
public static WidgetProperty valueOfSwitch(String value) {
switch (value) {
case "LABEL" :
return WidgetProperty.LABEL;
case "COLOR" :
return WidgetProperty.COLOR;
case "ON_LABEL" :
return WidgetProperty.ON_LABEL;
case "OFF_LABEL" :
return WidgetProperty.OFF_LABEL;
case "MIN" :
return WidgetProperty.MIN;
case "MAX" :
return WidgetProperty.MAX;
}
return null;
}
Этот метод не совсем такой же, так как он не выбрасывает исключение IllegalArgumentException
в случае неизвестного значения. Однако это делается специально, потому что, как вам известно, выбрасывание и создание исключения (из-за трассировки стека) — дорогостоящая операция.
Давайте сделаем тест производительности:
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Measurement(iterations = 10, time = 1)
public class EnumValueOf {
@Param({"LABEL", "ON_LABEL", "MAX"})
String strParams;
@Benchmark
public WidgetProperty enumValueOf() {
return WidgetProperty.valueOf(strParams);
}
@Benchmark
public WidgetProperty enumSwitch() {
return WidgetProperty.valueOfSwitch(strParams);
}
}
Результаты (более низкий балл означает большее быстродействие):
JDK 11.0.8, OpenJDK 64-Bit Server VM, 11.0.8+10
Benchmark (strParams) Mode Cnt Score Error Units
EnumValueOf.enumSwitch LABEL avgt 80 7.149 ± 0.030 ns/op
EnumValueOf.enumSwitch ON_LABEL avgt 80 7.758 ± 0.077 ns/op
EnumValueOf.enumSwitch MAX avgt 80 6.948 ± 0.083 ns/op
EnumValueOf.enumValueOf LABEL avgt 80 15.659 ± 0.450 ns/op
EnumValueOf.enumValueOf ON_LABEL avgt 80 14.734 ± 0.542 ns/op
EnumValueOf.enumValueOf MAX avgt 80 15.153 ± 0.578 ns/op
Хм… Наш пользовательский метод с оператором switch
работает в два раза быстрее Enum.valueOf
. Что-то здесь не так. Надо нырнуть глубже.
Как вы, вероятно, знаете, классы enum
не содержат метод valueOf()
. Компилятор генерирует его во время компиляции, поэтому, чтобы разобраться в происходящем, нам нужно заглянуть в байт-код сгенерированного класса:
LDC Lstring/WidgetProperty;.class
ALOAD 0
INVOKESTATIC java/lang/Enum.valueOf (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
CHECKCAST string/WidgetProperty
ARETURN
Похоже, что метод java.lang.Enum.valueOf
вызывается внутри метода WidgetPtoperty.valueOf
. К счастью, этот метод присутствует в коде Java. Давайте проверим это:
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
И enumConstantDirectory
:
Map<String, T> enumConstantDirectory() {
Map<String, T> directory = enumConstantDirectory;
if (directory == null) {
T[] universe = getEnumConstantsShared();
if (universe == null)
throw new IllegalArgumentException(
getName() + " is not an enum type");
directory = new HashMap<>((int)(universe.length / 0.75f) + 1);
for (T constant : universe) {
directory.put(((Enum<?>)constant).name(), constant);
}
enumConstantDirectory = directory;
}
return directory;
}
private transient volatile Map<String, T> enumConstantDirectory;
Ага, значит, все, что у нас здесь есть, — это обычная изменчивая хэш-карта HashMap
с именами перечислений в качестве ключей. И похоже, что в этом конкретном случае оператор switch
просто превосходит метод HashMap.get()
.
Добавим valueOf
с собственной реализацией HashMap
и тоже протестируем:
private final static Map<String, WidgetProperty> cache;
static {
cache = new HashMap<>();
for (WidgetProperty widgetProperty : values()) {
cache.put(widgetProperty.name(), widgetProperty);
}
}
public static WidgetProperty enumMap(String value) {
return cache.get(value);
}
Результаты (более низкий балл означает большее быстродействие, результаты enumMap
прикреплены к предыдущему тесту):
JDK 11.0.8, OpenJDK 64-Bit Server VM, 11.0.8+10
Benchmark (strParams) Mode Cnt Score Error Units
EnumValueOf.enumMap LABEL avgt 10 11.749 ± 0.761 ns/op
EnumValueOf.enumMap ON_LABEL avgt 10 11.778 ± 0.306 ns/op
EnumValueOf.enumMap MAX avgt 10 10.517 ± 0.056 ns/op
EnumValueOf.enumSwitch LABEL avgt 80 7.149 ± 0.030 ns/op
EnumValueOf.enumSwitch ON_LABEL avgt 80 7.758 ± 0.077 ns/op
EnumValueOf.enumSwitch MAX avgt 80 6.948 ± 0.083 ns/op
EnumValueOf.enumValueOf LABEL avgt 80 15.659 ± 0.450 ns/op
EnumValueOf.enumValueOf ON_LABEL avgt 80 14.734 ± 0.542 ns/op
EnumValueOf.enumValueOf MAX avgt 80 15.153 ± 0.578 ns/op
Похоже, собственная реализация HashMap
работает быстрее,чем обычный Enum.valueOf()
, но все же медленнее, чем оператор switch
.
Перепишем тест, чтобы получить случайный ввод:
@BenchmarkMode(Mode.AverageTime)
@Fork(5)
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Measurement(iterations = 10, time = 1)
public class EnumValueOf {
String[] names;
int index;
@Setup
public void init() {
WidgetProperty[] values = WidgetProperty.values();
names = new Random(0).ints(4096, 0, values.length)
.mapToObj(i -> values[i].name())
.toArray(String[]::new);
index = 0;
}
private String nextName() {
return names[index++ & (names.length - 1)];
}
@Benchmark
public WidgetProperty enumValueOf() {
return WidgetProperty.valueOf(nextName());
}
@Benchmark
public WidgetProperty enumSwitch() {
return WidgetProperty.valueOfSwitch(nextName());
}
@Benchmark
public WidgetProperty enumMap() {
return WidgetProperty.valueOfMap(nextName());
}
}
Результаты (более низкий балл = большее быстродействие):
JDK 11.0.8, OpenJDK 64-Bit Server VM, 11.0.8+10
```
Benchmark Mode Cnt Score Error Units
EnumValueOf.enumMap avgt 50 7.800 ± 0.097 ns/op
EnumValueOf.enumSwitch avgt 50 19.734 ± 0.350 ns/op
EnumValueOf.enumValueOf avgt 50 9.292 ± 0.826 ns/op
```
Любопытно… Теперь, при случайном вводе, оператор switch
работает в два раза медленнее. Похоже, что разница связана с предсказанием переходов. И самым быстрым вариантом будет наша собственная реализация хэша карт, так как она влечет за собой меньше проверок по сравнению с Enum.valueOf
.
Enum.values()
Теперь рассмотрим другой пример. Он не так популярен, как приведенный первым, но все же присутствует во многих проектах:
for (WidgetProperty property : WidgetProperty.values()) {
...
}
Видите, что здесь не так?
Опять же, метод Enum.values()
генерируется во время компиляции, поэтому, чтобы разобраться, что там не так, мы должны еще раз взглянуть на байт-код:
GETSTATIC string/WidgetProperty.$VALUES :
[Lstring/WidgetProperty;
INVOKEVIRTUAL [Lstring/WidgetProperty;.clone
()Ljava/lang/Object;
CHECKCAST [Lstring/WidgetProperty;
ARETURN
Не нужно быть экспертом по байт-коду, чтобы увидеть вызов метода .clone()
. Таким образом, каждый раз, когда вы вызываете метод Enum.values()
, всегда будет копироваться массив со значениями enum
.
Решение довольно очевидно — мы можем кэшировать результат метода Enum.values()
следующим образом:
public static final WidgetProperty[] values = values();
И взамен использовать поле кэша. Давайте проверим это с помощью теста производительности:
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Measurement(iterations = 10, time = 1)
public class EnumValues {
@Benchmark
public void enumValuesMethod(Blackhole bh) {
for (WidgetProperty property : WidgetProperty.values()) {
bh.consume(property);
}
}
@Benchmark
public void enumValuesVariable(Blackhole bh) {
for (WidgetProperty property : WidgetProperty.values) {
bh.consume(property);
}
}
}
Я применил в этом тесте цикл, потому что обычно в цикле используется Enum.values()
.
Результаты (более низкий балл означает большее быстродействие):
JDK 11.0.8, OpenJDK 64-Bit Server VM, 11.0.8+10
Benchmark Mode Cnt Score Error Units
EnumValues.enumValuesMethod avgt 10 21.576 ± 0.225 ns/op
EnumValues.enumValuesVariable avgt 10 16.811 ± 0.095 ns/op
Разница в производительности здесь около 30%. Это потому, что итерация цикла также требует кое-каких ресурсов ЦПУ. Однако, что более важно: после внесенного исправления никакого распределения не происходит, это даже более важно, чем улучшение показателей скорости.
Вы можете задаться вопросом: почемуEnum.values()
реализован именно таким образом? Объяснение довольно простое. Без метода copy()
любой желающий может изменить содержимое возвращаемого массива. А это может привести ко многим проблемам. Поэтому возвращать одну и ту же ссылку на массив со значениями, которые можно легко заменить, — не самая лучшая идея. Неизменяемые массивы могли бы решить эту проблему, но в Java их нет.
Да, такое изменение немного снижает читабельность, и вовсе не нужно применять его ко всякому перечислению. Это необходимо только для действительно горячих путей.
Для примера можете ознакомиться с этим пулл-реквестом jdbc-драйвера для Apache Cassandra или этим пулл-реквестом для HTTP-сервера.
Заключение
- Рассмотрите возможность воспользоваться собственным кэшем карт метода
Enum.valueOf()
. В качестве дополнительного бонуса можно избежать возникновения исключения при неправильном вводе. Это может изменить правила игры для некоторых потоков. - Также подойдет
Enum.values()
с кэшированным полем.Это позволит вам уменьшить скорость распределения.
Вот исходный код тестов производительности — можете попробовать их самостоятельно.
Спасибо за внимание!
Читайте также:
- 5 основных фреймворков для Java-разработчиков
- Аннотации для параллелизма в Java: расцвечивание потоков
- Жизненный цикл потока в Java
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Dmytro Dumanskiy: “Micro optimizations in Java. Good, nice and slow Enum”