如何使用`MethodHandle`模仿`tableswitch`?

问题描述

上下文:我一直在对使用 invokedynamic 和手动生成字节码之间的区别进行基准测试(这是在决定面向 JVM 的编译器是否应该发出更冗长的“传统" 字节码或只是带有巧妙引导方法invokedynamic 调用)。在此过程中,将字节码映射到至少与 MethodHandles 一样快的 tableswitch 组合器非常简单,但 tableswitch 除外。

问题:是否有使用 MethodHandle 模仿 MethodHandle[] 的技巧?我试着用一个跳转表来模仿它:使用一个常量 arrayElementGetter,用 MethodHandles.invoker 索引到它,然后用 private static MethodHandle makeProductElement(Class<?> receiverClass,List<MethodHandle> getters) { MethodHandle[] BoxedGetters = getters .stream() .map(getter -> getter.asType(getter.type().changeReturnType(java.lang.Object.class))) .toArray(MethodHandle[]::new); MethodHandle getGetter = MethodHandles // (I)H .arrayElementGetter(MethodHandle[].class) .bindTo(BoxedGetters); MethodHandle invokeGetter = MethodHandles.permuteArguments( // (RH)O MethodHandles.invoker(MethodType.methodType(java.lang.Object.class,receiverClass)),MethodType.methodType(java.lang.Object.class,receiverClass,MethodHandle.class),1,0 ); return MethodHandles.filterarguments(invokeGetter,getGetter); } 调用找到的句柄。然而,当我通过 JMH 运行它时,它最终比原始字节码慢了大约 50%。

这是产生方法句柄的代码

invokedynamic

这是初始字节码(我试图用一个 public java.lang.Object productElement(int); descriptor: (I)Ljava/lang/Object; flags: (0x0001) ACC_PUBLIC Code: stack=3,locals=3,args_size=2 0: iload_1 1: istore_2 2: iload_2 3: tableswitch { // 0 to 2 0: 28 1: 38 2: 45 default: 55 } 28: aload_0 29: invokevirtual #62 // Method i:()I 32: invokestatic #81 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 35: goto 67 38: aload_0 39: invokevirtual #65 // Method s:()Ljava/lang/String; 42: goto 67 45: aload_0 46: invokevirtual #68 // Method l:()J 49: invokestatic #85 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long; 52: goto 67 55: new #87 // class java/lang/indexoutofboundsexception 58: dup 59: iload_1 60: invokestatic #93 // Method java/lang/Integer.toString:(I)Ljava/lang/String; 63: invokespecial #96 // Method java/lang/indexoutofboundsexception."<init>":(Ljava/lang/String;)V 66: athrow 67: areturn 调用替换)

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

解决方法

invokedynamic 的好处在于它允许推迟决定,即如何将操作实现到实际运行时。这是 LambdaMetafactoryStringConcatFactory 背后的技巧,它们可能会返回组合方法句柄,例如在您的示例代码或动态生成的代码中,由特定实现自行决定。

甚至有一种可能的组合方法,生成您组合到操作中的类,例如解决现有的LambdaMetafactory

private static MethodHandle makeProductElement(
    MethodHandles.Lookup lookup,Class<?> receiverClass,List<MethodHandle> getters)
    throws Throwable {

    Function[] boxedGetters = new Function[getters.size()];
    MethodType factory = MethodType.methodType(Function.class);
    for(int ix = 0; ix < boxedGetters.length; ix++) {
        MethodHandle mh = getters.get(ix);
        MethodType actual = mh.type().wrap(),generic = actual.erase();
        boxedGetters[ix] = (Function)LambdaMetafactory.metafactory(lookup,"apply",factory,generic,mh,actual).getTarget().invokeExact();
    }

    Object switcher = new Object() {
        final Object get(Object receiver,int index) {
            return boxedGetters[index].apply(receiver);
        }
    };
    return lookup.bind(switcher,"get",MethodType.methodType(Object.class,Object.class,int.class))
        .asType(MethodType.methodType(Object.class,receiverClass,int.class));
}

这使用 LambdaMetafactory 为每个 getter 生成一个 Function 实例,类似于等效的方法引用。然后,调用正确的 Functionapply 方法的实际类被实例化,并返回其 get 方法的方法句柄。

这是与您的方法句柄类似的组合,但对于参考实现,不使用句柄而是使用完全物化的类。我希望组合句柄和这种方法能够在大量调用时收敛到相同的性能,但物化类在中等数量的调用中具有领先优势。

我添加了第一个参数 MethodHandles.Lookup lookup,它应该是 lookup 指令的 bootstrap 方法接收到的 invokedynamic 对象。如果以这种方式使用,生成的函数可以以与包含 invokedynamic 指令的代码相同的方式访问所有方法,包括该类的 private 方法。

或者,您可以自己生成一个包含真正 switch 指令的类。使用 the ASM library,它可能看起来像:

private static MethodHandle makeProductElement(
    MethodHandles.Lookup lookup,List<MethodHandle> getters)
    throws ReflectiveOperationException {

    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
    cw.visit(V1_8,ACC_INTERFACE|ACC_ABSTRACT,lookup.lookupClass().getName().replace('.','/')+"$Switch",null,"java/lang/Object",null);
    MethodType type = MethodType.methodType(Object.class,int.class);
    MethodVisitor mv = cw.visitMethod(ACC_STATIC|ACC_PUBLIC,type.toMethodDescriptorString(),null);
    mv.visitCode();

    Label defaultCase = new Label();
    Label[] cases = new Label[getters.size()];
    for(int ix = 0; ix < cases.length; ix++) cases[ix] = new Label();

    mv.visitVarInsn(ALOAD,0);
    mv.visitVarInsn(ILOAD,1);
    mv.visitTableSwitchInsn(0,cases.length - 1,defaultCase,cases);

    String owner = receiverClass.getName().replace('.','/');

    for(int ix = 0; ix < cases.length; ix++) {
        mv.visitLabel(cases[ix]);
        MethodHandle mh = getters.get(ix);
        mv.visitMethodInsn(INVOKEVIRTUAL,owner,lookup.revealDirect(mh).getName(),mh.type().dropParameterTypes(0,1).toMethodDescriptorString(),false);
        if(mh.type().returnType().isPrimitive()) {
            Class<?> boxed = mh.type().wrap().returnType();
            MethodType box = MethodType.methodType(boxed,mh.type().returnType());
            mv.visitMethodInsn(INVOKESTATIC,boxed.getName().replace('.','/'),"valueOf",box.toMethodDescriptorString(),false);
        }
        mv.visitInsn(ARETURN);
    }
    mv.visitLabel(defaultCase);
    mv.visitTypeInsn(NEW,"java/lang/IndexOutOfBoundsException");
    mv.visitInsn(DUP);
    mv.visitVarInsn(ILOAD,1);
    mv.visitMethodInsn(INVOKESTATIC,"java/lang/String","(I)Ljava/lang/String;",false);
    mv.visitMethodInsn(INVOKESPECIAL,"java/lang/IndexOutOfBoundsException","<init>","(Ljava/lang/String;)V",false);
    mv.visitInsn(ATHROW);
    mv.visitMaxs(-1,-1);
    mv.visitEnd();
    cw.visitEnd();

    lookup = lookup.defineHiddenClass(
        cw.toByteArray(),true,MethodHandles.Lookup.ClassOption.NESTMATE);
    return lookup.findStatic(lookup.lookupClass(),type);
}

这会生成一个带有 static 方法的新类,其中包含 tableswitch 指令和调用(以及我们现在必须自己进行的装箱转换)。此外,它还具有为越界值创建和抛出异常所需的代码。生成类后,它会返回该 static 方法的句柄。

,

我不知道你的时间表。但在 Java 17 中很可能会有 MethodHandles.tableSwitch 操作。目前正在通过 https://github.com/openjdk/jdk/pull/3401/

进行集成

这里有更多关于它的讨论: https://mail.openjdk.java.net/pipermail/core-libs-dev/2021-April/076105.html

,

事情是,tableswitch isn't always compiled to a jump table。对于少量标签,例如在您的示例中,它很可能充当二分搜索。因此,使用常规“if-then”MethodHandles 树将是最接近的等价物。