Java反射性能问题

时间:2011-08-04 19:50:46

标签: java performance reflection benchmarking

我知道有很多话题都在谈论反思表现。

即使是官方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以查看反射是否仍然更快。

没有sysout的结果是:

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

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

按照说明,我将每个循环分开运行,结果是(没有sysout)

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

8 个答案:

答案 0 :(得分:12)

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

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

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

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

  

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

     

1000000反射方法调用没有查找:393毫秒。

     

带循环的1000000反射方法调用:978毫秒。

循环在单独的程序中运行

  

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

     

1000000反射方法调用而不查找:555毫秒。

     

带循环的1000000反射方法调用:1160毫秒。

答案 1 :(得分:5)

article by Brian Goetz on microbenchmarks值得一读。在进行测量之前,看起来你没有采取任何措施来加热JVM(意味着让它有机会进行任何内联或其他优化),因此非反射测试可能仍然没有变暖 - 起来,这可能会扭曲你的数字。

答案 2 :(得分:3)

像这样的微基准测试永远不会准确 - 因为虚拟机“预热”它会内联代码并优化代码位,因此同样的事情执行2分钟后程序在开始时可以大大超越它。

就这里发生的事情而言,我的猜测是第一个“普通”方法调用块使其变暖,因此反射块(以及所有后续调用)会更快。通过反射调用我可以看到的方法添加的唯一开销是查找指向该方法的指针,无论如何这都是纳秒级操作,并且可以由JVM轻松缓存。剩下的就是虚拟机如何预热,当你到达反射呼叫时就是这样。

答案 3 :(得分: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 :(得分:3)

反射呼叫应该比正常呼叫慢,这没有固有的原因。 JVM可以将它们优化为同样的东西。

实际上,人力资源有限,他们必须先优化正常通话。随着时间的推移,他们可以优化反射呼叫;特别是当反射越来越受欢迎时。

答案 5 :(得分:2)

我一直在编写自己的微基准测试,没有循环,并且使用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回复中描述的微基准测试的注意事项 - 微基准测试肯定有很多缺陷 - 如果他们说反射比直接调用慢很多,那就相信文档。

答案 6 :(得分:1)

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

尝试删除“println()”调用并运行这样的代码,我确信你会对结果感到惊讶(需要进行一些愚蠢的计算以避免编译器完全取消调用): / p>

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

答案 7 :(得分:1)

即使你在两种情况下(即在第二和第三循环之前)查找方法, 第一次查找比第二次查找占用的时间少,这应该是另一种方式,而不是我的机器上的常规方法调用。

无论如何,如果你使用方法查找的第二个循环,以及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