Java 反射性能问题

8

我知道有很多话题讨论反射性能。

即使官方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.

仅作为注意,起初我没有打印那些sysouts,但之后我意识到一些JVM优化使得它变得更快,所以我添加了这些printls来查看反射是否仍然更快。

没有sysouts的结果如下:

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

我在网上看到,旧版本JVM上执行的相同测试,反射而无需查找的速度比常规调用慢两倍,并且随着新版本的更新,速度会下降。 如果有人能够执行并告诉我我错了,或者至少告诉我是否存在使其更快的不同之处。 按照说明,我分别运行了每个循环,并得出结果(没有sysouts)。
100000 regular method calls:70 milliseconds.
100000 reflective method calls without lookup:120 milliseconds.
100000 reflective method calls with lookup:129 milliseconds.

无论先执行哪些测试,您是否会得到相同的结果?或者更好的是,将其分成3次运行? - Michael Berry
8个回答

13

不要在同一个“运行”中同时对不同的代码部分进行性能测试。JVM有各种优化,虽然最终结果相同,但内部执行方式可能会有所不同。更具体地说,在您的测试过程中,JVM可能已经注意到您正在频繁调用Object.toString方法,并开始内联对Object.toString方法的调用。它可能已经开始执行循环展开。或者第一个循环中可能有垃圾回收,但第二个或第三个循环中可能没有。

为了获得更有意义的、但仍不完全准确的结果,您应该将测试分成三个单独的程序。

我的电脑上的结果(不打印,每个程序运行1000000次)

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

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

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

1000000次带有查找的反射方法调用:978毫秒。

每个循环在单独的程序中运行

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

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

1000000次带有查找的反射方法调用:1160毫秒。


5

这里有一篇由Brian Goetz撰写的关于微基准测试的文章(链接),值得一读。看起来你在进行测量之前并没有做任何JVM预热(即给它机会去进行内联或其他优化),因此非反射测试可能仍未被预热,这可能会影响你的数据。


4
当您有多个长时间运行的循环时,第一个循环可以触发方法编译,从而导致后面的循环从一开始就被优化。然而,优化可能是次优的,因为对于这些循环没有运行时信息。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;
    }
}

3

像这样的微基准测试永远不可能精确 - 随着虚拟机“热身”,它将内联代码的一部分并优化代码的一部分,因此在程序运行2分钟后执行的相同操作可能会比一开始快得多。

就这里发生的情况而言,我猜测第一个“正常”的方法调用块会使其热身,因此反射块(以及所有后续调用)将更快。通过反射调用方法所添加的唯一开销是查找该方法的指针,这是一个纳秒级别的操作,可以轻松地被JVM缓存。其余的取决于虚拟机何时被热身,当你到达反射调用时它已经被热身了。


1
嗯...我把那个测试分开做了,结果真的很不同。 - Marcos Vasconcelos

3

没有固有的理由反射调用应该比普通调用慢。JVM 可以将它们优化为相同的内容。

实际上,人力资源是有限的,必须先针对普通调用进行优化。随着时间的推移,当反射变得越来越流行时,他们可以开始优化反射调用。


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回复中描述的微基准测试的注意事项--这种微小基准测试中肯定存在很多缺陷--如果文档说明反射比直接调用慢得多,请相信文档。


2
你的实现方法存在一些问题。首先,据我所知,所有的VM中nanotime的精度只有微秒级别。其次,在每个基准测试中,很多时间都会花费在查找时间上,而不是你想要测试的实际方法 -- 这也是为什么要使用循环的原因之一。第三,在仅有一个调用的情况下,你正在测试以解释模式运行字节码的时间,而不是编译模式(如果该方法对性能至关重要,则使其变慢)。 - Dunes

2
我注意到你在内部基准测试循环中放置了一个 "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


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

我认为原帖作者并没有计划在循环中重新调用反射。基于反射的应用程序通常只进行一次反射(它不会改变),然后使用缓存实例来执行调用。 - Steve Mitcham
是的,史蒂夫,我刚刚注意到了。当你回答时,我正在编辑帖子的中间部分,我不确定我的编辑后的帖子是否会丢失。 - foka
在这里浏览帖子,我不确定是否已经得出了结论,但是在我的机器上执行int循环= 100,000,000;(并且没有System.out.println())的性能基础上,我认为:常规调用:12,800毫秒,查找(第一次,第二个循环):12,200毫秒和查找(第二次,第三个循环):61,600毫秒,因此对于低成本方法,反射可能没有性能成本要承担。 - foka

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接