Skip to content

Commit de23cc3

Browse files
committed
Record the two Lua emission costs and what would remove them
Both found by reading the emitted script rather than by profiling, and both entries say to measure first: a call count is not what the game's interpreter charges. Every primitive array read becomes an ensureInt/ensureBool/ensureReal call, and it survives the optimiser, so slotFor makes two Lua calls per probe step where it wants two table indexes. The shim is needed - an untouched key is nil where Jass reads 0 or false - but the default belongs on the table rather than at every read site. A metatable whose __index returns the default makes a present key a raw index with no call, and runs only on a miss, which is the rare case; three shared metatables cover the primitive types. A method with an override stops being a direct call and becomes an instance miss plus an __index hop. The tables are flat, so it is one hop rather than a walk, but adding an override anywhere silently converts every call site of that slot. Class hierarchy analysis at the call site fixes it where a slot has one reachable implementation, and getSubMethods already holds what that needs.
1 parent d26c0dc commit de23cc3

1 file changed

Lines changed: 46 additions & 0 deletions

File tree

BACKLOG.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,52 @@ itself, and one gap in what the suite can see.
107107
`FASTHASHMAP_CAPACITY`, `isFull()` and `Hashable.hash` to the public API when the Lua path makes
108108
all three meaningless on that target.
109109

110+
28. **Two costs the Lua emission pays which look avoidable.** Found by reading the emitted script
111+
for `FastHashMapTests_fastHashMapRuntimeLua`, not by profiling. **Measure both before touching
112+
either**; each proposal below is a structural argument and a call count, which is not the same as
113+
knowing what the game's interpreter charges for it.
114+
115+
*Every primitive array read is a function call.* `LuaNativeLowering.lowerPrimitiveArrayEnsure`
116+
wraps each non-lvalue primitive array access in `ensureInt`/`ensureBool`/`ensureReal`, and the
117+
wrapper survives the optimiser into the emitted script:
118+
119+
if not(__wurst_ensureBool(FastHashMap_used[s])) then
120+
return ((s2 >= this5.FastHashMap_base) and __wurst_ensureBool(FastHashMap_used[s2]))
121+
122+
Writes are raw, lvalues being skipped, so only reads pay. `slotFor` reads `used` and `dead` every
123+
probe step, which is two Lua calls per step in the hottest loop the container has. The shim itself
124+
is needed: an untouched table key is `nil` where Jass reads `0` or `false`, and class and handle
125+
typed arrays already skip it because `nil` is the right default for them.
126+
127+
The fix is to move the default off the access site and onto the table, which is the ordinary Lua
128+
idiom for exactly this:
129+
130+
__wurst_default_false = ({__index = function() return false end})
131+
setmetatable(FastHashMap_used, __wurst_default_false)
132+
133+
A present key is then a raw table index with no call at all, and the function runs only on a miss
134+
- which is the rare case, a read of a slot never written. One metatable per primitive array global
135+
replaces a wrapper at every read site. Three shared metatables cover int, bool and real. `pairs`
136+
is unaffected by `__index`, so nothing which iterates an array changes. What has to be checked:
137+
where the `setmetatable` calls are emitted (globals init, before any read), that a `0` or `false`
138+
actually stored still reads back rather than falling through, and whether anything relies on the
139+
stacktrace argument `callWithStacktrace` adds to the current wrapper.
140+
141+
*A method with an override stops being a direct call.* Without one, a bounded generic's method
142+
lowers to a direct call - `Box_render_specialized(Box_new_Box(), 42)`. Add a subclass which
143+
overrides it and the same call becomes `b:Box_size_specialized_integer(1)`, an instance miss
144+
followed by an `__index` hop to the class table. The tables are flat rather than chained -
145+
`SubBox.Box_size_specialized_integer` is assigned on `SubBox` itself, so it is one hop and not a
146+
walk up the hierarchy - so the cost is that hop plus a dynamic lookup against a global function
147+
call, and adding an override anywhere silently converts every call site of that slot.
148+
149+
The fix is class hierarchy analysis at the call site: if the receiver's static type has exactly one
150+
reachable implementation of the slot, emit the direct call. `ImMethod.getSubMethods()` already
151+
holds what that question needs, and `RemoveGarbage` already establishes reachability, so this is
152+
reading data which exists rather than computing anything new. It also composes with the erasure
153+
model in item 23: fewer virtual slots means fewer of the naming and binding problems items 15 and
154+
26 came from.
155+
110156
22. **The library's own tests do not run on Lua.** They run on the interpreter now — all 460 of
111157
them, collected by importing every package in the checkout whose name ends in `Tests`. That
112158
half is done; this is the other one.

0 commit comments

Comments
 (0)