在编译时不知道Java类的最快方法是什么? Java框架通常会这样做。 很多。 它可以直接影响其性能。 因此,让我们对不同的方法进行基准测试,例如反射,方法句柄和代码生成。
用例
假设我们有一个简单的Person
类,其中包含名称和地址:
public class Person {
...
public String getName() {...}
public Address getAddress() {...}
}
并且我们想使用诸如以下的框架:
- XStream ,JAXB或Jackson来将实例序列化为XML或JSON。
- JPA /休眠将人员存储在数据库中。
- OptaPlanner分配地址(如果他们是游客或无家可归的人)。
这些框架都不了解Person
类。 因此,他们不能简单地调用person.getName()
:
// Framework code
public Object executeGetter(Object object) {
// Compilation error: class Person is unknown to the framework
return ((Person) object).getName();
}
相反,代码使用反射,方法句柄或代码生成。
但是这样的代码被称为很多 :
- 如果在数据库中插入1000个不同的人,则JPA / Hibernate可能会调用2000次这样的代码:
- 1000次调用
Person.getName()
- 1000次调用
- 同样,如果您用XML或JSON编写1000个不同的人,则XStream,JAXB或Jackson可能会进行2000次调用。
显然,当这种代码每秒被调用x次时, 其性能很重要 。
基准测试
使用JMH,我在带有32GB RAM的64位8核Intel i7-4790台式机上的Linux上使用OpenJDK 1.8.0_111运行了一组微型基准测试。 JMH基准测试有3个分支,5个1秒的预热迭代和1秒的20个测量迭代。
该基准的源代码位于此GitHub存储库中 。
TL; DR结果
- Java反射很慢。 (*)
- Java MethodHandles也很慢。 (*)
- 用
javax.tools
生成的代码很快。 (*)
(*)在用例中,我以使用的工作量作为基准。 你的旅费可能会改变。
因此,魔鬼在细节中。 让我们浏览一下实现,以确认我应用了典型的魔术技巧(例如setAccessible(true)
)。
实作
直接访问(基准)
我使用了一个普通的person.getName()
调用作为基准:
public final class MyAccessor {
public Object executeGetter(Object object) {
return ((Person) object).getName();
}
}
每次操作大约需要2.7纳秒:
Benchmark Mode Cnt Score Error Units
===================================================
DirectAccess avgt 60 2.667 ± 0.028 ns/op
直接访问自然是运行时最快的方法,而没有引导成本。 但是它在编译时导入Person
,因此每个框架都无法使用它。
反射
框架在运行时读取getter的明显方法是不预先知道它的方法是通过Java Reflection:
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.667 ± 0.028 ns/op
Reflection avgt 60 5.511 ± 0.081 ns/op
反射比直接访问慢106%(大约慢一倍)。 预热还需要更长的时间。
这对我来说不是什么大惊喜,因为当我使用OptaPlanner在980个城市中描述(使用抽样)一个人为简单的旅行商问题时,反射成本像拇指酸痛一样突出:
方法句柄
Java 7中引入了MethodHandle来支持invokedynamic指令。 根据javadoc,它是对基础方法的类型化,直接可执行的引用。 听起来很快,对不对?
public final class MyAccessor {
private final MethodHandle getterMethodHandle;
public MyAccessor() {
MethodHandle temp = lookup.findVirtual(Person.class, "getName", MethodType.methodType(String.class));
temp = temp.asType(temp.type().changeParameterType(0 , Object.class));
getterMethodHandle = temp.asType(temp.type().changeReturnType(Object.class));
}
public Object executeGetter(Object bean) {
return getterMethodHandle.invokeExact(bean);
}
}
不幸的是, MethodHandle甚至比 OpenJDK 8中的反射还要慢 。它每次操作花费6.1纳秒,因此比直接访问慢132%。
Benchmark Mode Cnt Score Error Units
===================================================
DirectAccess avgt 60 2.667 ± 0.028 ns/op
Reflection avgt 60 5.511 ± 0.081 ns/op
MethodHandle avgt 60 6.188 ± 0.059 ns/op
StaticMethodHandle avgt 60 5.481 ± 0.069 ns/op
话虽如此,如果MethodHandle在静态字段中,则每次操作只需要5.5纳秒,这仍然与反射一样慢 。 此外,对于大多数框架而言,这是无法使用的。 例如,JPA实现可能需要反映n
类( Person
, Company
, Order
等等)的m
getters( getName()
, getAddress()
, getBirthDate()
,...),因此JPA实现如何有n * m
静态字段,在编译时不知道n
或m
?
我确实希望MethodHandle在将来的Java版本中能够像直接访问一样快,从而取代对...的需求。
使用javax.tools.JavaCompiler生成的代码
在Java中,可以在运行时编译和运行生成的Java代码。 因此,使用javax.tools.JavaCompiler
API,我们可以在运行时生成直接访问代码:
public abstract class MyAccessor {
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);
}
有关如何使用javax.tools.JavaCompiler
更多信息,请参见本文或本文的 第2页 。 除了javax.tools
之外,类似的方法也可以使用ASM或CGLIB,但是这些方法会推断出额外的依赖性,并且可能会产生不同的性能结果。
无论如何, 生成的代码与直接访问一样快 :
Benchmark Mode Cnt Score Error Units
===================================================
DirectAccess avgt 60 2.667 ± 0.028 ns/op
GeneratedCode avgt 60 2.745 ± 0.025 ns/op
因此,当我再次在OptaPlanner中运行该完全相同的Traveling Salesman问题时,这一次使用代码生成来访问计划变量, 因此总分计算速度提高了18% 。 并且分析(使用采样)看起来也更好:
请注意,在正常使用情况下,由于大量CPU需要实际复杂的分数计算,因此性能提升几乎是无法检测到的...
运行时代码生成的唯一缺点是,它会导致可观的引导成本,特别是如果生成的代码未进行批量编译时。 因此,我仍然希望有一天MethodHandles能够像直接访问一样快,只是为了避免增加引导成本。
结论
在此基准测试中,反射和MethodHandles的速度是OpenJDK 8中直接访问的两倍,但是生成的代码的速度是直接访问的速度。
Benchmark Mode Cnt Score Error Units
===================================================
DirectAccess avgt 60 2.667 ± 0.028 ns/op
Reflection avgt 60 5.511 ± 0.081 ns/op
MethodHandle avgt 60 6.188 ± 0.059 ns/op
StaticMethodHandle avgt 60 5.481 ± 0.069 ns/op
GeneratedCode avgt 60 2.745 ± 0.025 ns/op
翻译自: https://www.javacodegeeks.com/2018/01/java-reflection-much-faster.html