繁体   English   中英

Java反射性能问题

[英]Java Reflection Performance Issue

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

甚至官方的Java文档都说Reflection的速度较慢,但​​是我有以下代码:

  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优化只是使其运行得更快,所以我添加了这些printl以查看反射是否仍然更快。

没有sysout的结果是:

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.

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

为了获得更有意义但仍不完全准确的图像,应将测试分为三个单独的程序。

结果在我的计算机上(不打印,每次运行1,000,000)

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

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

1000000个不进行查找的反射方法调用:393毫秒。

具有循环的1000000个反射方法调用:978毫秒。

循环在单独的程序中运行

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

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

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

Brian Goetz一篇有关微基准测试文章值得一读。 在执行测量之前,您似乎没有采取任何措施来预热JVM(这意味着给它机会进行任何内联或其他优化),因此非反射测试可能仍未预热-上升,这可能会使您的数字出现偏差。

当您有多个长时间运行的循环时,第一个循环可以触发该方法进行编译,从而导致后面的循环从头开始进行优化。 但是,优化可能不是最佳的,因为它没有这些循环的运行时信息。 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;
    }
}

像这样的微基准永远都不会是准确的-随着VM“预热”,它将内嵌代码段并随着代码的进行优化代码段,因此在程序中执行2分钟执行同一件事可能会极大从一开始就超越它。

就这里发生的事情而言,我的猜测是第一个“正常”方法调用块会对其进行预热,因此反射块(实际上是所有后续调用)会更快。 通过反射调用一个方法可以看到的唯一开销是查找该方法的指针,该指针无论如何都是纳秒级的操作,很容易被JVM缓存。 其余的将取决于如何对VM进行预热,这取决于您到达反​​射性呼叫的时间。

没有内在的原因,为什么反射呼叫应该比普通呼叫慢。 JVM可以将它们优化为同一件事。

实际上,人力资源是有限的,因此必须首先优化常规呼叫。 随着时间的流逝,他们可以致力于优化反射呼叫; 特别是当反思变得越来越流行时。

令我惊讶的是,您已在内部基准测试循环内放置了“ 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

我一直在编写自己的微基准,没有循环,并带有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中执行了12次,结果相差很多,但这通常是我得到的:

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

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

即使您在两种情况下都查找方法(即在第二和第三循环之前),第一次查找所花的时间也比第二次查找要少,第二次查找本来应该是相反的,并且比我的机器上的常规方法调用要少。

不过,如果您将第二个循环与方法查找和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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM