【问题标题】:How to mimic `tableswitch` using `MethodHandle`?如何使用`MethodHandle`模仿`tableswitch`?
【发布时间】:2021-07-01 00:21:11
【问题描述】:

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

问题: 有没有使用MethodHandle 模仿tableswitch 的技巧?我尝试用跳转表来模仿它:使用常量MethodHandle[],用arrayElementGetter 对其进行索引,然后用MethodHandles.invoker 调用找到的句柄。然而,当我通过 JMH 运行它时,它最终比原始字节码慢了大约 50%。

下面是生成方法句柄的代码:

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, 1, getGetter);
}

这是初始字节码(我试图用一个 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

【问题讨论】:

  • 它是否必须是“暂时离开”的方法?引导方法可以决定生成包含切换指令的字节码或返回组合方法句柄。 StringConcatFactory 的参考实现,举一个实际的例子,能够同时生成代码或组成句柄,当然,编译器生成的 invokedynamic 指令不受该运行时选择的影响。
  • @Holger 我根本没有考虑过这种选择,如果可能的话,那确实没问题。你有StringConcatFactory相关部分的链接吗?我找不到任何直接从字节码构造MethodHandle 的示例。
  • 这不是“直接从字节码”,而是从字节码创建一个类,并返回该类方法的句柄。首选机制是defineHiddenClass,它允许生成的类调用查找类的private 方法。这已成为 JDK 15 中的标准功能,之前的版本为此使用了 sun.misc.Unsafe
  • @Holger 感谢您的指点! AFAICT, defineHiddenClass 根本不用于字符串 concat 的东西,但我确实在 java.lang.invoke.InnerClassLambdaMetafactory 中找到了一个合法(并且不足为奇)的用法。如果您想为某些 tableswitch 指令汇总一个执行此操作的示例(即使它与我的问题的功能不完全匹配)我会接受

标签: jvm bytecode methodhandle invokedynamic


【解决方案1】:

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,它应该是invokedynamic 指令的引导方法接收到的lookup 对象。如果以这种方式使用,生成的函数可以像包含invokedynamic 指令的代码一样访问所有方法,包括该类的private 方法。

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

private static MethodHandle makeProductElement(
    MethodHandles.Lookup lookup, Class<?> receiverClass, 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, receiverClass, int.class);
    MethodVisitor mv = cw.visitMethod(ACC_STATIC|ACC_PUBLIC, "get",
        type.toMethodDescriptorString(), null, 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",
        "valueOf", "(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(), "get", type);
}

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

【讨论】:

  • 只是想补充一点,我几乎逐字逐句地尝试了第二个示例(我所做的只是将invokevirtual 切换为更简单的getfield)并且性能(不出所料)与初始样板相同方法。这就是成功!这是一个需要注意的很好的逃生舱口 - 谢谢。
【解决方案2】:

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

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

【讨论】:

  • 这是个好消息,感谢您的链接!我一直在寻找这样的组合器(并且间接地想知道在哪里提出这个功能请求)。我的时间线并不狭窄,但 codegen 仍然需要支持 Java 8(如果用户明确选择 Java 17 的目标,我可能会使用它)。我猜这也是javac 需要的东西,如果他们想执行他们将switch 编译转换为使用 indy 的东西的计划。
  • 看到这个 SO 问题实际上提醒我要提交 PR:D
【解决方案3】:

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

【讨论】:

  • 在我的例子中,我同意嵌套if-s 会更好。也就是说,我正在尝试构建一个通用的引导方法,即使对于具有许多字段的类也应该是高性能的。无论如何,我都会尝试这种方法来确认它在大/小记录上的表现。
猜你喜欢
  • 2019-03-28
  • 1970-01-01
  • 2019-02-05
  • 2017-12-12
  • 1970-01-01
  • 2022-07-26
  • 1970-01-01
  • 2021-01-25
  • 1970-01-01
相关资源
最近更新 更多