java中enum
Enums are crucial part of every application larger than “Hello World“. We use them everywhere. They are actually very useful: they restrict the input, allow you to compare the values by the reference, provide compile-time checks, and make the code easier to read. However, using Enums has a few performance downsides. And we’ll review them in this post.
枚举是每个大于“ Hello World”的应用程序的重要组成部分。 我们到处都使用它们。 它们实际上非常有用:它们限制了输入,允许您按引用比较值,提供编译时检查,并使代码更易于阅读。 但是,使用Enums有一些性能下降。 我们将在这篇文章中对其进行审查。
(Please consider all the code below from the point of performance)
(请从性能角度考虑以下所有代码)
(Please do not focus on numbers, they are just examples to prove the point)
(请不要专注于数字,它们只是证明这一点的例子)
枚举值 (Enum.valueOf)
In Blynk, one of the most popular features is called “Widget Property”. It’s when you can change the appearance of the visual widget in real-time by sending from the hardware the command like that:
在Blynk中 ,最受欢迎的功能之一是“ 窗口小部件属性”。 此时,您可以通过从硬件发送如下命令来实时更改可视窗口小部件的外观:
Blynk.setProperty(v1, “LABEL”, “My New Label”);
Let’s take a look at existing Enum from our server:
让我们看一下服务器中的现有Enum:
enum WidgetProperty {
LABEL,
COLOR,
ON_LABEL,
OFF_LABEL,
MIN,
MAX
}
And now look at this basic code:
现在看下面的基本代码:
String inWidgetProperty;
...
WidgetProperty property = WidgetProperty.valueOf(inWidgetProperty)
Do you see what is wrong here?
你看到这里有什么问题吗?
Let’s rewrite the above code and create our own method based on the switch statement:
让我们重写上面的代码,并基于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;
}
This method isn’t precisely the same, as it doesn’t throw the IllegalArgumentException in case of an unknown value. However, it’s done on purpose. Because, as you know, throwing and creating the exception (because of the stack trace) is an expensive operation.
此方法并不完全相同,因为在未知值的情况下不会抛出IllegalArgumentException 。 但是,这是有目的的。 如您所知,因为抛出和创建异常(由于堆栈跟踪)是一项昂贵的操作。
Let’s make a benchmark:
让我们做一个基准:
@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);
}
}
Results (lower score means faster):
结果(分数越低意味着速度越快):
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
Hm… Our custom method with the switch statement is two times faster than Enum.valueOf. Something is wrong here. We need to go deeper.
嗯……我们带有switch语句的自定义方法比Enum.valueOf快两倍。 这里不对劲。 我们需要更深入。
As you probably know, enum classes don’t have the Java valueOf() method. The compiler generates it during the compilation. So we need to look into the byte code of the generated class to understand what is going on there:
您可能知道, 枚举类没有Java 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
Looks like java.lang.Enum.valueOf method is invoked inside WidgetPtoperty.valueOf method. Fortunately, this method is present in the Java code. Let’s check it:
看起来在WidgetPtoperty.valueOf方法内部调用了java.lang.Enum.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);
}
And enumConstantDirectory:
和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;
Aha, so all we have here is a regular volatile HashMap with the enum names as the keys. And it looks like in that particular case the switch statement just outperforms the HashMap.get() method.
啊哈,所以我们这里只是一个以枚举名称为键的常规volatile HashMap 。 在特定情况下, switch语句的性能似乎优于HashMap.get()方法。
Let’s add valueOf with own HashMap implementation to the test as well:
让我们也将带有自己的HashMap实现的valueOf添加到测试中:
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);
}
Results (lower score means faster, enumMap results attached to the prev test):
结果(分数越低意味着速度越快,将enumMap结果附加到prev测试中):
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
Own HashMap implementation seems faster than regular Enum.valueOf() but still slower than the switch statement.
自己的HashMap实现似乎比常规的Enum.valueOf()快,但仍比switch语句慢。
Interesting… I expected that the compiler-generated valueOf method would be more effective. It already has all the required info during the compilation, so the compiler should be able to make a more efficient code.
有趣的是……我希望编译器生成的valueOf方法会更有效。 它在编译期间已经具有所有必需的信息,因此编译器应该能够编写更有效的代码。
Enum.values() (Enum.values())
Now, let’s consider another example. It’s not so popular as above one, but still present in many projects:
现在,让我们考虑另一个示例。 它不像上面的那样流行,但是仍然存在于许多项目中:
for (WidgetProperty property : WidgetProperty.values()) {
...
}
Do you see what is wrong here?
你看到这里有什么问题吗?
Again, Enum.values() method is generated during the compilation, so to understand what is wrong there we have to look at the byte code one more time:
同样,在编译过程中会生成Enum.values()方法,因此要了解那里出了什么问题,我们必须再看一次字节码:
GETSTATIC string/WidgetProperty.$VALUES : [Lstring/WidgetProperty;
INVOKEVIRTUAL [Lstring/WidgetProperty;.clone ()Ljava/lang/Object;
CHECKCAST [Lstring/WidgetProperty;
ARETURN
You don’t need to be a byte code expert to see the .clone() method invocation. So, every time you call the Enum.values() method — the array with the enum values is always copied.
您无需成为字节码专家即可查看.clone()方法的调用。 因此,每次调用Enum.values()方法时,总是会复制带有枚举值的数组。
The solution is pretty obvious — we can cache the Enum.values() method result like that:
解决方案非常明显-我们可以像这样缓存Enum.values()方法的结果:
public static final WidgetProperty[] values = values();
And use the cache field instead. Let’s check it with the benchmark:
并改用缓存字段。 让我们用基准进行检查:
@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);
}
}
}
I used the loop in that benchmark because, usually, the Enum.values() is used within the loop.
我在该基准测试中使用了循环,因为通常在循环中使用Enum.values() 。
Results (lower score means faster):
结果(分数越低意味着速度越快):
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
The performance difference here is ~30%. That’s because the loop iteration takes some CPU as well. However, what is more important: after our fix, no allocation happens. And that’s even more important than gained speed improvement.
此处的性能差异约为30%。 那是因为循环迭代也需要一些CPU。 但是,更重要的是:修复后,没有分配发生。 这甚至比提高速度更为重要。
You may wonder, why Enum.values() is implemented in this way? Well, the explanation is quite simple. Without copy() method anyone could change the content of the returned array. And that could lead to many problems. So returning the same reference to the array with values that can be easily replaced isn’t the best idea. Immutable arrays would solve that issue, but Java doesn’t have one.
您可能想知道,为什么以这种方式实现Enum.values() ? 好吧,解释很简单。 没有copy()方法,任何人都可以更改返回数组的内容。 这可能会导致许多问题。 因此,将相同的引用返回到具有易于替换的值的数组并不是最好的主意。 不可变的数组可以解决这个问题,但是Java没有。
Yes, this change decreases the readability a bit and you don’t need to use it with every enum. It’s necessary only for really hot paths.
是的,此更改会稍微降低可读性,并且您不需要在每个枚举中都使用它。 仅在真正热的路径上才需要。
Check for example this PR for Apache Cassandra jdbc driver, mssql-jdbc driver PR or this PR for http server.
例如,检查此PR for Apache Cassandra jdbc驱动程序 , mssql-jdbc驱动程序PR或该HTTP服务器PR 。
结论 (Conclusion)
For hot paths, consider using your own implementation of the Enum.valueOf() method based on the switch statement. As an additional bonus, you could avoid throwing the Exception during the wrong input. That could be a game-changer for some flows
对于热路径,请考虑基于switch语句使用您自己的Enum.valueOf()方法的实现。 另外,您可以避免在输入错误时抛出Exception 。 这可能会改变某些流程
For hot paths, go with the cached Enum.values() field. This will allow you to decrease the allocation rate
对于热路径,请使用缓存的Enum.values()字段。 这将使您降低分配率
Here is a source code of benchmarks, so that you can try it yourself.
这是基准测试的源代码 ,因此您可以自己尝试。
Thank you for your attention, and stay tuned.
感谢您的关注,敬请期待。
Previous post: Micro optimizations in Java. String.equalsIgnoreCase()
翻译自: https://medium.com/javarevisited/micro-optimizations-in-java-good-nice-and-slow-enum-261e6f77bd2e
java中enum