From fb1f4c0c668316989817c3a54389b1ec12c7d483 Mon Sep 17 00:00:00 2001 From: folbrich Date: Tue, 20 Oct 2026 14:57:23 +0200 Subject: [PATCH] Add a field to a table without a metatable in the SETFIELD slow path directly A SETFIELD that adds a key, as every field of a table just built, missed the loop's hint and went through newindex, which looked the key up twice, hashed it at run time and measured the table before and after. constSet probes with the constant's compile-time hash and inserts when no resize is needed; other cases still go to newindex. The loop itself is unchanged: an inline insert there pushes fast past the inliner's big-function threshold. --- ops.go | 37 +++++++++++++++++++++++++++++++++++++ table.go | 6 +++--- testdata/meta.expected | 1 + testdata/meta.lua | 30 ++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/ops.go b/ops.go index 9e786d5..bae7571 100644 --- a/ops.go +++ b/ops.go @@ -132,12 +132,18 @@ func (ex *executor) slowOp() error { case opSetIndex: return ex.newindex(op, *ex.reg(a), Int(int64(b)), *ex.reg(c)) case opSetField: + if ex.constSet(*ex.reg(a), b, *ex.reg(c)) { + return nil + } return ex.newindex(op, *ex.reg(a), p.k[b], *ex.reg(c)) case opSetTableK: return ex.newindex(op, *ex.reg(a), *ex.reg(b), p.k[c]) case opSetIndexK: return ex.newindex(op, *ex.reg(a), Int(int64(b)), p.k[c]) case opSetFieldK: + if ex.constSet(*ex.reg(a), b, p.k[c]) { + return nil + } return ex.newindex(op, *ex.reg(a), p.k[b], p.k[c]) case opNewTable: // the sizes are hints: clamp them, so that no instruction can ask for a huge allocation @@ -1403,6 +1409,37 @@ func (ex *executor) builtinIter(base, c int) (bool, error) { return true, ex.tick() // a call ticks } +// constSet does obj[K[kc]] = val for a SETFIELD the loop could not do inline, such as one +// that adds a field to a table just built, when the table has no metatable and the write +// needs no resize: it finds the key with the hash computed at compile time, sets it or adds +// it, and records where for the site. It reports false when newindex must do the write. +func (ex *executor) constSet(obj Value, kc int, val Value) bool { + if !obj.isObj(kindTable) || val.IsNil() { + return false + } + t := (*Table)(obj.p) + if t.meta != nil || t.flags&(tabFrozen|tabGuarded) != 0 || len(t.nodes) == 0 { + return false + } + p := ex.st.cl.p + key, h := p.k[kc], p.khash[kc] + if !key.isString() || key.n&lenMask >= 2 && *(*uint16)(key.p) == 0x5f5f { + return false // a key starting with "__" clears the table's caches in set + } + idx := t.findNode(&key, h) + if idx >= 0 { + t.nodes[idx].v = val + } else { + if (t.used+1)*4 > len(t.nodes)*3 || t.memSize() >= maxAlloc/2 { + return false // a resize, which newindex charges for + } + idx = t.insert(key, val, h) + } + learn(&p.hints[ex.aux()], idx) + ex.st.pc++ // the aux word + return true +} + // constField does R[dst] = obj[K[kc]] for GETFIELD and SELF, when obj is a table and the // key is in it or one cached __index hop away, or obj is a string and the key is in the // string library, with the key's compile-time hash. It learns the site's hint and reports diff --git a/table.go b/table.go index 4d8716b..9f4b78e 100644 --- a/table.go +++ b/table.go @@ -380,15 +380,15 @@ func (t *Table) growArray(n int) { } } -// insert places a new key; the caller has made room. -func (t *Table) insert(key, val Value, h uint64) { +// insert places a new key and returns its node index; the caller has made room. +func (t *Table) insert(key, val Value, h uint64) int { mask := uint64(len(t.nodes) - 1) for j := h; ; j++ { nd := &t.nodes[j&mask] if nd.k.p == nil { nd.k, nd.v = key, val t.used++ - return + return int(j & mask) } } } diff --git a/testdata/meta.expected b/testdata/meta.expected index eb749f9..605c67e 100644 --- a/testdata/meta.expected +++ b/testdata/meta.expected @@ -14,3 +14,4 @@ a tail call through __call stays a tail call str:646f6e65 | true | 2 | 3 | str:6 metamethods that are not functions str:6d6574616d6574686f6473207468617420617265206e6f742066756e6374696f6e733a343a20617474656d707420746f2063616c6c206120737472696e672076616c756520286d6574616d6574686f6420276164642729 | true | str:6d6574616d6574686f6473207468617420617265206e6f742066756e6374696f6e733a363a20617474656d707420746f2063616c6c206120626f6f6c65616e2076616c756520286d6574616d6574686f6420276c656e2729 | str:6d6574616d6574686f6473207468617420617265206e6f742066756e6374696f6e733a373a20617474656d707420746f2063616c6c2061206e756d6265722076616c756520286d6574616d6574686f642027636f6e6361742729 | str:63616c6c6564 | str:6d6574616d6574686f6473207468617420617265206e6f742066756e6374696f6e733a31303a20617474656d707420746f2063616c6c2061207461626c652076616c756520286d6574616d6574686f64202765712729 | str:6d6574616d6574686f6473207468617420617265206e6f742066756e6374696f6e733a31323a20617474656d707420746f2063616c6c2061206e756d6265722076616c756520286d6574616d6574686f642027636c6f73652729 | str:6d6574616d6574686f6473207468617420617265206e6f742066756e6374696f6e733a31343a20617474656d707420746f2063616c6c2061206e696c2076616c756520286d6574616d6574686f642027636c6f73652729 | str:617474656d707420746f2063616c6c2061206e756d6265722076616c7565 | str:6e696c | str:7461626c65 __index and __newindex chains that loop false | str:5f5f696e64657820616e64205f5f6e6577696e64657820636861696e732074686174206c6f6f703a363a20275f5f696e6465782720636861696e20746f6f206c6f6e673b20706f737369626c65206c6f6f70 | false | str:5f5f696e64657820616e64205f5f6e6577696e64657820636861696e732074686174206c6f6f703a373a20275f5f6e6577696e6465782720636861696e20746f6f206c6f6e673b20706f737369626c65206c6f6f70 | false | str:275f5f696e6465782720636861696e20746f6f206c6f6e673b20706f737369626c65206c6f6f70 | false | str:275f5f6e6577696e6465782720636861696e20746f6f206c6f6e673b20706f737369626c65206c6f6f70 index errors raised by the library, called from Lua str:275f5f696e6465782720636861696e20746f6f206c6f6e673b20706f737369626c65206c6f6f70 | str:275f5f6e6577696e6465782720636861696e20746f6f206c6f6e673b20706f737369626c65206c6f6f70 | str:617474656d707420746f20696e6465782061206e756d6265722076616c7565 | str:617474656d707420746f20696e6465782061206e756d6265722076616c7565 +fields added to tables just built, with and without __newindex 270 | 11 | 22 | 33 | 44 | 55 | 66 | 6 | str:7121 | 43 diff --git a/testdata/meta.lua b/testdata/meta.lua index 2d0402b..ab75df9 100644 --- a/testdata/meta.lua +++ b/testdata/meta.lua @@ -131,3 +131,33 @@ return err(function() return table.unpack(setmetatable({}, L), 1, 1) end), err(function() table.insert(setmetatable({}, L), 1) end), err(function() for _ in ipairs(5) do end end), err(function() table.insert(setmetatable({}, {__newindex = 5}), 1) end) +--== fields added to tables just built, with and without __newindex +local function new(x, y) return {x = x, y = y} end +local r, sum = {}, 0 +for i = 1, 20 do + local p = new(i, 2 * i) + p.z = i; p.w = -i -- adds, the second one past the size the constructor asked for + p.y = nil; p.y = 3 -- sets a deleted key again + sum = sum + p.x + p.y + p.z + p.w +end +r[#r + 1] = sum +local logged = {} +local mt = {__newindex = function(t, k, v) logged[#logged + 1] = k; rawset(t, k, v * 10) end} +for i = 1, 6 do + local p = setmetatable(new(i, i), mt) + p.x = i -- present: no __newindex + p.z = i -- absent: __newindex + r[#r + 1] = p.x + p.z +end +r[#r + 1] = #logged +local m = new(1, 2) +local o = setmetatable({}, m) +m.__index = function(_, k) return k .. "!" end -- a "__" key added to a table in use as a metatable +r[#r + 1] = o.q +local n = 0 +local t = new(1, 2) +for i = 1, 40 do t["k" .. i] = i end +t.v = 0 +for _ in pairs(t) do n = n + 1 end +r[#r + 1] = n +return table.unpack(r)