Skip to content

Commit 0e83860

Browse files
authored
Optimize generated Lua table access in hot loops (#1297)
* Optimize generated Lua table access in hot loops * Update Lua inliner storage assertion
1 parent 23cfe5e commit 0e83860

2 files changed

Lines changed: 246 additions & 7 deletions

File tree

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

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
public class LuaTranslator {
2626
private static final int LUA_LOCALS_LIMIT = 200;
27+
private static final int LUA_STORAGE_ALIAS_LOCAL_BUDGET = 190;
28+
private static final int LUA_STORAGE_ALIAS_LIMIT = 16;
2729
private static final List<String> HASHTABLE_HANDLE_SAVE_NAMES = Arrays.asList(
2830
"SavePlayerHandle", "SaveWidgetHandle", "SaveDestructableHandle", "SaveItemHandle", "SaveUnitHandle",
2931
"SaveAbilityHandle", "SaveTimerHandle", "SaveTriggerHandle", "SaveTriggerConditionHandle",
@@ -80,6 +82,8 @@ public class LuaTranslator {
8082
private final LuaStatements deferredMainInit = LuaAst.LuaStatements();
8183
private final Map<String, Integer> uniqueNameCounters = new HashMap<>();
8284
private final Set<String> usedNames = LuaReservedNames.all();
85+
private final Set<LuaVariable> localizableStorageTables = Collections.newSetFromMap(new IdentityHashMap<>());
86+
private LuaFunction bootstrapFunction;
8387
private final Set<String> emittedDispatchSlots = new HashSet<>();
8488
private final Map<ImClass, Set<String>> emittedDispatchSlotsByClass = new IdentityHashMap<>();
8589
private final Map<ImClass, Map<String, Set<DispatchGroupIdentity>>> emittedDispatchSlotGroupsByClass = new IdentityHashMap<>();
@@ -222,8 +226,10 @@ public LuaVariable initFor(ImClass a) {
222226
GetAForB<ImVar, LuaVariable> luaFieldStorage = new GetAForB<ImVar, LuaVariable>() {
223227
@Override
224228
public LuaVariable initFor(ImVar field) {
225-
return LuaAst.LuaVariable(uniqueName(field.getName() + "_storage"),
229+
LuaVariable storage = LuaAst.LuaVariable(uniqueName(field.getName() + "_storage"),
226230
LuaAst.LuaTableConstructor(LuaAst.LuaTableFields()));
231+
localizableStorageTables.add(storage);
232+
return storage;
227233
}
228234
};
229235

@@ -382,6 +388,7 @@ public LuaCompilationUnit translate() {
382388

383389
createBootstrapFunction();
384390
cleanStatements();
391+
localizeHotStorageTables();
385392
enforceLuaLocalLimits();
386393

387394
return luaModel;
@@ -509,6 +516,7 @@ private void createBootstrapFunction() {
509516
LuaVariable doneFlag = LuaAst.LuaVariable(uniqueName("__wurst_bootstrap_done"), LuaAst.LuaExprNull());
510517
luaModel.add(doneFlag);
511518
LuaFunction boot = LuaAst.LuaFunction(uniqueName("__wurst_init_bootstrap"), LuaAst.LuaParams(), LuaAst.LuaStatements());
519+
bootstrapFunction = boot;
512520
boot.getBody().add(LuaAst.LuaLiteral("if " + doneFlag.getName() + " then return end"));
513521
boot.getBody().add(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(doneFlag), LuaAst.LuaExprBoolVal(true)));
514522
List<LuaStatement> stmts = new ArrayList<>();
@@ -955,6 +963,107 @@ public void visit(LuaMethod m) {
955963
});
956964
}
957965

966+
/**
967+
* Cache compiler-owned array and field-storage tables in function locals when they are indexed
968+
* from a loop. This removes a global lookup from every dynamic iteration without changing cold
969+
* paths or making assumptions about user-authored Lua tables. The cap and headroom avoid turning
970+
* the optimization into register pressure or immediately triggering the locals-table fallback.
971+
*/
972+
private void localizeHotStorageTables() {
973+
luaModel.accept(new LuaModel.DefaultVisitor() {
974+
@Override
975+
public void visit(LuaFunction f) {
976+
super.visit(f);
977+
localizeHotStorageTables(f.getParams(), f.getBody());
978+
}
979+
980+
@Override
981+
public void visit(LuaMethod m) {
982+
super.visit(m);
983+
localizeHotStorageTables(m.getParams(), m.getBody());
984+
}
985+
});
986+
}
987+
988+
private void localizeHotStorageTables(LuaParams params, LuaStatements body) {
989+
Map<LuaVariable, Integer> loopAccessCounts = new IdentityHashMap<>();
990+
collectLoopStorageAccesses(body, false, loopAccessCounts);
991+
if (loopAccessCounts.isEmpty()) {
992+
return;
993+
}
994+
995+
int existingLocals = params.size() + collectFunctionScopeLocals(body).size();
996+
int aliasCount = Math.min(LUA_STORAGE_ALIAS_LIMIT,
997+
LUA_STORAGE_ALIAS_LOCAL_BUDGET - existingLocals);
998+
if (aliasCount <= 0) {
999+
return;
1000+
}
1001+
1002+
List<LuaVariable> selected = new ArrayList<>(loopAccessCounts.keySet());
1003+
selected.sort(Comparator.<LuaVariable>comparingInt(loopAccessCounts::get).reversed()
1004+
.thenComparing(LuaVariable::getName));
1005+
if (selected.size() > aliasCount) {
1006+
selected = new ArrayList<>(selected.subList(0, aliasCount));
1007+
}
1008+
1009+
Map<LuaVariable, LuaVariable> aliases = new IdentityHashMap<>();
1010+
for (LuaVariable storage : selected) {
1011+
aliases.put(storage, LuaAst.LuaVariable(uniqueName(storage.getName() + "_local"),
1012+
LuaAst.LuaExprVarAccess(storage)));
1013+
}
1014+
rewriteStorageAccesses(body, aliases);
1015+
1016+
int insertionIndex = bootstrapCallInsertionIndex(body);
1017+
for (LuaVariable storage : selected) {
1018+
body.add(insertionIndex++, aliases.get(storage));
1019+
}
1020+
}
1021+
1022+
private void collectLoopStorageAccesses(de.peeeq.wurstscript.luaAst.Element element, boolean insideLoop,
1023+
Map<LuaVariable, Integer> counts) {
1024+
if (element instanceof LuaExprFunctionAbstraction
1025+
|| element instanceof LuaFunction || element instanceof LuaMethod) {
1026+
return;
1027+
}
1028+
boolean childInsideLoop = insideLoop || element instanceof LuaWhile;
1029+
if (childInsideLoop && element instanceof LuaExprArrayAccess) {
1030+
LuaExpr left = ((LuaExprArrayAccess) element).getLeft();
1031+
if (left instanceof LuaExprVarAccess) {
1032+
LuaVariable storage = ((LuaExprVarAccess) left).getVar();
1033+
if (localizableStorageTables.contains(storage)) {
1034+
counts.merge(storage, 1, Integer::sum);
1035+
}
1036+
}
1037+
}
1038+
element.forEachElement(child -> collectLoopStorageAccesses(child, childInsideLoop, counts));
1039+
}
1040+
1041+
private void rewriteStorageAccesses(de.peeeq.wurstscript.luaAst.Element element,
1042+
Map<LuaVariable, LuaVariable> aliases) {
1043+
if (element instanceof LuaExprFunctionAbstraction
1044+
|| element instanceof LuaFunction || element instanceof LuaMethod) {
1045+
return;
1046+
}
1047+
if (element instanceof LuaExprArrayAccess) {
1048+
LuaExpr left = ((LuaExprArrayAccess) element).getLeft();
1049+
if (left instanceof LuaExprVarAccess) {
1050+
LuaVariable alias = aliases.get(((LuaExprVarAccess) left).getVar());
1051+
if (alias != null) {
1052+
left.replaceBy(LuaAst.LuaExprVarAccess(alias));
1053+
}
1054+
}
1055+
}
1056+
element.forEachElement(child -> rewriteStorageAccesses(child, aliases));
1057+
}
1058+
1059+
private int bootstrapCallInsertionIndex(LuaStatements body) {
1060+
if (bootstrapFunction != null && !body.isEmpty() && body.get(0) instanceof LuaExprFunctionCall
1061+
&& ((LuaExprFunctionCall) body.get(0)).getFunc() == bootstrapFunction) {
1062+
return 1;
1063+
}
1064+
return 0;
1065+
}
1066+
9581067
private void spillLocalsIntoTableIfNeeded(String functionName, LuaParams params, LuaStatements body) {
9591068
List<LuaVariable> scopeLocals = collectFunctionScopeLocals(body);
9601069
int localCount = params.size() + scopeLocals.size();
@@ -2037,6 +2146,9 @@ private void translateGlobal(ImVar v) {
20372146
return;
20382147
}
20392148
LuaVariable lv = luaVar.getFor(v);
2149+
if (v.getType() instanceof ImArrayType || v.getType() instanceof ImArrayTypeMulti) {
2150+
localizableStorageTables.add(lv);
2151+
}
20402152
lv.setInitialValue(LuaAst.LuaExprNull());
20412153
luaModel.add(lv);
20422154
deferMainInit(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(lv), defaultValue(v.getType())));

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

Lines changed: 133 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2048,8 +2048,12 @@ public void tinyMonomorphicMethodsInlineOnLua() {
20482048
String body = topLevelFunctionBodyWithPrefix(compiled, "hotLoop");
20492049
assertFalse("a tiny method with exactly one implementation must inline in optimized Lua:\n" + body,
20502050
body.contains("Accumulator_add("));
2051-
assertTrue("the inlined method must retain its field read:\n" + body,
2052-
body.contains("Accumulator_offset_storage["));
2051+
java.util.regex.Matcher offsetAlias = java.util.regex.Pattern
2052+
.compile("local (\\w+) = Accumulator_offset_storage").matcher(body);
2053+
assertTrue("the inlined method's field storage must be localized:\n" + body,
2054+
offsetAlias.find());
2055+
assertTrue("the inlined method must retain its field read through the local alias:\n" + body,
2056+
body.contains(offsetAlias.group(1) + "["));
20532057
String dynamicBody = topLevelFunctionBodyWithPrefix(compiled, "dynamicCall");
20542058
assertTrue("a genuinely virtual method call must retain dispatch:\n" + dynamicBody,
20552059
dynamicBody.contains("dispatch_"));
@@ -2165,16 +2169,31 @@ public void optimizedUnitSpatialIndexInnerLoopUsesRawLuaOperations() {
21652169
// Register-pressure-aware inlining may keep the range helper as the hot-loop owner instead
21662170
// of folding it into query. Inspect whichever function actually retains the loop.
21672171
String body = topLevelFunctionBodyWithPrefix(compiled, "query");
2168-
if (!body.contains("UnitSpatialIndex_nextInCell[")) {
2172+
if (!body.contains("= UnitSpatialIndex_nextInCell")) {
21692173
assertTrue("query must call the retained range helper:\n" + body,
21702174
body.contains("addRangeMatches("));
21712175
body = topLevelFunctionBodyWithPrefix(compiled, "addRangeMatches");
21722176
}
2173-
assertTrue("spatial-index hot loop must read the next-link array directly:\n" + body,
2177+
java.util.regex.Matcher nextAlias = java.util.regex.Pattern
2178+
.compile("local (\\w+) = UnitSpatialIndex_nextInCell").matcher(body);
2179+
java.util.regex.Matcher xAlias = java.util.regex.Pattern
2180+
.compile("local (\\w+) = UnitSpatialIndex_lastX").matcher(body);
2181+
java.util.regex.Matcher yAlias = java.util.regex.Pattern
2182+
.compile("local (\\w+) = UnitSpatialIndex_lastY").matcher(body);
2183+
assertTrue("spatial-index hot loop must localize the next-link array:\n" + body, nextAlias.find());
2184+
assertTrue("spatial-index hot loop must localize cached X:\n" + body, xAlias.find());
2185+
assertTrue("spatial-index hot loop must localize cached Y:\n" + body, yAlias.find());
2186+
assertTrue("spatial-index hot loop must index the localized next-link array:\n" + body,
2187+
body.contains(nextAlias.group(1) + "["));
2188+
assertTrue("spatial-index hot loop must index localized cached X:\n" + body,
2189+
body.contains(xAlias.group(1) + "["));
2190+
assertTrue("spatial-index hot loop must index localized cached Y:\n" + body,
2191+
body.contains(yAlias.group(1) + "["));
2192+
assertFalse("spatial-index loop must not retain global next-link lookups:\n" + body,
21742193
body.contains("UnitSpatialIndex_nextInCell["));
2175-
assertTrue("spatial-index hot loop must read cached X directly:\n" + body,
2194+
assertFalse("spatial-index loop must not retain global cached-X lookups:\n" + body,
21762195
body.contains("UnitSpatialIndex_lastX["));
2177-
assertTrue("spatial-index hot loop must read cached Y directly:\n" + body,
2196+
assertFalse("spatial-index loop must not retain global cached-Y lookups:\n" + body,
21782197
body.contains("UnitSpatialIndex_lastY["));
21792198
assertFalse("typed array reads must not retain assurance calls:\n" + body,
21802199
body.contains("__wurst_ensure"));
@@ -2184,6 +2203,114 @@ public void optimizedUnitSpatialIndexInnerLoopUsesRawLuaOperations() {
21842203
body.contains("__wurst_intDiv("));
21852204
}
21862205

2206+
@Test
2207+
public void hotLoopStorageTablesAreLocalizedOnLua() {
2208+
String compiled = compileOptimizedLua(
2209+
"hotLoopStorageTablesAreLocalizedOnLua",
2210+
"package Test",
2211+
"int array values",
2212+
"class Counter",
2213+
" int total",
2214+
"@noinline function hotLoop(Counter counter, int limit)",
2215+
" var i = 0",
2216+
" while i < limit",
2217+
" values[i] = values[i] + counter.total",
2218+
" counter.total = values[i + 1] + counter.total",
2219+
" i++",
2220+
"@noinline function coldPath(Counter counter) returns int",
2221+
" return values[0] + counter.total",
2222+
"init",
2223+
" let counter = new Counter()",
2224+
" hotLoop(counter, 16)",
2225+
" coldPath(counter)"
2226+
);
2227+
2228+
String hotBody = topLevelFunctionBodyWithPrefix(compiled, "hotLoop");
2229+
java.util.regex.Matcher arrayAlias = java.util.regex.Pattern
2230+
.compile("local (\\w+) = Test_values").matcher(hotBody);
2231+
assertTrue("the loop's generated array table must be cached in a local:\n" + hotBody,
2232+
arrayAlias.find());
2233+
String arrayAliasName = arrayAlias.group(1);
2234+
assertTrue("the localized array must be used in the loop:\n" + hotBody,
2235+
hotBody.contains(arrayAliasName + "["));
2236+
assertFalse("loop indexes must not keep resolving the array through a global:\n" + hotBody,
2237+
hotBody.contains("Test_values["));
2238+
2239+
java.util.regex.Matcher fieldAlias = java.util.regex.Pattern
2240+
.compile("local (\\w+) = Counter_total_storage").matcher(hotBody);
2241+
assertTrue("the loop's generated field-storage table must be cached in a local:\n" + hotBody,
2242+
fieldAlias.find());
2243+
String fieldAliasName = fieldAlias.group(1);
2244+
assertTrue("the localized field storage must be used in the loop:\n" + hotBody,
2245+
hotBody.contains(fieldAliasName + "["));
2246+
assertFalse("loop field accesses must not keep resolving storage through a global:\n" + hotBody,
2247+
hotBody.contains("Counter_total_storage["));
2248+
2249+
String coldBody = topLevelFunctionBodyWithPrefix(compiled, "coldPath");
2250+
assertFalse("cold array access must not consume a local alias:\n" + coldBody,
2251+
coldBody.contains("local "));
2252+
assertTrue("cold array access must keep the generated global table:\n" + coldBody,
2253+
coldBody.contains("Test_values["));
2254+
assertTrue("cold field access must keep the generated global storage:\n" + coldBody,
2255+
coldBody.contains("Counter_total_storage["));
2256+
}
2257+
2258+
@Test
2259+
public void localizedHotStorageTablesPreserveLuaBehavior() throws IOException {
2260+
test().testLua(true).executeProg().lines(
2261+
"package Test",
2262+
"native testSuccess()",
2263+
"int array values",
2264+
"class Counter",
2265+
" int total",
2266+
"function hotLoop(Counter counter, int limit)",
2267+
" var i = 0",
2268+
" while i < limit",
2269+
" values[i] = values[i] + counter.total",
2270+
" counter.total = values[i + 1] + counter.total",
2271+
" i++",
2272+
"init",
2273+
" values[0] = 1",
2274+
" let counter = new Counter()",
2275+
" counter.total = 2",
2276+
" hotLoop(counter, 3)",
2277+
" if values[0] == 3 and values[1] == 2 and counter.total == 2",
2278+
" testSuccess()"
2279+
);
2280+
}
2281+
2282+
@Test
2283+
public void hotStorageLocalizationRespectsLuaRegisterHeadroom() {
2284+
List<String> lines = new ArrayList<>();
2285+
lines.add("package Test");
2286+
lines.add("native consume(int value)");
2287+
lines.add("int array values");
2288+
lines.add("@noinline function crowded(int limit)");
2289+
lines.add(" var sum = 0");
2290+
for (int i = 0; i < 187; i++) {
2291+
lines.add(" let v" + i + " = " + i);
2292+
lines.add(" sum += v" + i);
2293+
}
2294+
lines.add(" var i = 0");
2295+
lines.add(" while i < limit");
2296+
lines.add(" sum += values[i]");
2297+
lines.add(" i++");
2298+
lines.add(" consume(sum)");
2299+
lines.add("init");
2300+
lines.add(" crowded(1)");
2301+
2302+
String compiled = compileLuaWithRunArgs(
2303+
"hotStorageLocalizationRespectsLuaRegisterHeadroom",
2304+
new RunArgs().with("-lua"), lines.toArray(new String[0]));
2305+
String body = topLevelFunctionBodyWithPrefix(compiled, "crowded");
2306+
assertFalse("localization must retain headroom below Lua's hard local limit:\n" + body,
2307+
body.contains("= Test_values"));
2308+
assertTrue("a skipped alias must leave the generated array access intact:\n" + body,
2309+
body.contains("Test_values[i]"));
2310+
assertFalse("the optimization must not force the locals-table fallback:\n" + body,
2311+
body.contains("__wurst_locals"));
2312+
}
2313+
21872314
/**
21882315
* On Lua a vararg function used to keep its {@code ...} parameter and pack it into a table on
21892316
* every call, and the inliner refused it. With a static argument count at the call site the

0 commit comments

Comments
 (0)