[英]How to mimic `tableswitch` using `MethodHandle`?
上下文:我一直在對使用invokedynamic
和手動生成字節碼之間的區別進行基准測試(這是在決定針對 JVM 的編譯器是否應該發出更詳細的“傳統”字節碼或只是使用巧妙的引導方法的invokedynamic
調用的上下文中)。 在執行此操作時,將 map 字節碼轉換為至少一樣快的MethodHandles
組合器非常簡單,除了tableswitch
。
問題:有沒有使用MethodHandle
模擬tableswitch
的技巧? 我嘗試使用跳轉表來模仿它:使用常量MethodHandle[]
,使用MethodHandles.invoker
對其進行索引,然后使用arrayElementGetter
調用找到的句柄。 然而,當我通過 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
invokedynamic
的好處是它允許推遲決策,即如何將操作實現到實際運行時。 這是LambdaMetafactory
或StringConcatFactory
背后的技巧,它可能會返回組合方法句柄,如您的示例代碼或動態生成的代碼,由特定實現自行決定。
甚至還有一種可能的組合方法,生成您組合到操作中的類,例如解決已經存在的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
實例,類似於等效的方法引用。 然后,調用正確的Function
的apply
方法的實際 class 被實例化,並返回其get
方法的方法句柄。
這是與您的方法句柄類似的組合,但在參考實現中,不使用句柄,而是使用完全物化的類。 我希望組合句柄和這種方法能夠在大量調用中收斂到相同的性能,但物化類在中等數量的調用中具有領先優勢。
我添加了第一個參數MethodHandles.Lookup lookup
,它應該是調用動態指令的引導方法收到的lookup
invokedynamic
。 如果以這種方式使用,生成的函數可以以與包含invokedynamic
指令的代碼相同的方式訪問所有方法,包括該 class 的private
方法。
或者,您可以自己生成包含真實開關指令的 class。 使用ASM 庫,它可能看起來像:
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);
}
這將生成一個新的 class 和static
方法,其中包含tableswitch
指令和調用(以及我們現在必須自己進行的裝箱轉換)。 此外,它還具有必要的代碼來為越界值創建和拋出異常。 生成 class 后,它返回該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
並不總是編譯為 jump table 。 對於少量標簽,例如在您的示例中,它可能充當二進制搜索。 因此,使用常規的“if-then” MethodHandles 樹將是最接近的等價物。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.