Java反射性能问题

发布于 2024-11-28 08:49:19 字数 2433 浏览 2 评论 0原文

我知道有很多话题都在谈论反射性能。

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

请注意,首先我在没有那堆系统输出的情况下执行了它,然后我意识到一些 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 上执行的相同测试使得没有查找的反射比常规调用慢两倍,并且速度低于新更新。 如果有人可以执行它并说我错了,或者至少告诉我是否有与过去不同的东西使它更快。

按照说明,我分开运行每个循环,结果是(没有系统输出)

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

I know there's a lot of topics talking about Reflection performance.

Even official Java docs says that Reflection is slower, but I have this code:

  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.");

   }

}

That I don't think is a valid benchmark, but at least should show some difference.
I executed it waiting to see the reflection normal calls being a bit slower than regular ones.

But this prints this:

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

Just for note, first I executed it without that bunch of sysouts, and then I realized that some JVM optimization are just making it goes faster, so I added these printls to see if reflection was still faster.

The result without sysouts are:

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

I saw over internet that the same test executed on old JVMs make the reflective without lookup are two times slower than regular calls, and that speed falls over new updates.
If anyone can execute it and say me I'm wrong, or at least show me if there's something different than the past that make it faster.

Following instructions, I ran every loop separated and the result are (without sysouts)

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

娇柔作态 2024-12-05 08:49:19

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

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

我的计算机上的结果(没有打印,每个运行 1,000,000 次)

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

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

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

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

循环在单独的程序中运行

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

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

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

Never performance test different bits of code in the same "run". The JVM has various optimisations that mean it though the end result is the same, how the internals are performed may differ. In more concrete terms, during your test the JVM may have noticed you are calling Object.toString a lot and have started to inline the method calls to Object.toString. It may have started to perform loop unfolding. Or there could have been a garbage collection in the first loop but not the second or third loops.

To get a more meaningful, but still not totally accurate picture you should separate your test into three separate programs.

The results on my computer (with no printing and 1,000,000 runs each)

All three loops run in same program

1000000 regular method calls: 490 milliseconds.

1000000 reflective method calls without lookup: 393 milliseconds.

1000000 reflective method calls with loopup: 978 milliseconds.

Loops run in separate programs

1000000 regular method calls: 475 milliseconds.

1000000 reflective method calls without lookup: 555 milliseconds.

1000000 reflective method calls with loopup: 1160 milliseconds.

私藏温柔 2024-12-05 08:49:19

Brian Goetz 撰写的关于微基准测试的文章值得一读。在进行测量之前,您似乎没有做任何事情来预热 JVM(这意味着给它一个机会做任何内联或其他优化),所以在进行测量之前,非反射测试很可能仍未预热 -还没有增加,这可能会扭曲你的数字。

There's an article by Brian Goetz on microbenchmarks that's worth reading. It looks like you're not doing anything to warm up the JVM (meaning give it a chance to do whatever inlining or other optimizations it's going to do) before doing your measurements, so it's likely the non-reflective test is still not warmed-up yet, and that could skew your numbers.

〆一缕阳光ご 2024-12-05 08:49:19

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

When you have multiple long running loops, the first loop can trigger the method to compile resulting in the later loops being optimised from the start. However the optimisation can be sub-optimal as it has no runtime information for those loops. The toString is relatively expensive and couple be taking longer than the reflections calls.

You don't need separate programs to avoid loop being optimised due to an earlier loop. You can run them in different methods.

The results I get are

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

The code

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;
    }
}
烂柯人 2024-12-05 08:49:19

像这样的微基准永远不会准确——当虚拟机“预热”时,它会内联代码段并在运行过程中优化代码段,因此在程序中执行 2 分钟的同样的事情可能会极大地提高性能。一开始就超越它。

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

Micro-benchmarks like this are never going to be accurate at all - as the VM "warms up" it'll inline bits of code and optimise bits of code as it goes along, so the same thing executed 2 minutes into a program could vastly outperform it right at the start.

In terms of what's happening here, my guess is that the first "normal" method call block warms it up, so the reflective blocks (and indeed all subsequent calls) would be faster. The only overhead added through reflectively calling a method that I can see is looking up the pointer to that method, which is a nanosecond-scale operation anyway and would be easily cached by the JVM. The rest would be on how the VM is warmed up, which it is by the time you reach the reflective calls.

牵你手 2024-12-05 08:49:19

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

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

There is no inherent reason why reflective call should be slower than a normal call. JVM can optimize them into the same thing.

Practically, human resources are limited, and they had to optimize normal calls first. As time passes by they can work on optimizing reflective calls; especially when reflection becomes more and more popular.

夏见 2024-12-05 08:49:19

让我印象深刻的是,您在内部基准循环中放置了“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

It strikes me that you have placed a "System.out.println(s)" call inside your inner benchmark loop.
Since performing IO is bound to be slow, it actually "swallows up" your benchmark and the overhead of the invoke becomes negligible.

Try removing the "println()" call and running code like this, I'm sure you'd be surprised by the result (some of the silly calculations are needed to avoid the compiler optimizing away the calls altogether):

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

别挽留 2024-12-05 08:49:19

我一直在编写自己的微基准,没有循环,并且使用 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 的回复中描述的关于微基准的警告 - 该微基准肯定存在很多缺陷 - 如果他们说反射比直接调用慢很多,请相信文档。

I have been writing my own micro-benchmark, without loops, and with 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);
}

I have been executing that in Eclipse on my machine a dozen times, and the results vary quite a bit, but here is what I typically get:

  • the direct method invocation clocks at 40-50 microseconds
  • method lookup clocks at 150-200 microseconds
  • reflective invocation with the method variable clocks at 250-310 microseconds.

Now, do not forget the caveats on microbenchmarks described in Nathan's reply - there are certainly a lot of flaws in that micro benchmark - and trust the documentation if they say that reflection is a LOT slower than direct invocation.

︶葆Ⅱㄣ 2024-12-05 08:49:19

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

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

Even if you look up the method in both cases (i.e. before 2nd and 3rd loop),
the first lookup takes way less time than the second lookup, which should have been the other way around and less than a regular method call on my machine.

Neverthless, if you use the 2nd loop with method lookup, and System.out.println statement, I get this:

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

Without System.out.println statement, I get:

regular call    : 78 ms
look up (2nd)   : 37 ms
look up (3rd )  : 112 ms
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文