diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java index bf13da627..0d1efb8f9 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java @@ -917,6 +917,13 @@ public LuaCompilationUnit transformProgToLua() { // inliner stage = 5; if (runArgs.isInline()) { + // Expose hot loop calls which cannot dispatch anywhere else to the ordinary inliner. + // Calls outside loops keep their established method/slot representation. + beginPhase(5, "lower monomorphic Lua method calls"); + LuaMethodCallLowering.transform(imProg); + imTranslator.assertProperties(); + timeTaker.endPhase(); + beginPhase(5, "inlining"); optimizer.doInlining(); imTranslator2.assertProperties(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaMethodCallLowering.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaMethodCallLowering.java new file mode 100644 index 000000000..5ced26a84 --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaMethodCallLowering.java @@ -0,0 +1,67 @@ +package de.peeeq.wurstscript.translation.imtranslation; + +import de.peeeq.wurstscript.jassIm.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * Lowers loop-local method calls with exactly one possible implementation to ordinary function + * calls on Lua. + * + *

The Lua emitter has always used the same direct-call fast path. Performing the lowering before + * optimization exposes hot calls to the ordinary inliner without guessing about receiver types or + * generated method names. Calls outside loops, and calls which can participate in virtual dispatch, + * remain untouched to avoid broad code-shape churn for a speculative gain. + */ +public final class LuaMethodCallLowering { + + private LuaMethodCallLowering() { + } + + public static int transform(ImProg prog) { + List calls = new ArrayList<>(); + prog.accept(new ImProg.DefaultVisitor() { + @Override + public void visit(ImMethodCall call) { + super.visit(call); + if (isInsideLoop(call) && canLowerDirectly(call.getMethod())) { + calls.add(call); + } + } + }); + + for (ImMethodCall call : calls) { + lower(call); + } + return calls.size(); + } + + private static boolean isInsideLoop(ImMethodCall call) { + Element owner = call.getParent(); + while (owner != null && !(owner instanceof ImFunction)) { + if (owner instanceof ImLoop || owner instanceof ImVarargLoop) { + return true; + } + owner = owner.getParent(); + } + return false; + } + + public static boolean canLowerDirectly(ImMethod method) { + return method != null + && !method.getIsAbstract() + && method.getImplementation() != null + && method.getSubMethods().isEmpty(); + } + + private static void lower(ImMethodCall call) { + ImExpr receiver = call.getReceiver(); + receiver.setParent(null); + ImExprs arguments = JassIm.ImExprs(receiver); + arguments.addAll(call.getArguments().removeAll()); + call.replaceBy(JassIm.ImFunctionCall(call.getTrace(), call.getMethod().getImplementation(), + JassIm.ImTypeArguments(call.getTypeArguments().removeAll()), arguments, + call.getTuplesEliminated(), CallType.NORMAL)); + } +} diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/VarargEliminator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/VarargEliminator.java index e522281d0..c2371a729 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/VarargEliminator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/VarargEliminator.java @@ -148,8 +148,8 @@ private Collection collectMonomorphicVarargMethodCalls() { public void visit(ImMethodCall c) { super.visit(c); ImMethod method = c.getMethod(); - if (method != null && !method.getIsAbstract() && method.getImplementation() != null - && method.getSubMethods().isEmpty() && method.getImplementation().hasFlag(IS_VARARG)) { + if (LuaMethodCallLowering.canLowerDirectly(method) + && method.getImplementation().hasFlag(IS_VARARG)) { calls.add(c); } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java index 3c1c92871..769950154 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java @@ -5,6 +5,7 @@ import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.luaAst.*; import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; +import de.peeeq.wurstscript.translation.imtranslation.LuaMethodCallLowering; import de.peeeq.wurstscript.types.TypesHelper; import java.util.Optional; @@ -187,9 +188,7 @@ public static LuaExpr translate(ImMemberAccess e, LuaTranslator tr) { public static LuaExpr translate(ImMethodCall e, LuaTranslator tr) { ImMethod method = e.getMethod(); - if (!method.getIsAbstract() - && method.getImplementation() != null - && method.getSubMethods().isEmpty()) { + if (LuaMethodCallLowering.canLowerDirectly(method)) { LuaExprlist args = LuaAst.LuaExprlist(); args.add(e.getReceiver().translateToLua(tr)); for (ImExpr arg : e.getArguments()) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java index 58fea9e67..5d579530e 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java @@ -2016,6 +2016,118 @@ public void tinyPopularLuaHelpersInlineWithoutAnnotations() { compiled.contains("arithmetic(")); } + @Test + public void tinyMonomorphicMethodsInlineOnLua() { + String compiled = compileOptimizedLua( + "tinyMonomorphicMethodsInlineOnLua", + "package Test", + "native consume(int value)", + "class Accumulator", + " int offset", + " construct(int offset)", + " this.offset = offset", + " function add(int value) returns int", + " return value + offset", + "abstract class Operation", + " abstract function apply(int value) returns int", + "class DoubleOperation extends Operation", + " override function apply(int value) returns int", + " return value * 2", + "@noinline function hotLoop(Accumulator accumulator)", + " var i = 0", + " while i < 16", + " consume(accumulator.add(i))", + " i++", + "@noinline function dynamicCall(Operation operation)", + " consume(operation.apply(3))", + "init", + " hotLoop(new Accumulator(2))", + " dynamicCall(new DoubleOperation())" + ); + + String body = topLevelFunctionBodyWithPrefix(compiled, "hotLoop"); + assertFalse("a tiny method with exactly one implementation must inline in optimized Lua:\n" + body, + body.contains("Accumulator_add(")); + assertTrue("the inlined method must retain its field read:\n" + body, + body.contains("Accumulator_offset_storage[")); + String dynamicBody = topLevelFunctionBodyWithPrefix(compiled, "dynamicCall"); + assertTrue("a genuinely virtual method call must retain dispatch:\n" + dynamicBody, + dynamicBody.contains("dispatch_")); + } + + @Test + public void monomorphicMethodInliningEvaluatesReceiverOnce() { + test().testLua(true).luaOnly(false).optimize().executeProg().lines( + "package Test", + "native testSuccess()", + "int receiverEvaluations = 0", + "class Accumulator", + " int offset", + " construct(int offset)", + " this.offset = offset", + " function add(int value) returns int", + " return value + offset", + "function makeAccumulator() returns Accumulator", + " receiverEvaluations++", + " return new Accumulator(2)", + "function evaluate() returns int", + " var result = 0", + " var i = 0", + " while i < 1", + " result = makeAccumulator().add(5)", + " i++", + " return result", + "init", + " if evaluate() == 7 and receiverEvaluations == 1", + " testSuccess()" + ); + } + + @Test + public void monomorphicMethodInliningKeepsCallbackBoundary() { + String compiled = compileOptimizedLua( + "monomorphicMethodInliningKeepsCallbackBoundary", + "package Test", + "native consume(code callback)", + "function callback()", + "class Registrar", + " function install()", + " consume(function callback)", + "@noinline function hotPath(Registrar registrar)", + " var i = 0", + " while i < 1", + " registrar.install()", + " i++", + "init", + " hotPath(new Registrar())" + ); + + assertFunctionBodyContains(compiled, "hotPath", "Registrar_install(", true); + } + + @Test + public void monomorphicMethodInliningKeepsLocalPlayerBoundary() { + String compiled = compileOptimizedLua( + "monomorphicMethodInliningKeepsLocalPlayerBoundary", + "type player extends handle", + "package Test", + "@extern native GetLocalPlayer() returns player", + "native consume(bool value)", + "class Probe", + " function isLocal() returns bool", + " return GetLocalPlayer() != null", + "@noinline function hotPath(Probe probe)", + " var i = 0", + " while i < 1", + " consume(probe.isLocal())", + " i++", + "init", + " hotPath(new Probe())" + ); + + assertFunctionBodyContains(compiled, "hotPath", "Probe_isLocal(", true); + } + /** * Measured after Lua native lowering: unit_getX = 59 IM nodes, * unit_getAbilityLevel = 63, real_floor = 35, and __wurst_intDiv = 31. Each helper is called @@ -2050,14 +2162,19 @@ public void optimizedUnitSpatialIndexInnerLoopUsesRawLuaOperations() { " query(vec2(0., 0.))" ); - // The spatial-index helpers each have one call site, so the optimizer folds the whole - // query chain into our retained entry point. Inspect that surviving hot-loop owner. + // Register-pressure-aware inlining may keep the range helper as the hot-loop owner instead + // of folding it into query. Inspect whichever function actually retains the loop. String body = topLevelFunctionBodyWithPrefix(compiled, "query"); - assertTrue("query loop must read the next-link array directly:\n" + body, + if (!body.contains("UnitSpatialIndex_nextInCell[")) { + assertTrue("query must call the retained range helper:\n" + body, + body.contains("addRangeMatches(")); + body = topLevelFunctionBodyWithPrefix(compiled, "addRangeMatches"); + } + assertTrue("spatial-index hot loop must read the next-link array directly:\n" + body, body.contains("UnitSpatialIndex_nextInCell[")); - assertTrue("query loop must read cached X directly:\n" + body, + assertTrue("spatial-index hot loop must read cached X directly:\n" + body, body.contains("UnitSpatialIndex_lastX[")); - assertTrue("query loop must read cached Y directly:\n" + body, + assertTrue("spatial-index hot loop must read cached Y directly:\n" + body, body.contains("UnitSpatialIndex_lastY[")); assertFalse("typed array reads must not retain assurance calls:\n" + body, body.contains("__wurst_ensure"));