在编译时不知道类的情况下从 Java 类读取 getter 的最快方法是什么? Java 框架经常这样做。很多。它可以直接影响他们的表现。因此,让我们对不同的方法进行基准测试,例如反射、方法句柄和代码生成。
假设我们有一个带有名称和地址的简单 Person
类:
public class Person { ... public String getName() {...} public Address getAddress() {...} }
我们想使用如下框架:
这些框架都不知道 Person
类。所以它们不能简单地调用 person.getName()
:
// Framework code public Object executeGetter(Object object) { // Compilation error: class Person is unknown to the framework return ((Person) object).getName(); }
相反,代码使用反射、方法句柄或代码生成。
但是这样的代码被称为很多:
Person.getName()
Person.getAddress()
显然,当此类代码每秒被调用 x 次时,其性能很重要。
使用 JMH,我在具有 32GB RAM 的 64 位 8 核 Intel i7-4790 台式机上使用 Linux 上的 OpenJDK 1.8.0_111 运行了一组微基准测试。 JMH 基准测试运行了 3 个分叉,5 次 1 秒的预热迭代和 20 次 1 秒的测量迭代。
该基准测试 的源代码位于此GitHub 存储库 中。
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 反射:
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 中描述(使用抽样)一个人为简单的 Traveling Salesman Problem 与 980 个城市时,反射成本像拇指酸痛:
MethodHandle 是在 Java 7 中引入的,用于支持 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); } }
不幸的是,在 OpenJDK 8 中,MethodHandle 甚至比反射慢。每次操作需要 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 版本中变得和直接访问一样快,取代对……的需要
在 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 Problem 时,这次使用代码生成来访问计划变量,得分计算速度总体上提高了 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
标签2: Java教程地址:https://www.cundage.com/article/jcg-java-reflection-much-faster.html