Skip to content

Commit b776cf9

Browse files
authored
Inline monomorphic methods in optimized Lua (#1296)
* Inline monomorphic methods in Lua * Limit Lua method lowering to hot loops
1 parent f20350b commit b776cf9

5 files changed

Lines changed: 200 additions & 10 deletions

File tree

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,13 @@ public LuaCompilationUnit transformProgToLua() {
917917
// inliner
918918
stage = 5;
919919
if (runArgs.isInline()) {
920+
// Expose hot loop calls which cannot dispatch anywhere else to the ordinary inliner.
921+
// Calls outside loops keep their established method/slot representation.
922+
beginPhase(5, "lower monomorphic Lua method calls");
923+
LuaMethodCallLowering.transform(imProg);
924+
imTranslator.assertProperties();
925+
timeTaker.endPhase();
926+
920927
beginPhase(5, "inlining");
921928
optimizer.doInlining();
922929
imTranslator2.assertProperties();
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package de.peeeq.wurstscript.translation.imtranslation;
2+
3+
import de.peeeq.wurstscript.jassIm.*;
4+
5+
import java.util.ArrayList;
6+
import java.util.List;
7+
8+
/**
9+
* Lowers loop-local method calls with exactly one possible implementation to ordinary function
10+
* calls on Lua.
11+
*
12+
* <p>The Lua emitter has always used the same direct-call fast path. Performing the lowering before
13+
* optimization exposes hot calls to the ordinary inliner without guessing about receiver types or
14+
* generated method names. Calls outside loops, and calls which can participate in virtual dispatch,
15+
* remain untouched to avoid broad code-shape churn for a speculative gain.
16+
*/
17+
public final class LuaMethodCallLowering {
18+
19+
private LuaMethodCallLowering() {
20+
}
21+
22+
public static int transform(ImProg prog) {
23+
List<ImMethodCall> calls = new ArrayList<>();
24+
prog.accept(new ImProg.DefaultVisitor() {
25+
@Override
26+
public void visit(ImMethodCall call) {
27+
super.visit(call);
28+
if (isInsideLoop(call) && canLowerDirectly(call.getMethod())) {
29+
calls.add(call);
30+
}
31+
}
32+
});
33+
34+
for (ImMethodCall call : calls) {
35+
lower(call);
36+
}
37+
return calls.size();
38+
}
39+
40+
private static boolean isInsideLoop(ImMethodCall call) {
41+
Element owner = call.getParent();
42+
while (owner != null && !(owner instanceof ImFunction)) {
43+
if (owner instanceof ImLoop || owner instanceof ImVarargLoop) {
44+
return true;
45+
}
46+
owner = owner.getParent();
47+
}
48+
return false;
49+
}
50+
51+
public static boolean canLowerDirectly(ImMethod method) {
52+
return method != null
53+
&& !method.getIsAbstract()
54+
&& method.getImplementation() != null
55+
&& method.getSubMethods().isEmpty();
56+
}
57+
58+
private static void lower(ImMethodCall call) {
59+
ImExpr receiver = call.getReceiver();
60+
receiver.setParent(null);
61+
ImExprs arguments = JassIm.ImExprs(receiver);
62+
arguments.addAll(call.getArguments().removeAll());
63+
call.replaceBy(JassIm.ImFunctionCall(call.getTrace(), call.getMethod().getImplementation(),
64+
JassIm.ImTypeArguments(call.getTypeArguments().removeAll()), arguments,
65+
call.getTuplesEliminated(), CallType.NORMAL));
66+
}
67+
}

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/VarargEliminator.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ private Collection<ImMethodCall> collectMonomorphicVarargMethodCalls() {
148148
public void visit(ImMethodCall c) {
149149
super.visit(c);
150150
ImMethod method = c.getMethod();
151-
if (method != null && !method.getIsAbstract() && method.getImplementation() != null
152-
&& method.getSubMethods().isEmpty() && method.getImplementation().hasFlag(IS_VARARG)) {
151+
if (LuaMethodCallLowering.canLowerDirectly(method)
152+
&& method.getImplementation().hasFlag(IS_VARARG)) {
153153
calls.add(c);
154154
}
155155
}

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import de.peeeq.wurstscript.jassIm.*;
66
import de.peeeq.wurstscript.luaAst.*;
77
import de.peeeq.wurstscript.translation.imtranslation.ImTranslator;
8+
import de.peeeq.wurstscript.translation.imtranslation.LuaMethodCallLowering;
89
import de.peeeq.wurstscript.types.TypesHelper;
910

1011
import java.util.Optional;
@@ -187,9 +188,7 @@ public static LuaExpr translate(ImMemberAccess e, LuaTranslator tr) {
187188

188189
public static LuaExpr translate(ImMethodCall e, LuaTranslator tr) {
189190
ImMethod method = e.getMethod();
190-
if (!method.getIsAbstract()
191-
&& method.getImplementation() != null
192-
&& method.getSubMethods().isEmpty()) {
191+
if (LuaMethodCallLowering.canLowerDirectly(method)) {
193192
LuaExprlist args = LuaAst.LuaExprlist();
194193
args.add(e.getReceiver().translateToLua(tr));
195194
for (ImExpr arg : e.getArguments()) {

de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java

Lines changed: 122 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2016,6 +2016,118 @@ public void tinyPopularLuaHelpersInlineWithoutAnnotations() {
20162016
compiled.contains("arithmetic("));
20172017
}
20182018

2019+
@Test
2020+
public void tinyMonomorphicMethodsInlineOnLua() {
2021+
String compiled = compileOptimizedLua(
2022+
"tinyMonomorphicMethodsInlineOnLua",
2023+
"package Test",
2024+
"native consume(int value)",
2025+
"class Accumulator",
2026+
" int offset",
2027+
" construct(int offset)",
2028+
" this.offset = offset",
2029+
" function add(int value) returns int",
2030+
" return value + offset",
2031+
"abstract class Operation",
2032+
" abstract function apply(int value) returns int",
2033+
"class DoubleOperation extends Operation",
2034+
" override function apply(int value) returns int",
2035+
" return value * 2",
2036+
"@noinline function hotLoop(Accumulator accumulator)",
2037+
" var i = 0",
2038+
" while i < 16",
2039+
" consume(accumulator.add(i))",
2040+
" i++",
2041+
"@noinline function dynamicCall(Operation operation)",
2042+
" consume(operation.apply(3))",
2043+
"init",
2044+
" hotLoop(new Accumulator(2))",
2045+
" dynamicCall(new DoubleOperation())"
2046+
);
2047+
2048+
String body = topLevelFunctionBodyWithPrefix(compiled, "hotLoop");
2049+
assertFalse("a tiny method with exactly one implementation must inline in optimized Lua:\n" + body,
2050+
body.contains("Accumulator_add("));
2051+
assertTrue("the inlined method must retain its field read:\n" + body,
2052+
body.contains("Accumulator_offset_storage["));
2053+
String dynamicBody = topLevelFunctionBodyWithPrefix(compiled, "dynamicCall");
2054+
assertTrue("a genuinely virtual method call must retain dispatch:\n" + dynamicBody,
2055+
dynamicBody.contains("dispatch_"));
2056+
}
2057+
2058+
@Test
2059+
public void monomorphicMethodInliningEvaluatesReceiverOnce() {
2060+
test().testLua(true).luaOnly(false).optimize().executeProg().lines(
2061+
"package Test",
2062+
"native testSuccess()",
2063+
"int receiverEvaluations = 0",
2064+
"class Accumulator",
2065+
" int offset",
2066+
" construct(int offset)",
2067+
" this.offset = offset",
2068+
" function add(int value) returns int",
2069+
" return value + offset",
2070+
"function makeAccumulator() returns Accumulator",
2071+
" receiverEvaluations++",
2072+
" return new Accumulator(2)",
2073+
"function evaluate() returns int",
2074+
" var result = 0",
2075+
" var i = 0",
2076+
" while i < 1",
2077+
" result = makeAccumulator().add(5)",
2078+
" i++",
2079+
" return result",
2080+
"init",
2081+
" if evaluate() == 7 and receiverEvaluations == 1",
2082+
" testSuccess()"
2083+
);
2084+
}
2085+
2086+
@Test
2087+
public void monomorphicMethodInliningKeepsCallbackBoundary() {
2088+
String compiled = compileOptimizedLua(
2089+
"monomorphicMethodInliningKeepsCallbackBoundary",
2090+
"package Test",
2091+
"native consume(code callback)",
2092+
"function callback()",
2093+
"class Registrar",
2094+
" function install()",
2095+
" consume(function callback)",
2096+
"@noinline function hotPath(Registrar registrar)",
2097+
" var i = 0",
2098+
" while i < 1",
2099+
" registrar.install()",
2100+
" i++",
2101+
"init",
2102+
" hotPath(new Registrar())"
2103+
);
2104+
2105+
assertFunctionBodyContains(compiled, "hotPath", "Registrar_install(", true);
2106+
}
2107+
2108+
@Test
2109+
public void monomorphicMethodInliningKeepsLocalPlayerBoundary() {
2110+
String compiled = compileOptimizedLua(
2111+
"monomorphicMethodInliningKeepsLocalPlayerBoundary",
2112+
"type player extends handle",
2113+
"package Test",
2114+
"@extern native GetLocalPlayer() returns player",
2115+
"native consume(bool value)",
2116+
"class Probe",
2117+
" function isLocal() returns bool",
2118+
" return GetLocalPlayer() != null",
2119+
"@noinline function hotPath(Probe probe)",
2120+
" var i = 0",
2121+
" while i < 1",
2122+
" consume(probe.isLocal())",
2123+
" i++",
2124+
"init",
2125+
" hotPath(new Probe())"
2126+
);
2127+
2128+
assertFunctionBodyContains(compiled, "hotPath", "Probe_isLocal(", true);
2129+
}
2130+
20192131
/**
20202132
* Measured after Lua native lowering: unit_getX = 59 IM nodes,
20212133
* unit_getAbilityLevel = 63, real_floor = 35, and __wurst_intDiv = 31. Each helper is called
@@ -2050,14 +2162,19 @@ public void optimizedUnitSpatialIndexInnerLoopUsesRawLuaOperations() {
20502162
" query(vec2(0., 0.))"
20512163
);
20522164

2053-
// The spatial-index helpers each have one call site, so the optimizer folds the whole
2054-
// query chain into our retained entry point. Inspect that surviving hot-loop owner.
2165+
// Register-pressure-aware inlining may keep the range helper as the hot-loop owner instead
2166+
// of folding it into query. Inspect whichever function actually retains the loop.
20552167
String body = topLevelFunctionBodyWithPrefix(compiled, "query");
2056-
assertTrue("query loop must read the next-link array directly:\n" + body,
2168+
if (!body.contains("UnitSpatialIndex_nextInCell[")) {
2169+
assertTrue("query must call the retained range helper:\n" + body,
2170+
body.contains("addRangeMatches("));
2171+
body = topLevelFunctionBodyWithPrefix(compiled, "addRangeMatches");
2172+
}
2173+
assertTrue("spatial-index hot loop must read the next-link array directly:\n" + body,
20572174
body.contains("UnitSpatialIndex_nextInCell["));
2058-
assertTrue("query loop must read cached X directly:\n" + body,
2175+
assertTrue("spatial-index hot loop must read cached X directly:\n" + body,
20592176
body.contains("UnitSpatialIndex_lastX["));
2060-
assertTrue("query loop must read cached Y directly:\n" + body,
2177+
assertTrue("spatial-index hot loop must read cached Y directly:\n" + body,
20612178
body.contains("UnitSpatialIndex_lastY["));
20622179
assertFalse("typed array reads must not retain assurance calls:\n" + body,
20632180
body.contains("__wurst_ensure"));

0 commit comments

Comments
 (0)