Skip to content

Ne9roni/lua-update-metamethod

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5,817 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

__update Metamethod for Lua 5.5

A patch for Lua 5.5.0 that adds the __update metamethod — a complement to __newindex that intercepts assignments to existing table fields.

Motivation

Lua's __newindex metamethod only fires when assigning to a key that does not exist in the table. There is no built-in mechanism to intercept updates to existing keys. This forces developers to use proxy-table patterns (an empty proxy + __newindex + __index forwarding), which are complex, error-prone, and have significant performance overhead.

The __update metamethod fills this gap cleanly:

  • Reactive data binding: detect when a property changes (e.g., UI frameworks)
  • ORM dirty tracking: know which fields were modified before a database flush
  • Configuration change monitoring: log or validate config changes at runtime
  • Debugging / instrumentation: trace all writes to a table without proxies

Usage

local t = setmetatable({}, {
    __update = function(t, k, newval)
        print("update:", k, t[k], "->", newval)
        rawset(t, k, newval)
    end
})

t.x = 1        -- update: x  nil  ->  1   (new key, non-nil)
t.x = 2        -- update: x  1    ->  2   (existing key, different value)
t.x = 2        -- (no output: same value, not triggered)

Trigger Rules

Scenario Behavior
Existing key, different value __update fires
Existing key, same value No trigger, direct write
New key, non-nil value (no __newindex) __update fires
New key, with __newindex __newindex fires (__newindex takes priority)
New key, nil value No trigger
__update is not a function Falls back to normal write

Important: Setup Order

Like __gc, __update must be present in the metatable before calling setmetatable. Adding it afterward has no effect:

-- Correct:
local mt = { __update = function(t, k, v) rawset(t, k, v) end }
local t = setmetatable({}, mt)

-- Wrong (will NOT work):
local mt = {}
local t = setmetatable({}, mt)
mt.__update = function(t, k, v) rawset(t, k, v) end  -- too late

Design Principles

  1. Zero overhead: Tables without __update have no measurable performance impact. A single bit flag (hasupdatemt) in the table's flags field enables fast-path bypass.
  2. Clear semantics: Existing keys → __update. New keys → __newindex. No ambiguity.
  3. Minimal invasion: Only the assignment path is modified. No changes to reads, GC, or other subsystems.

Files Modified

File Change
lobject.h BITHASUPDATE flag macros (bit 7 of flags)
ltm.h TM_UPDATE enum, getupdatetm macro, gfasttm assertion
ltm.c "__update" string, luaT_gettm special handling
lvm.h luaV_fastset/luaV_fastseti fast-path interception
lvm.c luaV_finishupdate + core logic in luaV_finishset
lapi.c lua_setmetatable flag caching

Total: ~95 lines added, ~6 lines modified.

Build & Test

# Build
make SYSCFLAGS="-DLUA_USE_LINUX" SYSLIBS="-Wl,-E -ldl"

# Run regression tests (60 tests, no external dependencies)
./lua update_test/update_spec.lua

# Run benchmark
./lua update_test/benchmark_update_same_vs_diff.lua

# Run full comparison: Vanilla Lua 5.5 vs Patched (requires ~/lua-5.5.0/bin/lua)
bash update_test/benchmark_compare.sh [rounds]

Benchmark Results

All benchmarks: Vanilla Lua 5.5.0 vs Patched Lua 5.5 (with __update), 5 rounds averaged.

Overhead (tables without __update)

Tables without __update show negligible overhead:

baseline_same — Plain table, same-value writes:

Table Size Vanilla Lua 5.5.0 Patched Diff
10,000 0.033s ± 0.002s 0.033s ± 0.002s -0.4%
100,000 0.326s ± 0.004s 0.324s ± 0.003s -0.5%

baseline_diff — Plain table, different-value writes:

Table Size Vanilla Lua 5.5.0 Patched Diff
10,000 0.043s ± 0.000s 0.046s ± 0.001s +6.9%
100,000 0.435s ± 0.006s 0.456s ± 0.008s +4.7%

empty_mt — Table with empty metatable:

Table Size Vanilla Lua 5.5.0 Patched Diff
10,000 0.033s ± 0.000s 0.034s ± 0.004s +5.7%
100,000 0.330s ± 0.007s 0.321s ± 0.008s -2.9%

baseline/empty_mt confirm near-zero overhead for tables without __update (within noise).

Tables with __update (integer keys)

update_same — Same-value writes (skipped by fast-path value comparison):

Table Size Vanilla Lua 5.5.0 Patched Diff
10,000 0.036s ± 0.003s 0.043s ± 0.003s +18.2%
100,000 0.331s ± 0.006s 0.411s ± 0.006s +23.9%

update_diff — Different-value writes (callback fires every time):

Table Size Vanilla Lua 5.5.0 Patched Diff
10,000 0.044s ± 0.000s 0.379s ± 0.005s +771.5%
100,000 0.448s ± 0.019s 3.889s ± 0.025s +767.9%

Tables with __update (string keys)

str_update_same — Same-value writes:

Table Size Vanilla Lua 5.5.0 Patched Diff
10,000 0.110s ± 0.003s 0.125s ± 0.005s +13.4%
100,000 1.521s ± 0.008s 1.734s ± 0.083s +14.0%

str_update_diff — Different-value writes:

Table Size Vanilla Lua 5.5.0 Patched Diff
10,000 0.124s ± 0.002s 0.461s ± 0.008s +270.7%
100,000 1.668s ± 0.023s 5.321s ± 0.135s +219.0%

Same-value overhead is low (+14~24%) thanks to the fast-path value comparison in ltable.c. Different-value overhead is expected — vanilla Lua does plain writes while patched invokes the callback.

Proxy Table vs Native __update

The key comparison: traditional proxy-table pattern vs native __update (both on patched Lua 5.5):

Scenario Description 10K 100K
diff_value Update with different values Proxy 0.431s vs Native 0.385s (-10.7%) Proxy 4.283s vs Native 3.886s (-9.3%)
same_value Write same values Proxy 0.192s vs Native 0.041s (-78.6%) Proxy 1.956s vs Native 0.411s (-79.0%)
read Read existing keys Proxy 0.114s vs Native 0.039s (-65.8%) Proxy 1.214s vs Native 0.397s (-67.3%)
pairs Iterate with pairs() Proxy 0.128s vs Native 0.126s (-1.6%) Proxy 1.273s vs Native 1.277s (+0.3%)

Key takeaways:

  • Same-value writes: Native __update is ~79% faster — fast-path value comparison skips the write entirely, while proxy still invokes __newindex every time.
  • Different-value writes: Native __update is ~10% faster than proxy.
  • Reads: Native __update is ~66% faster — proxy requires __index metamethod on every read, while __update stores data directly in the table.
  • Iteration: No meaningful difference — both approaches ultimately call next on the backing data.
  • Code simplicity: Native __update eliminates the need for __index, __pairs, and the backing table entirely.

Run bash update_test/benchmark_compare.sh [ROUNDS] to reproduce.

Test environment:

  • CPU: 13th Gen Intel Core i7-13700KF (12 cores / 24 threads)
  • Memory: 32 GB
  • OS: Ubuntu 20.04.6 LTS (WSL2, kernel 6.6.87.2-microsoft-standard-WSL2)
  • Compiler: GCC 9.4.0

Based On

This patch is based on Lua 5.5.0 (released 18 Mar 2025). See PROPOSAL.md for detailed design documentation.

License

Same as Lua — MIT License.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages