diff --git a/changes/fixed/55-sparse-integer-admission.md b/changes/fixed/55-sparse-integer-admission.md new file mode 100644 index 0000000..f818157 --- /dev/null +++ b/changes/fixed/55-sparse-integer-admission.md @@ -0,0 +1 @@ +- Compact numeric table storage now admits half-dense integer keys regardless of their offset or insertion direction (#55). diff --git a/corpus/D.manifest b/corpus/D.manifest index 57ab1e7..bf117d5 100644 --- a/corpus/D.manifest +++ b/corpus/D.manifest @@ -155,4 +155,5 @@ operators: not u- # u~ + - * / // % ^ .. == ~= < <= > >= and or & | ~ << >> co_wrap_close_yieldable.lua unspecified-audit: ok tbl_table_value_type_matrix.lua unspecified-audit: ok tbl_sparse_geometric_keys.lua unspecified-audit: ok +tbl_sparse_offset_descending.lua unspecified-audit: ok tbl_set_churn_bounded.lua unspecified-audit: ok diff --git a/corpus/D/tbl_sparse_offset_descending.lua b/corpus/D/tbl_sparse_offset_descending.lua new file mode 100644 index 0000000..9e950db --- /dev/null +++ b/corpus/D/tbl_sparse_offset_descending.lua @@ -0,0 +1,38 @@ +-- surface: half-dense integer keys with an offset and descending insertion +local function exercise(t, first, last) + assert(#t == 0) + local count, sum = 0, 0 + for k, v in pairs(t) do + assert(k >= first and k <= last and k % 2 == 1) + count, sum = count + 1, sum + v + end + assert(count == 8 and sum == 36) + + local k = next(t) + assert(k ~= nil) + local old = t[k] + t[k] = nil + assert(t[k] == nil) + t[k] = old + assert(t[k] == old) + assert(next(t) ~= nil) +end + +local offset = {} +for i = 1, 8 do offset[1001 + 2 * i] = i end +exercise(offset, 1003, 1017) + +local descending = {} +for i = 8, 1, -1 do descending[2000001 + 2 * i] = i end +exercise(descending, 2000003, 2000017) + +-- Exercise both sides of the compact/hash admission boundary: the isolated +-- key stays hashed, while the subsequent half-dense run is compact. +local boundary = {} +for i = 1, 8 do boundary[3001 + 2 * i] = i end +boundary[9000001] = 9 +assert(boundary[9000001] == 9 and boundary[3003] == 1 and boundary[3017] == 8) +boundary[9000001] = nil +boundary[9000001] = 10 +assert(boundary[9000001] == 10) +print("sparse-shapes-ok") diff --git a/src/runtime.eigs b/src/runtime.eigs index a56a318..34d28fc 100644 --- a/src/runtime.eigs +++ b/src/runtime.eigs @@ -8,7 +8,8 @@ # a: the array part, keys 1..len(a); holes are null, the last element is # never null, and key len(a)+1 is never in o, so len(a) is a border; # h: lazy string dictionary; o: lazy [encoded-key map, keys, values, -# numeric slots, free identity slots, live positive-integer hash keys]. +# numeric slots, free identity slots, live positive-integer hash keys, +# first positive-integer hash key]. # A Lua function takes one list of arguments and returns a list of results. # Errors are thrown as {"LUAERR": 1, "v": value}. The call-site stack lr_stk holds # the site id of every active call (0 = called from the runtime, "C"); the @@ -852,13 +853,19 @@ define lr_rawset(t, k, v, site) as: # very large isolated integers still use the identity map below. local nek is lr_okey of (k) # Existing numeric slots remain addressable after density changes. - # For a new slot, k <= 2*(live integer hash keys + 1), so after the - # insertion padding is at most twice the live integer-key count. - # Grown identity capacity and non-integral numeric keys never count. + # Admit around the first integer rather than around zero. Thus the + # same density rule applies to offset and descending insertion, while + # isolated integers still stay in the identity map. Numeric slots + # remain absolute indices, so their padding is bounded by the fixed + # offset plus twice the live integer-key count. local numeric_old is null if t[4] != null and k == floor of k and k >= 1 and k <= len of t[4][3]: numeric_old is t[4][3][k - 1] - if numeric_old != null or (v != null and t[5] == null and k == floor of k and k > 1000 and t[4] != null and not (has_key of [t[4][0], nek]) and k <= 2 * (t[4][5] + 1)): + local numeric_admit is false + if v != null and t[5] == null and k == floor of k and k > 1000 and t[4] != null and t[4][6] != null and not (has_key of [t[4][0], nek]): + local radius is 2 * (t[4][5] + 1) + numeric_admit is k >= t[4][6] - radius and k <= t[4][6] + radius + if numeric_old != null or numeric_admit: local old is null if k <= len of t[4][3]: old is t[4][3][k - 1] @@ -890,7 +897,7 @@ define lr_rawset(t, k, v, site) as: t[5][3] is t[5][3] - 1 else: if t[4] == null: - t[4] is [{}, [], [], [], [], 0] + t[4] is [{}, [], [], [], [], 0, null] local slot is t[4][0][ek] if slot == null: lr_iter_insert of [t, k] @@ -906,6 +913,8 @@ define lr_rawset(t, k, v, site) as: t[4][2][slot] is v if type of k == "num" and k == floor of k and k >= 1: t[4][5] is t[4][5] + 1 + if t[4][6] == null: + t[4][6] is k else: t[4][2][slot] is v return null diff --git a/tools/measure/run-table-memory.sh b/tools/measure/run-table-memory.sh index 610e1fa..5074a9c 100755 --- a/tools/measure/run-table-memory.sh +++ b/tools/measure/run-table-memory.sh @@ -26,7 +26,7 @@ for n in 0 10000 100000 1000000; do measure lua empty "$n" "$REF" "$ROOT/tools/measure/table-memory.lua" measure eigenlua empty "$n" env EIGENLUA_ARGV0=eigenlua MEASURE_MODE=empty MEASURE_N="$n" "$EIGS" "$OUT/table-memory.eigs" done -for mode in array strings sparse; do +for mode in array strings sparse sparse_offset sparse_descending; do measure lua "$mode" 1000000 "$REF" "$ROOT/tools/measure/table-memory.lua" measure eigenlua "$mode" 1000000 env EIGENLUA_ARGV0=eigenlua MEASURE_MODE="$mode" MEASURE_N=1000000 "$EIGS" "$OUT/table-memory.eigs" done diff --git a/tools/measure/table-memory.lua b/tools/measure/table-memory.lua index cc9fec9..1404773 100644 --- a/tools/measure/table-memory.lua +++ b/tools/measure/table-memory.lua @@ -1,6 +1,7 @@ -- EigenLua #31 table-memory workload. Keep every allocation live until exit. -- Usage: lua table-memory.lua MODE N --- MODE: empty, array, strings, sparse, small4, or baseline. +-- MODE: empty, array, strings, sparse, sparse_offset, sparse_descending, +-- small4, or baseline. -- EigenLua currently does not forward host argv to a compiled chunk, so the -- environment fallback makes the identical source runnable in both arms. local mode = assert(arg[1] or os.getenv("MEASURE_MODE")) @@ -16,6 +17,10 @@ elseif mode == "strings" then elseif mode == "sparse" then -- Stay outside the array part in both implementations. for i = 1, n do root[2 * i] = i end +elseif mode == "sparse_offset" then + for i = 1, n do root[1001 + 2 * i] = i end +elseif mode == "sparse_descending" then + for i = n, 1, -1 do root[2 * i] = i end elseif mode == "small4" then for i = 1, n do root[i] = { tag = i, first = i + 1, second = i + 2, value = i + 3 }