Repository navigation
Find string keys that a script built at run time inline - #51
Merged
Merged
Conversation
SETFIELDK and SETTABLEK/SETINDEXK repeated SETFIELD and SETTABLE/SETINDEX with the constant for the register. One case each, choosing the value source by opcode as the arithmetic cases do, leaves the loop's spill counts as they were and makes it about 150 instructions smaller.
A key that a script builds at run time, by capture, concatenation or sub, never shared a constant's pointer, so every read or write of it at a constant site missed the loop's hint, went megamorphic and exited to the driver, and a lookup through such a key broke out of GETTABLE's probe on the first equal-length key. A table parsed from input and then read by field name ran 3 to 4 times slower than one written by constants. A new string key now takes the pointer of the constant that names it, if a loaded script has one, as Table.Set has given Go keys: the registry lookup moves from Set and the map conversion into insertNew, where the VM's inserts pass too, behind a first-byte check that skips keys no name can match. Constant sites then hit their hints, and keys_dyn (a table from gmatch, read by field) goes from 45 ms to 14 ms, C Lua's 13. GETTABLE's inline probe compares short keys by their bytes instead of giving up on an equal-length key behind another pointer; the EQ case shares the loop. A byte loop is the only form the inliner takes into fast. The gate's baseline for GETTABLE's string path falls, and GETFIELD's megamorphic hit path takes one more load on amd64-v1. A lookup through a captured key goes from 50 ms to 25 ms. newindex handles a string key with a value, the common store, with one hash and probe: a key the table has takes the value, an absent one asks the metatable for __newindex and is then inserted when no resize is due, through tryInsert, which shares the load factor with set and constSet; a resize goes through tableSet, which charges it. Writes through a captured key go from 15 ms to 12 ms, and a new field of an object with a metatable costs one probe fewer than before. The same compare in the GETFIELD, SETFIELD and SETTABLE cases of the loop spills the dispatch head on amd64, so those stay as they are. Inserting a new name costs the registry lookup, about 10 ns, which shows as 6 percent on a script that builds a table of four names per line and drops it, and 3 percent on json; parse, which builds and reads back, is unchanged.
ebarkie
force-pushed
the
string-keys-inline
branch
from
October 20, 2026 19:39
c0e8af2 to
00115f6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A key that a script builds at run time, by capture, concatenation or
sub, never shared aconstant's pointer, so every read or write of it at a constant site missed the loop's hint,
went megamorphic and exited to the driver, and a lookup through such a key broke out of
GETTABLE's probe on the first equal-length key. A table parsed from input and then read by
field name ran 3 to 4 times slower than one written by constants.
Two commits:
Merge the K-variant store cases of the loop into their register cases. SETFIELDK and
SETTABLEK/SETINDEXK repeated SETFIELD and SETTABLE/SETINDEX with the constant for the
register. One case each, choosing the value source by opcode as the arithmetic cases do,
leaves the loop's spill counts as they were and makes it about 150 instructions smaller,
which keeps the next commit under the inliner's big-function threshold.
Find string keys that a script built at run time inline.
one, as
Table.Sethas given Go keys: the registry lookup moves fromSetand the mapconversion into the insert path of
set, where the VM's inserts pass too, behind afirst-byte check that skips keys no name can match. Constant sites then hit their hints.
equal-length key behind another pointer; the EQ case shares the loop. A byte loop is the
only form the inliner takes into
fast. The gate's baseline for GETTABLE's string pathfalls (8 to 2 on amd64-v1, 12 to 6 on v3), and GETFIELD's megamorphic hit path takes one
more load on amd64-v1 (6 to 7); the dispatch head and the hint paths are unchanged.
quickSet(from Sweep: constructor sizing, hash-part keys, iterators, concat and resume #50) gives a new string key its canonical pointer too, unless the key isa constant, which has it already.
newindexhandles a string key with a value, the common store, with one hash and probe:a key the table has takes the value, an absent one asks the metatable for
__newindexand is then inserted when no resize is due, through
tryInsert, which shares the loadfactor with
setandconstSet; a resize goes throughtableSet, which charges it. Anew field of an object with a metatable costs one probe fewer than before.
dispatch head on amd64 even in its smallest form, so those stay as they are.
Times on an Apple M-series machine against upstream main at 80f93c7, two rounds each, best of
five per run, 1M iterations each:
Inserting a new name costs the registry lookup, about 10 ns, which shows as 6 to 9 percent on
the build-and-drop pattern and 3 percent on the suite's json script; parse, which builds and
reads the fields back, is unchanged, and the other suite scripts are unchanged within noise.
The published suite numbers do not move: none of its scripts is dominated by run-time keys.
Tests: new tests cover canonical pointers for script-built keys, dynamic-key reads and writes
on both loops with same-length keys, keys over 16 bytes, deleted keys with
__index,__newindex, metamethod cache clearing through a dynamic__indexkey, reserved names andfrozen tables. vet, tests, race, checkptr, stack stress, asmgate and the generated-loop check
pass locally; fuzzing was not run.
TestDifferentialagainst a C Lua built on macOS showsthree pre-existing differences (
os.timefor years before 1,%Aof denormals) that mainshows too; they come from macOS libc.