Add some benchmarks for class generation strategies
This mostly just confirms what I hoped/expected - using dynamic constants and MethodHandle.invokeExact is roughly equivalent to calling the method directly.
This commit is contained in:
parent
6ac09742fc
commit
48bd75faac
|
@ -8,6 +8,7 @@
|
|||
/build
|
||||
/projects/*/logs
|
||||
/projects/*/build
|
||||
/projects/*/src/test/generated_tests/
|
||||
/buildSrc/build
|
||||
/out
|
||||
/buildSrc/out
|
||||
|
|
|
@ -11,7 +11,6 @@ plugins {
|
|||
alias(libs.plugins.githubRelease)
|
||||
id("org.jetbrains.gradle.plugin.idea-ext")
|
||||
id("cc-tweaked")
|
||||
id("com.github.ben-manes.versions") version "0.47.0"
|
||||
}
|
||||
|
||||
val isUnstable = project.properties["isUnstable"] == "true"
|
||||
|
|
|
@ -16,7 +16,7 @@ parchment = "2023.08.20"
|
|||
parchmentMc = "1.20.1"
|
||||
|
||||
# Normal dependencies
|
||||
asm = "9.3"
|
||||
asm = "9.5"
|
||||
autoService = "1.1.1"
|
||||
checkerFramework = "3.32.0"
|
||||
cobalt = "0.7.3"
|
||||
|
@ -48,6 +48,7 @@ sodium = "mc1.20-0.4.10"
|
|||
byteBuddy = "1.14.7"
|
||||
hamcrest = "2.2"
|
||||
jqwik = "1.7.4"
|
||||
jmh = "1.37"
|
||||
junit = "5.10.0"
|
||||
|
||||
# Build tools
|
||||
|
@ -112,9 +113,11 @@ rubidium = { module = "maven.modrinth:rubidium", version.ref = "rubidium" }
|
|||
sodium = { module = "maven.modrinth:sodium", version.ref = "sodium" }
|
||||
|
||||
# Testing
|
||||
byteBuddyAgent = { module = "net.bytebuddy:byte-buddy-agent", version.ref = "byteBuddy" }
|
||||
byteBuddy = { module = "net.bytebuddy:byte-buddy", version.ref = "byteBuddy" }
|
||||
byteBuddyAgent = { module = "net.bytebuddy:byte-buddy-agent", version.ref = "byteBuddy" }
|
||||
hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" }
|
||||
jmh = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" }
|
||||
jmh-processor = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" }
|
||||
jqwik-api = { module = "net.jqwik:jqwik-api", version.ref = "jqwik" }
|
||||
jqwik-engine = { module = "net.jqwik:jqwik-engine", version.ref = "jqwik" }
|
||||
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
|
||||
|
|
|
@ -34,6 +34,9 @@ dependencies {
|
|||
testImplementation(libs.bundles.test)
|
||||
testRuntimeOnly(libs.bundles.testRuntime)
|
||||
testRuntimeOnly(libs.slf4j.simple)
|
||||
|
||||
testImplementation(libs.jmh)
|
||||
testAnnotationProcessor(libs.jmh.processor)
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.asm;
|
||||
|
||||
import dan200.computercraft.api.lua.*;
|
||||
import dan200.computercraft.core.methods.LuaMethod;
|
||||
import org.objectweb.asm.*;
|
||||
import org.openjdk.jmh.annotations.Benchmark;
|
||||
import org.openjdk.jmh.annotations.Scope;
|
||||
import org.openjdk.jmh.annotations.State;
|
||||
import org.openjdk.jmh.runner.Runner;
|
||||
import org.openjdk.jmh.runner.RunnerException;
|
||||
import org.openjdk.jmh.runner.options.OptionsBuilder;
|
||||
import org.openjdk.jmh.runner.options.TimeValue;
|
||||
|
||||
import java.lang.constant.ConstantDescs;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.objectweb.asm.Opcodes.*;
|
||||
|
||||
/**
|
||||
* Benchmarks for possible implementation strategies for {@link GeneratorBenchmark}.
|
||||
*/
|
||||
public class GeneratorBenchmark {
|
||||
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
|
||||
|
||||
public static final MethodHandle ADDER;
|
||||
|
||||
static {
|
||||
try {
|
||||
ADDER = LOOKUP.findVirtual(Adder.class, "add", MethodType.methodType(int.class, int.class, int.class));
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final MethodType METHOD_TYPE = MethodType.methodType(MethodResult.class, Object.class, ILuaContext.class, IArguments.class);
|
||||
|
||||
@State(Scope.Benchmark)
|
||||
public static class ScriptScope {
|
||||
final IArguments arguments = new ObjectArguments(1, 5);
|
||||
final LuaMethod asmDirect;
|
||||
final LuaMethod asmMethodHandle;
|
||||
|
||||
public ScriptScope() {
|
||||
try {
|
||||
asmDirect = makeAsmDirect();
|
||||
asmMethodHandle = makeAsmMethodHandle();
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public MethodResult asmDirect(ScriptScope scope) throws LuaException {
|
||||
return scope.asmDirect.apply(Adder.INSTANCE, null, scope.arguments);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public MethodResult asmMethodHandle(ScriptScope scope) throws LuaException {
|
||||
return scope.asmMethodHandle.apply(Adder.INSTANCE, null, scope.arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a {@link LuaMethod} via a generated class which invokes {@link Adder#add(int, int)} directly.
|
||||
*
|
||||
* @return The created {@link LuaMethod} instance.
|
||||
* @throws ReflectiveOperationException If the class could not be generated.
|
||||
*/
|
||||
private static LuaMethod makeAsmDirect() throws ReflectiveOperationException {
|
||||
var bytes = createClass("AsmDirect", mw -> {
|
||||
// Receiver
|
||||
mw.visitVarInsn(ALOAD, 1);
|
||||
mw.visitTypeInsn(CHECKCAST, Type.getInternalName(Adder.class));
|
||||
// Arg 1
|
||||
mw.visitVarInsn(ALOAD, 3);
|
||||
mw.visitInsn(ICONST_0);
|
||||
mw.visitMethodInsn(INVOKEINTERFACE, Type.getInternalName(IArguments.class), "getInt", "(I)I", true);
|
||||
// Arg 2
|
||||
mw.visitVarInsn(ALOAD, 3);
|
||||
mw.visitInsn(ICONST_1);
|
||||
mw.visitMethodInsn(INVOKEINTERFACE, Type.getInternalName(IArguments.class), "getInt", "(I)I", true);
|
||||
// Invoke
|
||||
mw.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Adder.class), "add", "(II)I", false);
|
||||
// Wrap and return
|
||||
mw.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Integer.class), "valueOf", "(I)" + Type.getDescriptor(Integer.class), false);
|
||||
mw.visitMethodInsn(INVOKESTATIC, Type.getInternalName(MethodResult.class), "of", "(Ljava/lang/Object;)" + Type.getDescriptor(MethodResult.class), false);
|
||||
mw.visitInsn(ARETURN);
|
||||
});
|
||||
|
||||
return LOOKUP.defineHiddenClass(bytes, true)
|
||||
.lookupClass().asSubclass(LuaMethod.class).getConstructor().newInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a {@link LuaMethod} via a generated class which invokes {@link Adder#add(int, int)} with {@link MethodHandle#invokeExact(Object...)}.
|
||||
*
|
||||
* @return The created {@link LuaMethod} instance.
|
||||
* @throws ReflectiveOperationException If the class could not be generated.
|
||||
*/
|
||||
private static LuaMethod makeAsmMethodHandle() throws ReflectiveOperationException {
|
||||
var castingHandle = ADDER.asType(MethodType.methodType(Object.class, Object.class, int.class, int.class));
|
||||
|
||||
var bytes = createClass("AsmMethodHandle", mw -> {
|
||||
var classData = new Handle(
|
||||
H_INVOKESTATIC, Type.getInternalName(MethodHandles.class), "classData",
|
||||
MethodType.methodType(Object.class, MethodHandles.Lookup.class, String.class, Class.class).descriptorString(), false
|
||||
);
|
||||
mw.visitLdcInsn(new ConstantDynamic(ConstantDescs.DEFAULT_NAME, MethodHandle.class.descriptorString(), classData));
|
||||
|
||||
// Receiver
|
||||
mw.visitVarInsn(ALOAD, 1);
|
||||
// Arg 1
|
||||
mw.visitVarInsn(ALOAD, 3);
|
||||
mw.visitInsn(ICONST_0);
|
||||
mw.visitMethodInsn(INVOKEINTERFACE, Type.getInternalName(IArguments.class), "getInt", "(I)I", true);
|
||||
// Arg 2
|
||||
mw.visitVarInsn(ALOAD, 3);
|
||||
mw.visitInsn(ICONST_1);
|
||||
mw.visitMethodInsn(INVOKEINTERFACE, Type.getInternalName(IArguments.class), "getInt", "(I)I", true);
|
||||
// Invoke
|
||||
mw.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(MethodHandle.class), "invokeExact", castingHandle.type().descriptorString(), false);
|
||||
// Wrap and return
|
||||
mw.visitMethodInsn(INVOKESTATIC, Type.getInternalName(MethodResult.class), "of", "(Ljava/lang/Object;)" + Type.getDescriptor(MethodResult.class), false);
|
||||
mw.visitInsn(ARETURN);
|
||||
});
|
||||
|
||||
return LOOKUP.defineHiddenClassWithClassData(bytes, castingHandle, true)
|
||||
.lookupClass().asSubclass(LuaMethod.class).getConstructor().newInstance();
|
||||
}
|
||||
|
||||
private static byte[] createClass(String name, Consumer<MethodVisitor> method) {
|
||||
var cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
|
||||
cw.visit(
|
||||
V17, ACC_PUBLIC | ACC_FINAL, GeneratorBenchmark.class.getPackageName().replace('.', '/') + "/" + name,
|
||||
null, "java/lang/Object", new String[]{ Type.getInternalName(LuaMethod.class) }
|
||||
);
|
||||
cw.visitSource("CC generated method", null);
|
||||
|
||||
{ // Constructor just invokes super.
|
||||
var mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
|
||||
mw.visitCode();
|
||||
mw.visitVarInsn(ALOAD, 0);
|
||||
mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
|
||||
mw.visitInsn(RETURN);
|
||||
mw.visitMaxs(0, 0);
|
||||
mw.visitEnd();
|
||||
}
|
||||
|
||||
{
|
||||
var mw = cw.visitMethod(
|
||||
ACC_PUBLIC, "apply", METHOD_TYPE.descriptorString(),
|
||||
null, new String[]{ Type.getInternalName(LuaException.class) }
|
||||
);
|
||||
mw.visitCode();
|
||||
|
||||
method.accept(mw);
|
||||
|
||||
mw.visitMaxs(0, 0);
|
||||
mw.visitEnd();
|
||||
}
|
||||
|
||||
cw.visitEnd();
|
||||
|
||||
return cw.toByteArray();
|
||||
}
|
||||
|
||||
public static class Adder {
|
||||
static final Adder INSTANCE = new Adder();
|
||||
|
||||
public int add(int left, int right) {
|
||||
return left + right;
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String... args) throws RunnerException {
|
||||
var opts = new OptionsBuilder()
|
||||
.include(GeneratorBenchmark.class.getName() + ".*")
|
||||
.warmupIterations(2)
|
||||
.measurementIterations(5)
|
||||
.measurementTime(TimeValue.milliseconds(5000))
|
||||
.jvmArgsPrepend("-server")
|
||||
.forks(3)
|
||||
.build();
|
||||
new Runner(opts).run();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue