【问题标题】:Java Reflection Performance IssueJava 反射性能问题
【发布时间】:2011-08-04 19:50:45
【问题描述】:

我知道有很多关于反射性能的话题。

甚至官方 Java 文档都说反射速度较慢,但​​我有这段代码:

  public class ReflectionTest {
   public static void main(String[] args) throws Exception {
       Object object = new Object();
       Class<Object> c = Object.class;

       int loops = 100000;

       long start = System.currentTimeMillis();
       Object s;
       for (int i = 0; i < loops; i++) {
           s = object.toString();
           System.out.println(s);
       }
       long regularCalls = System.currentTimeMillis() - start;
       java.lang.reflect.Method method = c.getMethod("toString");

       start = System.currentTimeMillis();
       for (int i = 0; i < loops; i++) {
           s = method.invoke(object);
           System.out.println(s);
       }

       long reflectiveCalls = System.currentTimeMillis() - start;

       start = System.currentTimeMillis();
       for (int i = 0; i < loops; i++) {
           method = c.getMethod("toString");
           s = method.invoke(object);
           System.out.println(s);
       }

       long reflectiveLookup = System.currentTimeMillis() - start;

       System.out.println(loops + " regular method calls:" + regularCalls
               + " milliseconds.");

       System.out.println(loops + " reflective method calls without lookup:"
               + reflectiveCalls+ " milliseconds.");

       System.out.println(loops + " reflective method calls with lookup:"
               + reflectiveLookup + " milliseconds.");

   }

}

我认为这不是一个有效的基准,但至少应该显示出一些差异。 我执行它等待看到反射正常调用比普通调用慢一些。

但这会打印:

100000 regular method calls:1129 milliseconds.
100000 reflective method calls without lookup:910 milliseconds.
100000 reflective method calls with lookup:994 milliseconds.

请注意,首先我在没有那一堆 sysout 的情况下执行它,然后我意识到一些 JVM 优化只是让它变得更快,所以我添加了这些 printls 以查看反射是否仍然更快。

没有系统输出的结果是:

100000 regular method calls:68 milliseconds.
100000 reflective method calls without lookup:48 milliseconds.
100000 reflective method calls with lookup:168 milliseconds.

我在 Internet 上看到,在旧 JVM 上执行的相同测试使得无需查找的反射比常规调用慢两倍,并且该速度低于新的更新。 如果有人可以执行它并说我错了,或者至少告诉我是否有与过去不同的东西使它更快。

按照说明,我运行了每个单独的循环,结果是(没有系统输出)

100000 regular method calls:70 milliseconds.
100000 reflective method calls without lookup:120 milliseconds.
100000 reflective method calls with lookup:129 milliseconds.

【问题讨论】:

  • 不管先执行什么测试,你得到相同的结果吗?或者更好,分成 3 次运行?

标签: java performance reflection benchmarking


【解决方案1】:

永远不要在同一个“运行”中测试不同的代码位。 JVM 有各种优化,这意味着尽管最终结果相同,但内部的执行方式可能会有所不同。更具体地说,在您的测试期间,JVM 可能已经注意到您经常调用 Object.toString,并开始将方法调用内联到 Object.toString。它可能已经开始执行循环展开。或者可能在第一个循环中进行了垃圾收集,但在第二个或第三个循环中没有。

要获得更有意义但仍不完全准确的图片,您应该将测试分成三个独立的程序。

我的电脑上的结果(没有打印,每次运行 1,000,000 次)

三个循环都在同一个程序中运行

1000000 次常规方法调用:490 毫秒。

1000000 次反射方法调用,无需查找:393 毫秒。

1000000 次反射方法调用,循环时间:978 毫秒。

循环在不同的程序中运行

1000000 次常规方法调用:475 毫秒。

1000000 次反射方法调用,无需查找:555 毫秒。

1000000 次反射方法调用,循环时间:1160 毫秒。

【讨论】:

    【解决方案2】:

    有一个article by Brian Goetz on microbenchmarks 值得一读。在进行测量之前,您似乎没有做任何事情来预热 JVM(这意味着给它一个机会来做任何内联或其他优化它要做的事情),所以很可能非反射测试仍然没有预热 -还没有,这可能会扭曲你的数字。

    【讨论】:

      【解决方案3】:

      当您有多个长时间运行的循环时,第一个循环可以触发该方法进行编译,从而导致后面的循环从一开始就被优化。然而,优化可能是次优的,因为它没有这些循环的运行时信息。 toString 相对昂贵,并且比反射调用花费的时间更长。

      您不需要单独的程序来避免由于较早的循环而优化循环。您可以使用不同的方法运行它们。

      我得到的结果是

      Average regular method calls:2 ns.
      Average reflective method calls without lookup:10 ns.
      Average reflective method calls with lookup:240 ns.
      

      代码

      import java.lang.reflect.Method;
      
      public class ReflectionTest {
          public static void main(String[] args) throws Exception {
              int loops = 1000 * 1000;
      
              Object object = new Object();
              long start = System.nanoTime();
              Object s;
              testMethodCall(object, loops);
              long regularCalls = System.nanoTime() - start;
              java.lang.reflect.Method method = Object.class.getMethod("getClass");
              method.setAccessible(true);
      
              start = System.nanoTime();
              testInvoke(object, loops, method);
      
              long reflectiveCalls = System.nanoTime() - start;
      
              start = System.nanoTime();
              testGetMethodInvoke(object, loops);
      
              long reflectiveLookup = System.nanoTime() - start;
      
              System.out.println("Average regular method calls:"
                      + regularCalls / loops + " ns.");
      
              System.out.println("Average reflective method calls without lookup:"
                      + reflectiveCalls / loops + " ns.");
      
              System.out.println("Average reflective method calls with lookup:"
                      + reflectiveLookup / loops + " ns.");
      
          }
      
          private static Object testMethodCall(Object object, int loops) {
              Object s = null;
              for (int i = 0; i < loops; i++) {
                  s = object.getClass();
              }
              return s;
          }
      
          private static Object testInvoke(Object object, int loops, Method method) throws Exception {
              Object s = null;
              for (int i = 0; i < loops; i++) {
                  s = method.invoke(object);
              }
              return s;
          }
      
          private static Object testGetMethodInvoke(Object object, int loops) throws Exception {
              Method method;
              Object s = null;
              for (int i = 0; i < loops; i++) {
                  method = Object.class.getMethod("getClass");
                  s = method.invoke(object);
              }
              return s;
          }
      }
      

      【讨论】:

        【解决方案4】:

        像这样的微基准测试永远不会准确 - 随着虚拟机“预热”,它会内联代码位并在运行过程中优化代码位,所以同样的事情在 2 分钟内执行程序一开始就可以大大超越它。

        就这里发生的情况而言,我的猜测是第一个“正常”方法调用块会对其进行预热,因此反射块(以及实际上所有后续调用)会更快。通过反射调用我可以看到的方法增加的唯一开销是查找指向该方法的指针,无论如何这是一个纳秒级的操作,并且很容易被 JVM 缓存。剩下的就是关于 VM 是如何预热的,即在您到达反射调用时。

        【讨论】:

        • 嗯..我做了那个分开的测试,它真的很不一样。
        【解决方案5】:

        反射调用应该比普通调用慢并没有内在的原因。 JVM 可以将它们优化成相同的东西。

        实际上,人力资源是有限的,他们必须首先优化正常呼叫。随着时间的推移,他们可以优化反射调用;尤其是当反射变得越来越流行时。

        【讨论】:

          【解决方案6】:

          我一直在编写自己的微基准测试,没有循环,使用System.nanoTime()

          public static void main(String[] args) throws NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException
          {
            Object obj = new Object();
            Class<Object> objClass = Object.class;
            String s;
          
            long start = System.nanoTime();
            s = obj.toString();
            long directInvokeEnd = System.nanoTime();
            System.out.println(s);
            long methodLookupStart = System.nanoTime();
            java.lang.reflect.Method method = objClass.getMethod("toString");
            long methodLookupEnd = System.nanoTime();
            s = (String) (method.invoke(obj));
            long reflectInvokeEnd = System.nanoTime();
            System.out.println(s);
            System.out.println(directInvokeEnd - start);
            System.out.println(methodLookupEnd - methodLookupStart);
            System.out.println(reflectInvokeEnd - methodLookupEnd);
          }
          

          我已经在我的机器上的 Eclipse 中执行了十几次,结果差异很大,但这是我通常得到的结果:

          • 直接方法调用的时钟为 40-50 微秒
          • 方法查找时钟为 150-200 微秒
          • 反射调用与方法变量时钟在 250-310 微秒。

          现在,不要忘记 Nathan 的回复中描述的关于微基准的警告 - 该微基准肯定存在很多缺陷 - 如果他们说反射比直接调用慢很多,请相信文档。

          【讨论】:

          • 您的实施存在许多问题。首先,我所知道的所有虚拟机中的纳米时间仅精确到一微秒。其次,每个基准测试中的大部分时间都将用于查找时间,而不是您要测试的实际方法——循环重复完成的原因之一。第三,只需一次调用,您就可以测试在解释模式而不是编译模式下运行字节码的时间(如果这种方法对性能至关重要,则它会比其他方式更慢)。
          【解决方案7】:

          令我震惊的是,您在内部基准循环中放置了“System.out.println(s)”调用。 由于执行 IO 肯定会很慢,它实际上会“吞噬”您的基准测试,并且调用的开销变得可以忽略不计。

          尝试删除“println()”调用并像这样运行代码,我相信你会对结果感到惊讶(需要一些愚蠢的计算来避免编译器完全优化调用):

          public class Experius
          {
          
              public static void main(String[] args) throws Exception
              {
                  Experius a = new Experius();
                  int count = 10000000;
                  int v = 0;
          
                  long tm = System.currentTimeMillis();
                  for ( int i = 0; i < count; ++i )
                  {
                      v = a.something(i + v);
                      ++v;
                  }
                  tm = System.currentTimeMillis() - tm;
          
                  System.out.println("Time: " + tm);
          
          
                  tm = System.currentTimeMillis();
                  Method method = Experius.class.getMethod("something", Integer.TYPE);
                  for ( int i = 0; i < count; ++i )
                  {
                      Object o = method.invoke(a, i + v);
                      ++v;
                  }
                  tm = System.currentTimeMillis() - tm;
          
                  System.out.println("Time: " + tm);
              }
          
              public int something(int n)
              {
                  return n + 5;
              }
          
          }
          

          --TR

          【讨论】:

            【解决方案8】:

            即使您在这两种情况下(即在第 2 次和第 3 次循环之前)都查找该方法, 第一次查找比第二次查找花费的时间少得多,这应该是相反的,比我机器上的常规方法调用要少。

            无论如何,如果您使用带有方法查找的第二个循环和System.out.println 语句,我会得到:

            regular call        : 740 ms
            look up(2nd loop)   : 640 ms
            look up ( 3rd loop) : 800 ms
            

            没有System.out.println 声明,我得到:

            regular call    : 78 ms
            look up (2nd)   : 37 ms
            look up (3rd )  : 112 ms
            

            【讨论】:

            • 我不认为原始发布者打算在循环中重新调用反射。基于反射的应用程序通常执行一次反射(它不会改变),然后使用缓存的实例来执行调用。
            • 是的,史蒂夫,我刚刚注意到了。当您发布答案时,我正在编辑帖子,我不确定我编辑的帖子是否在某个地方丢失了。
            • 浏览这里的帖子我不确定是否已经提出了结论性意见,但在我看来,基于我的机器上的性能 int loops = 100,000,000; (并且没有 System.out.println())定期调用:12,800 毫秒,查找(第一次,第二次循环):12,200 毫秒和查找(第二次,第三次循环):61、600 毫秒,因此用于低成本方法反射可能不需要承担任何性能成本。
            猜你喜欢
            • 2010-09-30
            • 2011-07-13
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-12-06
            • 2014-08-21
            • 1970-01-01
            相关资源
            最近更新 更多