Java反射的更快替代品[已关闭]

2022-08-31 15:04:08

众所周知,反射是一种灵活但缓慢的方法,用于在运行时维护和修改代码的行为。

但是,如果我们必须使用这样的功能,那么与用于动态修改的Reflectle API相比,Java中是否有任何更快的编程技术?这些反对反思的替代方案的利弊是什么?


答案 1

反射的一种替代方法是动态生成类文件。这个生成的类应该执行所需的操作,例如调用在运行时发现的方法,并在编译时实现一个已知的方法,以便可以使用该接口以非反射方式调用生成的方法。有一个问题:如果适用,Reflection在内部也执行相同的操作。这在特殊情况下不起作用,例如在调用方法时,因为您无法生成调用它的合法类文件。因此,在反射实现中,有不同类型的调用处理程序,使用生成的代码或本机代码。你不能打败它。interfaceprivate

但更重要的是,Reflection 对每个调用都会进行安全检查。因此,您生成的类将仅在加载和实例化时进行检查,这可能是一个巨大的胜利。或者,您可以在实例上调用以关闭安全检查。然后只剩下自动装箱和 varargs 数组创建的轻微性能损失。setAccessible(true)Method

由于Java 7有两者的替代方案,.最大的优点是,与其他两个不同,它甚至可以在安全受限的环境中工作。的访问检查在获取时执行,但在调用时不执行。它具有所谓的“多态签名”,这意味着您可以使用任意参数类型调用它,而无需自动装箱或数组创建。当然,错误的参数类型将创建一个适当的 .MethodHandleMethodHandleRuntimeException

(更新)在 Java 8 中,可以选择在运行时使用 lambda 表达式和方法参考语言功能的后端。这个后端完全按照开头的描述,动态生成一个类,该类实现代码在编译时已知时可以直接调用的类。确切的机制是特定于实现的,因此未定义,但您可以假设实现将尝试尽可能快地进行调用。Oracle JRE的当前实现完美地做到了这一点。这不仅使您免于生成此类访问器类的负担,还能够执行您永远无法执行的操作 - 甚至通过生成的代码调用方法。我已经更新了示例以包含此解决方案。此示例使用已存在且恰好具有所需方法签名的标准。如果不存在此类匹配,则必须使用具有正确签名的方法创建自己的访问器功能接口。但是,当然,现在示例代码需要 Java 8 才能运行。interfaceprivateinterfaceinterface

下面是一个简单的基准测试示例:

import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.util.function.IntBinaryOperator;

public class TestMethodPerf
{
  private static final int ITERATIONS = 50_000_000;
  private static final int WARM_UP = 10;

  public static void main(String... args) throws Throwable
  {
 // hold result to prevent too much optimizations
    final int[] dummy=new int[4];

    Method reflected=TestMethodPerf.class
      .getDeclaredMethod("myMethod", int.class, int.class);
    final MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodHandle mh=lookup.unreflect(reflected);
    IntBinaryOperator lambda=(IntBinaryOperator)LambdaMetafactory.metafactory(
      lookup, "applyAsInt", MethodType.methodType(IntBinaryOperator.class),
      mh.type(), mh, mh.type()).getTarget().invokeExact();

    for(int i=0; i<WARM_UP; i++)
    {
      dummy[0]+=testDirect(dummy[0]);
      dummy[1]+=testLambda(dummy[1], lambda);
      dummy[2]+=testMH(dummy[1], mh);
      dummy[3]+=testReflection(dummy[2], reflected);
    }
    long t0=System.nanoTime();
    dummy[0]+=testDirect(dummy[0]);
    long t1=System.nanoTime();
    dummy[1]+=testLambda(dummy[1], lambda);
    long t2=System.nanoTime();
    dummy[2]+=testMH(dummy[1], mh);
    long t3=System.nanoTime();
    dummy[3]+=testReflection(dummy[2], reflected);
    long t4=System.nanoTime();
    System.out.printf("direct: %.2fs, lambda: %.2fs, mh: %.2fs, reflection: %.2fs%n",
      (t1-t0)*1e-9, (t2-t1)*1e-9, (t3-t2)*1e-9, (t4-t3)*1e-9);

    // do something with the results
    if(dummy[0]!=dummy[1] || dummy[0]!=dummy[2] || dummy[0]!=dummy[3])
      throw new AssertionError();
  }

  private static int testMH(int v, MethodHandle mh) throws Throwable
  {
    for(int i=0; i<ITERATIONS; i++)
      v+=(int)mh.invokeExact(1000, v);
    return v;
  }

  private static int testReflection(int v, Method mh) throws Throwable
  {
    for(int i=0; i<ITERATIONS; i++)
      v+=(int)mh.invoke(null, 1000, v);
    return v;
  }

  private static int testDirect(int v)
  {
    for(int i=0; i<ITERATIONS; i++)
      v+=myMethod(1000, v);
    return v;
  }

  private static int testLambda(int v, IntBinaryOperator accessor)
  {
    for(int i=0; i<ITERATIONS; i++)
      v+=accessor.applyAsInt(1000, v);
    return v;
  }

  private static int myMethod(int a, int b)
  {
    return a<b? a: b;
  }
}

在我的Java 7设置中打印的旧程序:这表明这是一个很好的选择。现在,在同一台机器上打印的Java 8下运行的更新程序清楚地表明反射性能已经提高到一定程度,除非您使用它来执行lambda技巧,否则它显然优于所有反射替代方案,这并不奇怪,因为它只是一个直接调用(好吧, 几乎:一级间接寻址)。请注意,我制作了目标方法来演示有效调用甚至方法的能力。direct: 0,03s, mh: 0,32s, reflection: 1,05sMethodHandledirect: 0,02s, lambda: 0,02s, mh: 0,35s, reflection: 0,40sMethodHandleprivateprivate

与往常一样,我必须指出这个基准的简单性以及它是多么人性化。但我认为,这种趋势是清晰可见的,更重要的是,结果是令人信服的可以解释的。


答案 2

我创建了一个名为lambda-factory的小型库。它基于LambdaMetafactory,但省去了您查找或创建与该方法匹配的接口的麻烦。

以下是 10E8 迭代的一些示例运行时(可使用类 PerformanceTest 重现):

Lambda: 0.02s, 直接: 0.01s, 反射: 4.64s for method(int, int)
Lambda: 0.03s, Direct: 0.02s, Reflection: 3.23s for method(Object, int)

假设我们有一个名为 的类,它定义了以下方法:MyClass

private static String myStaticMethod(int a, Integer b){ /*some logic*/ }
private float myInstanceMethod(String a, Boolean b){ /*some logic*/ }

我们可以像这样访问这些方法:

Method method = MyClass.class.getDeclaredMethod("myStaticMethod", int.class, Integer.class); //Regular reflection call
Lambda lambda = LambdaFactory.create(method);  
String result = (String) lambda.invoke_for_Object(1000, (Integer) 565); //Don't rely on auto boxing of arguments!

Method method = MyClass.class.getDeclaredMethod("myInstanceMethod", String.class, Boolean.class);
Lambda lambda = LambdaFactory.create(method);
float result = lambda.invoke_for_float(new MyClass(), "Hello", (Boolean) null);  //No need to cast primitive results!

请注意,在调用 lambda 时,您必须选择名称中包含目标方法的返回类型的调用方法。- varargs和自动拳击太贵了。

在上面的示例中,所选方法指示我们正在调用一个方法,该方法返回一个浮点数。如果您尝试访问的方法返回 fx 字符串、盒装基元(整数、布尔值等)或某些自定义对象,则可以调用 。invoke_for_floatinvoke_for_Object

该项目是试验LambdaMetafactory的良好模板,因为它包含各个方面的工作代码:

  1. 静态调用和实例调用
  2. 访问私有方法和其他包中的方法
  3. 'invokeSpecial'逻辑,即如果创建的实现是这样,它绕过了动态方法调度。

推荐