Java反射性能

dma*_*iii 168 java reflection optimization performance

使用反射创建对象而不是调用类构造函数会导致任何显着的性能差异吗?

Yuv*_*dam 168

是的,一点没错.通过反思查找课程的幅度更大.

引用Java关于反射的文档:

由于反射涉及动态解析的类型,因此无法执行某些Java虚拟机优化.因此,反射操作的性能低于非反射操作,并且应避免在性能敏感应用程序中频繁调用的代码段中.

这是一个简单的测试,我在机器上运行5分钟,运行Sun JRE 6u10:

public class Main {

    public static void main(String[] args) throws Exception
    {
        doRegular();
        doReflection();
    }

    public static void doRegular() throws Exception
    {
        long start = System.currentTimeMillis();
        for (int i=0; i<1000000; i++)
        {
            A a = new A();
            a.doSomeThing();
        }
        System.out.println(System.currentTimeMillis() - start);
    }

    public static void doReflection() throws Exception
    {
        long start = System.currentTimeMillis();
        for (int i=0; i<1000000; i++)
        {
            A a = (A) Class.forName("misc.A").newInstance();
            a.doSomeThing();
        }
        System.out.println(System.currentTimeMillis() - start);
    }
}
Run Code Online (Sandbox Code Playgroud)

有了这些结果:

35 // no reflection
465 // using reflection
Run Code Online (Sandbox Code Playgroud)

请记住,查找和实例化是一起完成的,在某些情况下,查找可以重构,但这只是一个基本的例子.

即使您只是实例化,您仍然会受到性能影响:

30 // no reflection
47 // reflection using one lookup, only instantiating
Run Code Online (Sandbox Code Playgroud)

再次,YMMV.

  • 下面的@Peter Lawrey指出,这个测试完全无效,因为编译器正在优化非反射解决方案(它甚至可以证明没有做任何事情并优化for循环).需要重新工作,并且可能应该从SO中删除作为错误/误导性信息.在两种情况下都将创建的对象缓存在数组中,以防止优化器对其进行优化.(它不能在反射情况下这样做,因为它无法证明构造函数没有副作用) (56认同)
  • 我刚刚目睹了JVM优化反射35倍.在循环中重复运行测试是测试优化代码的方式.第一次迭代:3045ms,第二次迭代:2941ms,第三次迭代:90ms,第四次迭代:83ms.代码:c.newInstance(i).c是一个构造函数.非反射代码:新的A(i),产生13,4,3 ms次.所以是的,在这种情况下,反射很慢,但并不像人们总结的那么慢,因为我看到的每一个测试,他们只是运行测试一次而没有让JVM有机会用机器替换字节代码码. (9认同)
  • @Bill K-让我们不要被带走.是的,由于优化,数字已关闭.不,测试**不是**完全无效.我添加了一个调用,可以消除任何扭曲结果的可能性,并且数字仍然反映在反射上.在任何情况下,请记住,这是一个非常粗略的微基准测试,它只是表明反射总会产生一定的开销 (6认同)
  • 在我的机器上.newInstance()调用只有一个Class.forName()调用得分30左右.根据VM版本,差异可能比您认为适当的缓存策略更接近. (5认同)
  • 这可能是一个无用的基准.取决于doSomething的作用.如果它没有任何可见的副作用,那么你的基准测试只运行死代码. (4认同)
  • 请注意,`System.currentTimeMillis()`*不能用于这样的基准测试,因为它的公共粒度大约为10-20ms!应该使用`System.nanoTime()`,或者循环计数增加100倍或更多. (3认同)
  • 将JMH用于所有微基准测试,我建议在此删除您的基准代码,因为它具有误导性 (3认同)

Bil*_*l K 85

是的,它慢了.

但请记住该死的#1规则 - PREMATURE OPTIMIZATION是所有邪恶的根源

(好吧,DRY可能与#1并列)

我发誓,如果有人在工作时向我求助并告诉我这一点,我会对他们未来几个月的代码保持警惕.

在您确定需要它之前,您必须永远不会优化,直到那时,只需编写好的,可读的代码.

哦,我也不是说写愚蠢的代码.只是考虑一下你可以做到的最干净的方式 - 没有复制和粘贴等等(仍然要警惕内部循环和使用最符合你需要的集合 - 忽略这些不是"未经优化的"编程,这是"糟糕"的编程)

当我听到这样的问题时,它吓坏了我,但后来我忘了每个人都必须在他们真正得到它之前先学习所有的规则.你花了一个月的时间调试一些"优化"的东西后你会得到它.

编辑:

这个帖子发生了一件有趣的事情.检查#1答案,这是编译器在优化方面有多强大的一个例子.测试完全无效,因为可以完全排除非反射实例化.

课?在你编写一个干净,整齐编码的解决方案并证明它太慢之前,不要进行优化.

  • 我完全赞同这种反应的情绪,但是如果你要做出一个重大的设计决定,那么就有必要对性能有所了解,这样你就不会走上一条完全不可行的道路.也许他只是做尽职调查? (27认同)
  • -1:以错误的方式避免做事不是优化,而只是做事.由于实际或虚构的性能问题,优化是错误的,复杂的方式. (26认同)
  • 任何敏感的分析师程序员都需要在早期阶段考虑效率,否则最终可能无法在高效且具有成本效益的时间范围内优化系统.不,你没有优化每个时钟周期,但你肯定会采用最佳实践来做类基本实例化的基本操作.这个例子是你为何考虑有关反思的问题的一个很好的例子.这将是一个非常糟糕的程序员,他继续在一百万行系统中使用反射,后来发现它的数量级太慢. (14认同)
  • @soru完全同意.在数组列表中为插入排序选择链接列表是执行操作的正确方法.但是这个特殊的问题 - 原始问题的双方都有很好的用例,所以选择一个基于性能而不是最有用的解决方案是错误的.我不确定我们有什么不同意,所以我不确定你为什么说"-1". (5认同)
  • @Richard Riley对于你将使用反射的选定类,通常类实例化是一个非常罕见的事件.我想你是对的 - 有些人可能会反思性地实例化每一个类,甚至是那些不断重建的类.我会称之为非常糟糕的编程(尽管那时你甚至可以实现类实例的缓存以便在事后重用并且不会过多地损害你的代码 - 所以我想我仍然会说设计总是为了可读性,然后进行配置和优化后来) (2认同)
  • @LukasEder 对于“自杀的最佳方法是什么”或“我如何最好地将这把弓瞄准以将苹果从我姐姐头上射下来”的问题,直接回答并不能解决问题的存在所暴露的潜在问题,并且很可能会激怒它——可能导致某人受伤! (2认同)

Pet*_*rey 36

您可能会发现J a正在优化A a = new A().如果将对象放入数组中,它们的表现就不会那么好.;)以下打印...

new A(), 141 ns
A.class.newInstance(), 266 ns
new A(), 103 ns
A.class.newInstance(), 261 ns

public class Run {
    private static final int RUNS = 3000000;

    public static class A {
    }

    public static void main(String[] args) throws Exception {
        doRegular();
        doReflection();
        doRegular();
        doReflection();
    }

    public static void doRegular() throws Exception {
        A[] as = new A[RUNS];
        long start = System.nanoTime();
        for (int i = 0; i < RUNS; i++) {
            as[i] = new A();
        }
        System.out.printf("new A(), %,d ns%n", (System.nanoTime() - start)/RUNS);
    }

    public static void doReflection() throws Exception {
        A[] as = new A[RUNS];
        long start = System.nanoTime();
        for (int i = 0; i < RUNS; i++) {
            as[i] = A.class.newInstance();
        }
        System.out.printf("A.class.newInstance(), %,d ns%n", (System.nanoTime() - start)/RUNS);
    }
}
Run Code Online (Sandbox Code Playgroud)

这表明我的机器差异大约为150 ns.

  • @gbjbaanb如果优化器正在优化创建本身,那么它不是一个有效的测试.因此,@ Peter的测试是有效的,因为它实际上比较了创建时间(优化器无法在任何实际情况下工作,因为在任何实际情况下,您需要实例化的对象). (13认同)
  • @ nes1983在这种情况下,您可以借此机会创建更好的基准.也许你可以提供一些建设性的东西,比如方法体内应该是什么. (10认同)

kdg*_*ory 26

"重要"完全取决于背景.

如果您使用反射基于某个配置文件创建单个处理程序对象,然后花费其余的时间来运行数据库查询,那么它是无关紧要的.如果你通过紧密循环中的反射创建大量对象,那么是的,它是重要的.

通常,设计灵活性(在需要的地方!)应该驱动您使用反射,而不是性能.但是,要确定性能是否是一个问题,您需要分析而不是从讨论论坛获取任意响应.


Esk*_*ola 26

如果确实需要比反射更快的东西,并且它不仅仅是一个过早的优化,那么使用ASM或更高级别的库生成字节码是一种选择.第一次生成字节码比仅使用反射慢,但是一旦生成字节码,它就像普通的Java代码一样快,并且将由JIT编译器优化.

使用代码生成的应用程序的一些示例:

  • CGLIB生成的代理上调用方法比Java的动态代理略快,因为CGLIB为其代理生成字节码,但动态代理仅使用反射(我测量 CGLIB在方法调用中快10倍左右,但创建代理的速度较慢).

  • JSerial生成字节码,用于读取/写入序列化对象的字段,而不是使用反射.JSerial网站上有一些基准测试.

  • 我不是100%肯定(我现在不想阅读源代码),但我认为Guice会生成字节码来执行依赖注入.如我错了请纠正我.


Mar*_*ing 24

反射有一些开销,但它在现代VM上比以前要小很多.

如果你使用反射来创建程序中的每个简单对象,那么就会出现问题.偶尔使用它,当你有充分的理由时,根本不应该成为一个问题.


mel*_*ngs 10

是的,使用Reflection时会出现性能损失,但优化的可能解决方法是缓存该方法:

  Method md = null;     // Call while looking up the method at each iteration.
      millis = System.currentTimeMillis( );
      for (idx = 0; idx < CALL_AMOUNT; idx++) {
        md = ri.getClass( ).getMethod("getValue", null);
        md.invoke(ri, null);
      }

      System.out.println("Calling method " + CALL_AMOUNT+ " times reflexively with lookup took " + (System.currentTimeMillis( ) - millis) + " millis");



      // Call using a cache of the method.

      md = ri.getClass( ).getMethod("getValue", null);
      millis = System.currentTimeMillis( );
      for (idx = 0; idx < CALL_AMOUNT; idx++) {
        md.invoke(ri, null);
      }
      System.out.println("Calling method " + CALL_AMOUNT + " times reflexively with cache took " + (System.currentTimeMillis( ) - millis) + " millis");
Run Code Online (Sandbox Code Playgroud)

将导致:

[java]用查找反射性地调用方法1000000次,耗时5618毫秒

[java]用缓存反复调用方法1000000次需要270毫安


Dor*_*dus 7

反射很慢,尽管对象分配并不像反射的其他方面那样没有希望.通过基于反射的实例化实现相同的性能需要您编写代码,以便jit可以告诉实例化哪个类.如果无法确定类的标识,则无法内联分配代码.更糟糕的是,转义分析失败,并且对象无法进行堆栈分配.如果幸运的话,如果此代码变热,JVM的运行时分析可能​​会受到拯救,并且可以动态地确定哪个类占优势并且可以针对该类进行优化.

请注意,此线程中的微基准测试存在严重缺陷,因此请使用一粒盐.迄今为止最缺陷的是彼得·劳里(Peter Lawrey):它确实进行了热身运动以使方法进行了操作,并且它(有意识地)击败逃逸分析以确保分配实际发生.即使那个问题也存在问题:例如,大量的数组存储可能会导致缓存缓存和存储缓冲区,因此如果分配速度非常快,这将最终成为内存基准测试.(虽然得到了正确的结论,但彼得得到了结论:不同之处是"150ns"而不是"2.5x".我怀疑他是为了生活而做这件事.)


小智 7

有趣的是,设置了跳过安全检查的setAccessible(true),成本降低了20%.

没有setAccessible(true)

new A(), 70 ns
A.class.newInstance(), 214 ns
new A(), 84 ns
A.class.newInstance(), 229 ns
Run Code Online (Sandbox Code Playgroud)

使用setAccessible(true)

new A(), 69 ns
A.class.newInstance(), 159 ns
new A(), 85 ns
A.class.newInstance(), 171 ns
Run Code Online (Sandbox Code Playgroud)


Eli*_*lie 6

是的,它明显变慢了.我们正在运行一些代码,虽然我目前没有可用的指标,但最终结果是我们不得不重构该代码而不使用反射.如果你知道类是什么,只需直接调用构造函数.