在编译时不知道类的情况下,从Java类读取getter的最快方法是什么?Java框架经常这样做——很多。而且,它可以直接影响他们的表现。所以,让我们对不同的方法进行基准测试,比如反射、方法句柄和代码生成。
反射用例
假设我们有一个简单的Person
类,它有一个名称和地址:
public class Person {
...
public String getName() {...}
public Address getAddress() {...}
}
我们希望使用其中一种框架,例如:
- XStream、JAXB或Jackson将实例序列化为XML或JSON
- JPA/Hibernate在数据库中存储人员
- OptaPlanner指定地址(如果他们是游客或无家可归者)
这些框架都不知道Person
类。所以,他们不能简单地调用person.getName()
:
// Framework code
public Object executeGetter(Object object) {
// Compilation error: class Person is unknown to the framework
return ((Person) object).getName();
}
相反,代码使用反射、方法句柄或代码生成。
但是,这种代码被称为可怕的东西:
- 如果Hibernate在一个不同的数据库中插入1000次,则可能会调用Hibernate/2000次:
- 1000次调用
Person.getName()
- 再调用1000次的
Person.getAddress()
- 类似地,如果您向XML或JSON编写1000个不同的人,XStream、JAXB或Jackson可能会有2000个调用。
显然,当这样的代码每秒被调用x次时,它的性能很重要。
基准测试
使用JMH,我在一个64位8核inteli7-4790台式机上运行了一组微基准测试,使用openjdk1.8.0_111操作系统。JMH基准测试运行了三个fork
,5次1秒的预热迭代,20次1秒的测量迭代。所有的预热成本都消失了;将迭代的长度增加到5秒对这里报告的数字几乎没有影响。
该基准测试的源代码可在GitHub存储库中找到。
http://github.com/ge0ffrey/ge0ffrey-presentations/tree/master/code/fasterreflection
TL;DR结果
- Java反射很慢。
- Java
MethodHandles
也很慢。 - 生成的代码
javax.tools.JavaCompiler
很快。 LambdaMetafactory
相当快。
注意:这些观察是基于我使用的工作负载进行基准测试的用例。您的里程数可能会有所不同!
所以,魔鬼就在细节上。让我们通过实现来确认我应用了典型的魔术技巧,比如setAccessible(true)
。
启动位置
直接访问:基线
我用了普通的person.getName()
呼叫为基线:
public final class MyAccessor {
public Object executeGetter(Object object) {
return ((Person) object).getName();
}
}
每次操作大约需要2.6纳秒:
Benchmark Mode Cnt Score Error Units
===================================================
DirectAccess avgt 60 2.590 ± 0.014 ns/op
直接访问自然是运行时最快的方法,无需启动开销。但是,它在编译时导入了Person
,所以每个框架都不能使用它。
反射
对于一个框架来说,在运行时理解getter
的最明显的方法是通过Java反射来理解它:
public final class MyAccessor {
private final Method getterMethod;
public MyAccessor() {
getterMethod = Person.class.getMethod("getName");
// Skip Java language access checking during executeGetter()
getterMethod.setAccessible(true);
}
public Object executeGetter(Object bean) {
return getterMethod.invoke(bean);
}
}
添加setAccessible(true)
调用可以使这些反射调用更快,但即使这样,每次调用也需要5.5纳秒。
Benchmark Mode Cnt Score Error Units
===================================================
DirectAccess avgt 60 2.590 ± 0.014 ns/op
Reflection avgt 60 5.275 ± 0.053 ns/op
反射比直接访问慢104%,这意味着它的速度是直接访问的两倍。热身也需要更长的时间。
这对我来说并不奇怪,因为当我使用抽样和一个简单的旅行推销员问题来描述OptaPlanner
的980个城市时,反射成本就像一个痛苦的拇指一样突出:
方法句柄 MethodHandles
在Java 7中引入了对调用ED7指令的支持。根据Javadoc
,它是对底层方法的类型化、直接可执行的引用。听起来很快,对吧?
public final class MyAccessor {
private final MethodHandle getterMethodHandle;
public MyAccessor() {
MethodHandles.Lookup lookup = MethodHandles.lookup();
// findVirtual() matches signature of Person.getName()
getterMethodHandle = lookup.findVirtual(Person.class, "getName", MethodType.methodType(String.class))
// asType() matches signature of MyAccessor.executeGetter().asType(MethodType.methodType(Object.class, Object.class));
}
public Object executeGetter(Object bean) {
return getterMethodHandle.invokeExact(bean);
}
}
不幸的是,在jdk中处理反射甚至比open8慢。每次操作需要6.1纳秒,因此比直接访问慢136%。
Benchmark Mode Cnt Score Error Units
===================================================
DirectAccess avgt 60 2.590 ± 0.014 ns/op
Reflection avgt 60 5.275 ± 0.053 ns/op
MethodHandle avgt 60 6.100 ± 0.079 ns/op
使用查找lookup.unreflectGetter(字段)
而不是lookup.findVirtual(……)
无显著性差异。我希望MethodHandle
在未来的Java版本中能够像直接访问一样快。
静态方法句柄
我还用MethodHandle
在静态字段中运行了一个基准测试。JVM可以在静态字段上发挥更大的作用。Aleksey和John O’Hara正确地指出,最初的基准测试没有正确地使用静态字段,所以我修复了这个问题。修正结果如下:
Benchmark Mode Cnt Score Error Units
===================================================
DirectAccess avgt 60 2.590 ± 0.014 ns/op
MethodHandle avgt 60 6.100 ± 0.079 ns/op
StaticMethodHandle avgt 60 2.635 ± 0.027 ns/op
是的,静态MethodHandle
与直接访问一样快,但它仍然没有用,除非我们想编写这样的代码:
public final class MyAccessors {
private static final MethodHandle handle1; // Person.getName()
private static final MethodHandle handle2; // Person.getAge()
private static final MethodHandle handle3; // Company.getName()
private static final MethodHandle handle4; // Company.getAddress()
private static final MethodHandle handle5; // ...
private static final MethodHandle handle6;
private static final MethodHandle handle7;
private static final MethodHandle handle8;
private static final MethodHandle handle9;
...
private static final MethodHandle handle1000;
}
如果我们的框架处理一个有四个getter
的域类层次结构,它将填充前四个字段。但是,如果它处理100个域类,每个类有20个getter
,总计2000个getter
,它将由于缺少静态字段而崩溃。
另外,如果我写这样的代码,即使是一年级的学生也会来告诉我我做得不对。静态字段不应用于实例变量。
通过 javax.tools.JavaCompiler 生成代码
在Java中,可以在运行时编译和运行生成的Java代码。所以和javax.tools.JavaCompilerAPI
,我们可以在运行时生成直接访问代码:
public abstract class MyAccessor {
// Just a gist of the code, the full source code is linked in a previous section
public static MyAccessor generate() {
final String String fullClassName = "x.y.generated.MyAccessorPerson$getName";
final String source = "package x.y.generated;\n"
+ "public final class MyAccessorPerson$getName extends MyAccessor {\n"
+ " public Object executeGetter(Object bean) {\n"
+ " return ((Person) object).getName();\n"
+ " }\n"
+ "}";
JavaFileObject fileObject = new ...(fullClassName, source);
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
ClassLoader classLoader = ...;
JavaFileManager javaFileManager = new ...(..., classLoader)
CompilationTask task = compiler.getTask(..., javaFileManager, ..., singletonList(fileObject));
boolean success = task.call();
...
Class compiledClass = classLoader.loadClass(fullClassName);
return compiledClass.newInstance();
}
// Implemented by the generated subclass
public abstract Object executeGetter(Object object);
}
完整的源代码要长得多,可以在GitHub存储库中使用。有关如何使用javax.tools.JavaCompiler
,请参阅本文或本文的第2页。在Java8中,它需要工具.jar在类路径上,这在JDK安装中自动存在。在Java9中,它需要模块java.编译器在模块路径中。此外,还需要采取适当的谨慎措施,以免产生类列表.mf
文件,并使用正确的类加载器。
此外javax.tools.JavaCompiler
,类似的方法可以使用ASM或CGLIB,但是这些方法推断Maven依赖关系,并且可能有不同的性能结果。
在任何情况下,生成的代码与直接访问一样快:
Benchmark Mode Cnt Score Error Units
===================================================
DirectAccess avgt 60 2.590 ± 0.014 ns/op
JavaCompiler avgt 60 2.726 ± 0.026 ns/op
所以,当我在OptaPlanner
中再次运行旅行推销员问题时,这次使用代码生成来访问计划变量,总的来说,分数计算速度快了18%。而且,分析(使用采样)看起来也更好:
请注意,在正常的用例中,由于实际复杂的分数计算的大量CPU需求,性能增益几乎无法检测。
在运行时生成代码的一个缺点是,它会导致显著的引导开销,特别是如果生成的代码不是批量编译的话。所以,我仍然希望有朝一日MethodHandles
能够像直接访问一样快,这样可以避免引导开销和依赖性痛苦。
LambdaMetafactory
在Reddit上,我收到了一个使用LambdaMetafactory
的有力建议:
由于缺少文档和StackOverflow问题,让lambdametfactory
处理非静态方法非常困难,但它确实有效:
public final class MyAccessor {
private final Function getterFunction;
public MyAccessor() {
MethodHandles.Lookup lookup = MethodHandles.lookup();
CallSite site = LambdaMetafactory.metafactory(lookup,
"apply",
MethodType.methodType(Function.class),
MethodType.methodType(Object.class, Object.class),
lookup.findVirtual(Person.class, "getName", MethodType.methodType(String.class)),
MethodType.methodType(String.class, Person.class));
getterFunction = (Function) site.getTarget().invokeExact();
}
public Object executeGetter(Object bean) {
return getterFunction.apply(bean);
}
}
而且,看起来不错:LambdaMetafactory
几乎和直接访问一样快。它只比直接访问慢33%,比反射好得多。
Benchmark Mode Cnt Score Error Units
===================================================
DirectAccess avgt 60 2.590 ± 0.014 ns/op
Reflection avgt 60 5.275 ± 0.053 ns/op
LambdaMetafactory avgt 60 3.453 ± 0.034 ns/op
当我在OptaPlanner
中再次运行旅行推销员问题时,这次使用lambdametafactor
访问计划变量时,总分计算速度比以前快了9%。但是,分析(使用采样)仍然显示了很多executeGetter()
时间,但仍然小于反射时间。
在非科学的测量中,元空间成本似乎是每lambda
约2kb,它通常会被垃圾收集。
引导成本
运行时成本最重要,因为每秒检索数千个实例的getter
并不少见。但是,引导成本也很重要,因为我们需要为域层次结构中的每个getter创建一个MyAccessor
,例如Person.getName()
, Person.getAddress()
, Address.getStreet()
, Address.getCity()
.
反射和方法句柄具有可忽略的引导成本。对于LambdaMetafactory
,它仍然可以接受。我的机器每秒创建大约25k访问器。但是对于Java编译器,它不是——我的机器每秒只创建200个访问器。
Benchmark Mode Cnt Score Error Units
=======================================================================
Reflection Bootstrap avgt 60 268.510 ± 25.271 ns/op // 0.3µs/op
MethodHandle Bootstrap avgt 60 1519.177 ± 46.644 ns/op // 1.5µs/op
JavaCompiler Bootstrap avgt 60 4814526.314 ± 503770.574 ns/op // 4814.5µs/op
LambdaMetafactory Bootstrap avgt 60 38904.287 ± 1330.080 ns/op // 39.9µs/op
这个基准测试不做缓存或大容量复杂化。
结论
在本次调查中,反射和(可用)MethodHandles
的速度是openjdk8中直接访问的两倍。生成的代码和直接访问一样快,但这很痛苦。LambdaMetafactory
几乎和直接访问一样快。
Benchmark Mode Cnt Score Error Units
===================================================
DirectAccess avgt 60 2.590 ± 0.014 ns/op
Reflection avgt 60 5.275 ± 0.053 ns/op // 104% slower
MethodHandle avgt 60 6.100 ± 0.079 ns/op // 136% slower
StaticMethodHandle avgt 60 2.635 ± 0.027 ns/op // 2% slower
JavaCompiler avgt 60 2.726 ± 0.026 ns/op // 5% slower
LambdaMetafactory avgt 60 3.453 ± 0.034 ns/op // 33% slower