更快地替代Java的反思

ovu*_*tin 56 java reflection performance

我们知道,反射是一种灵活但缓慢的方法,用于在运行时维护和修改代码的行为.

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

Hol*_*ger 123

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

但更重要的是,Reflection会对每次调用进行安全检查.因此,只有在加载和实例化时才会检查生成的类,这可能是一个很大的胜利.另外,也可调用setAccessible(true)一个Method实例打开的安全检查.然后,只剩下自动装箱和varargs阵列创建的轻微性能损失.

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

(更新)使用Java 8,可以选择在运行时使用lambda表达式的后端和方法引用语言功能.这个后端完全符合开头所描述的内容,动态生成一个类,实现一个interface代码,可以在编译时知道它直接调用.确切的机制是特定于实现的,因此是未定义的,但您可以假设实现将尽可能快地进行调用.Oracle JRE的当前实现完美无缺.这不仅可以减轻您生成此类访问者类的负担,还可以执行您从未做过的事情 - 甚至可以调用private方法通过生成的代码.我已更新示例以包含此解决方案.此示例使用interface已存在且恰好具有所需方法签名的标准.如果不interface存在这样的匹配,则必须使用具有正确签名的方法创建自己的访问者功能接口.但是,当然,现在示例代码需要运行Java 8.

这是一个简单的基准示例:

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;
  }
}
Run Code Online (Sandbox Code Playgroud)

在我的Java 7设置中打印的旧程序:direct: 0,03s, mh: 0,32s, reflection: 1,05s这表明这MethodHandle是一个很好的选择.现在,在同一台机器上运行的Java 8下运行的更新程序direct: 0,02s, lambda: 0,02s, mh: 0,35s, reflection: 0,40s清楚地表明,反射性能已经提高到可以处理MethodHandle不必要的程度,除非你用它来做lambda技巧,它明显优于所有反射选择,这并不奇怪,因为它只是一个直接的呼叫(好吧,差不多:一个间接的水平).请注意,我制作了目标方法private来演示private有效调用方法的能力.

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

  • @Boyolame:见[这里](http://stackoverflow.com/a/27605965/2711488) (3认同)
  • 在目前的测试版中,他们不是.但是,那就是'beta`.它们将被lambda实现使用,因此需要更多的压力来改进它. (2认同)
  • @Hervian:它将实现由名称和类型指定的方法。当您使用 [`altMetafactory`](https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/LambdaMetafactory.html#altMetafactory-java.lang.invoke.MethodHandles.Lookup- java.lang.String-java.lang.invoke.MethodType-java.lang.Object...-),你可以指定要实现的重载的类型签名,但它们仍然具有相同的名称(这只是为了处理桥接方法)。不会实现其他方法(它*可能*覆盖`java.lang.Object`的方法,因为这是允许的,但目前,它不允许)。 (2认同)
  • @xer21:好吧,对于这种程度的差异,您可以归咎于不太复杂的基准测试方法(如果我们敢称其为基准测试的话)。仅显示趋势很有用,即更大的幅度仍然类似于“直接和 lambda 10,mh 100,反射 200”,尽管我很惊讶地看到在最近的 JDK 中反射 *那* 糟糕。 (2认同)
  • @LukeHutchison 你必须指定擦除的签名 (`(Ljava/lang/Object;)V`) 作为 `samMethodType` 参数和实际的类型签名 (`(Lio/vertx/ext/web/RoutingContext;)V`) 作为`实例化方法类型`。对比 [`LambdaMetafactory.metafactory(...)`](https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/LambdaMetafactory.html#metafactory-java.lang.invoke .MethodHandles.Lookup-java.lang.String-java.lang.invoke.MethodType-java.lang.invoke.MethodType-java.lang.invoke.MethodHandle-java.lang.invoke.MethodType-)。Stackverflow 上有更多关于“LambdaMetafactory”的问答... (2认同)

Her*_*ian 15

我创建了一个名为lambda-factory的小型库.它基于LambdaMetafactory,但可以省去查找或创建与该方法匹配的界面的麻烦.

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

Lambda:0.02s,Direct: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*/ }
Run Code Online (Sandbox Code Playgroud)

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

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!
Run Code Online (Sandbox Code Playgroud)

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

在上面的示例中,所选invoke_for_float方法表明我们正在调用一个返回float的方法.如果您尝试访问的方法返回fx一个字符串,一个盒装基元(整数,布尔等)或一些自定义对象,您可以调用invoke_for_Object.

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

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


Try*_*ing 6

反射的替代方法是使用Interface.只需从Joshua Bloch那里获取Effective Java.

我们可以通过仅以非常有限的形式使用它来获得反射的许多好处,同时产生很少的成本.对于许多必须使用在编译时不可用的类的程序,在编译时存在一个适当的接口或超类来引用该类.如果是这种情况,您可以反射创建实例并通过其接口或超类正常访问它们.如果适当的构造函数没有参数,那么你甚至不需要使用java.lang.reflect; Class.newInstance方法提供所需的功能.

使用反射仅用于创建对象,即

// Reflective instantiation with interface access
   public static void main(String[] args) {
       // Translate the class name into a Class object
       Class<?> cl = null;
       try {
           cl = Class.forName(args[0]);
       } catch(ClassNotFoundException e) {
           System.err.println("Class not found.");
           System.exit(1);
       }
       // Instantiate the class
       Set<String> s = null;
       try {
           s = (Set<String>) cl.newInstance();
       } catch(IllegalAccessException e) {
           System.err.println("Class not accessible.");
           System.exit(1);
       } catch(InstantiationException e) {
           System.err.println("Class not instantiable.");
           System.exit(1);
       }
       // Exercise the set
       s.addAll(Arrays.asList(args).subList(1, args.length));
       System.out.println(s);
}
Run Code Online (Sandbox Code Playgroud)

虽然这个程序只是一个玩具,但它演示的技术非常强大.玩具程序可以很容易地变成通用集测试程序,通过积极地操纵一个或多个实例并检查它们是否遵守Set契约来验证指定的Set实现.同样,它可以变成通用的集合性能分析工具.实际上,该技术足以实现全面的服务提供者框架.大多数时候,这种技术就是你在反思中所需要的.

这个例子说明了反射的两个缺点.首先,该示例可以生成三个运行时错误,如果未使用反射实例化,则所有错误都是编译时错误.其次,从名称生成类的实例需要20行繁琐的代码,而构造函数调用可以整齐地放在一行上.但是,这些缺点仅限于实例化对象的程序部分.实例化后,它与任何其他Set实例无法区分.