Java反射速度更快的案例

位置:首页>文章>详情   分类: Java教程 > 编程技术   阅读(60)   2024-06-05 06:21:11

在编译时不知道类的情况下从 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();
   }

相反,代码使用反射、方法句柄或代码生成。

但是这样的代码被称为很多

  • 如果您在数据库中插入 1000 个不同的人,JPA/Hibernate 可能会调用这样的代码 2000 次:
    • 1000 次调用 Person.getName()
    • 另外 1000 次调用 Person.getAddress()
  • 类似地,如果您将 1000 个不同的人写入 XML 或 JSON,则 XStream、JAXB 或 Jackson 可能会调用 2000 个。

显然,当此类代码每秒被调用 x 次时,其性能很重要

基准

使用 JMH,我在具有 32GB RAM 的 64 位 8 核 Intel i7-4790 台式机上使用 Linux 上的 OpenJDK 1.8.0_111 运行了一组微基准测试。 JMH 基准测试运行了 3 个分叉,5 次 1 秒的预热迭代和 20 次 1 秒的测量迭代。

该基准测试 的源代码位于此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 反射:

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 类(PersonCompanyOrder、... ) 的 m getters (getName(), getAddress(), getBirthDate(), … ),那么 JPA 实现如何在编译时不知道 n * mn 是否有 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 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

相关阅读

Java HashSet 教程展示了如何使用 Java HashSet 集合。 Java哈希集 HashSet 是一个不包含重复元素的集合。此类为基本操作(添加、删除、包含和大小)提供恒定时间性...
SpringApplicationBuilder 教程展示了如何使用 SpringApplicationBuilder 创建一个简单的 Spring Boot 应用程序。 春天 是用于创建企业应...
通道是继 buffers 之后 java.nio 的第二个主要新增内容,我们在之前的教程中已经详细了解了这一点。通道提供与 I/O 服务的直接连接。 通道是一种在字节缓冲区和通道另一端的实体(通...
课程大纲 Elasticsearch 是一个基于 Lucene 的搜索引擎。它提供了一个分布式的、支持多租户的全文搜索引擎,带有 HTTP Web 界面和无模式的 JSON 文档。 Elasti...
解析器是强大的工具,使用 ANTLR 可以编写可用于多种不同语言的各种解析器。 在这个完整的教程中,我们将: 解释基础:什么是解析器,它可以用来做什么 查看如何设置 ANTLR 以便在 Java...
Java 是用于开发各种桌面应用程序、Web 应用程序和移动应用程序的最流行的编程语言之一。以下文章将帮助您快速熟悉 Java 语言,并迈向 API 和云开发等更复杂的概念。 1. Java语言...
Java中的继承是指子类继承或获取父类的所有非私有属性和行为的能力。继承是面向对象编程的四大支柱之一,用于提高层次结构中类之间的代码可重用性。 在本教程中,我们将了解 Java 支持的继承类型,...
Java Message Service 是一种支持正式通信的 API,称为 网络上计算机之间的消息传递。 JMS 为支持 Java 程序的标准消息协议和消息服务提供了一个通用接口。 JMS 提...
之前,我介绍了spring 3 + hibernate 集成 示例和struts 2 hello world 示例。在本教程中,我将讨论在将 spring 框架与 struts 与 hibern...
Java 项目中的一项常见任务是将日期格式化或解析为字符串,反之亦然。解析日期意味着你有一个代表日期的字符串,例如“2017-08-3”,你想把它转换成一个代表 Java 中日期的对象,例如Ja...