Skip to content

Commit 07df942

Browse files
committed
Cap table array size growth to 256 item increments
This limits "wasteful" pre-allocation in user-created tables to a reasonable minimum while still allowing reasonable performance when quickly creating a lot of lists.
1 parent 3d26304 commit 07df942

File tree

3 files changed

+223
-9
lines changed

3 files changed

+223
-9
lines changed

VM/src/ltable.cpp

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
#define MAXBITS 26
3939
#define MAXSIZE (1 << MAXBITS)
4040

41+
// ServerLua: alignment for large array sizes (caps wasted slots at this value - 1)
42+
constexpr int kLargeArrayAlign = 256;
43+
4144
static_assert(offsetof(LuaNode, val) == 0, "Unexpected Node memory layout, pointer cast in gval2slot is incorrect");
4245

4346
// TKey is bitpacked for memory efficiency so we need to validate bit counts for worst case
@@ -284,7 +287,7 @@ int luaH_next(lua_State* L, LuaTable* t, StkId key)
284287

285288
#define getaboundary(t) (t->aboundary < 0 ? -t->aboundary : t->sizearray)
286289

287-
static int computesizes(int nums[], int* narray)
290+
static int computesizes(int nums[], int* narray, int max_idx)
288291
{
289292
int i;
290293
int twotoi; // 2^i
@@ -305,6 +308,18 @@ static int computesizes(int nums[], int* narray)
305308
if (a == *narray)
306309
break; // all elements already counted
307310
}
311+
312+
// ServerLua: For large arrays, cap to 256-aligned size if all elements fit.
313+
// This is necessary because otherwise we'll use use the next power of 2
314+
// that fits 50%. For large array-like tables, that's not necessarily what
315+
// you want in a memory-constrained environment.
316+
if (max_idx > 0 && n > kLargeArrayAlign && na > kLargeArrayAlign)
317+
{
318+
int capped = ((na + kLargeArrayAlign - 1) / kLargeArrayAlign) * kLargeArrayAlign;
319+
if (capped < n && max_idx <= capped)
320+
n = capped;
321+
}
322+
308323
*narray = n;
309324
LUAU_ASSERT(*narray / 2 <= na && na <= *narray);
310325
return na;
@@ -322,7 +337,7 @@ static int countint(double key, int* nums)
322337
return 0;
323338
}
324339

325-
static int numusearray(const LuaTable* t, int* nums)
340+
static int numusearray(const LuaTable* t, int* nums, int* max_idx)
326341
{
327342
int lg;
328343
int ttlg; // 2^lg
@@ -342,15 +357,19 @@ static int numusearray(const LuaTable* t, int* nums)
342357
for (; i <= lim; i++)
343358
{
344359
if (!ttisnil(&t->array[i - 1]))
360+
{
345361
lc++;
362+
// ServerLua: track max_idx for size capping
363+
*max_idx = i;
364+
}
346365
}
347366
nums[lg] += lc;
348367
ause += lc;
349368
}
350369
return ause;
351370
}
352371

353-
static int numusehash(const LuaTable* t, int* nums, int* pnasize)
372+
static int numusehash(const LuaTable* t, int* nums, int* pnasize, int* max_idx)
354373
{
355374
int totaluse = 0; // total number of elements
356375
int ause = 0; // summation of `nums'
@@ -361,7 +380,14 @@ static int numusehash(const LuaTable* t, int* nums, int* pnasize)
361380
if (!ttisnil(gval(n)))
362381
{
363382
if (ttisnumber(gkey(n)))
364-
ause += countint(nvalue(gkey(n)), nums);
383+
{
384+
// ServerLua: support non-pow2 sizes for array in rehash()
385+
double key = nvalue(gkey(n));
386+
ause += countint(key, nums);
387+
int k = arrayindex(key);
388+
if (k > *max_idx)
389+
*max_idx = k;
390+
}
365391
totaluse++;
366392
}
367393
}
@@ -511,18 +537,32 @@ static void rehash(lua_State* L, LuaTable* t, const TValue* ek)
511537
{
512538
int nums[MAXBITS + 1]; // nums[i] = number of keys between 2^(i-1) and 2^i
513539
for (int i = 0; i <= MAXBITS; i++)
514-
nums[i] = 0; // reset counts
515-
int nasize = numusearray(t, nums); // count keys in array part
516-
int totaluse = nasize; // all those keys are integer keys
517-
totaluse += numusehash(t, nums, &nasize); // count keys in hash part
540+
nums[i] = 0; // reset counts
541+
int max_idx = -1; // ServerLua: track max array-eligible index
542+
int nasize = numusearray(t, nums, &max_idx); // count keys in array part
543+
int totaluse = nasize; // all those keys are integer keys
544+
totaluse += numusehash(t, nums, &nasize, &max_idx); // count keys in hash part
518545

519546
// count extra key
520547
if (ttisnumber(ek))
548+
{
549+
// ServerLua: we need to keep track of the _max_.
550+
int k = arrayindex(nvalue(ek));
551+
if (k > 0 && k > max_idx)
552+
max_idx = k;
521553
nasize += countint(nvalue(ek), nums);
554+
}
522555
totaluse++;
523556

557+
// ServerLua: Don't try to be conservative about array allocation size,
558+
// this is a system table.
559+
if (t->memcat < 2)
560+
{
561+
max_idx = -1;
562+
}
563+
524564
// compute new size for array part
525-
int na = computesizes(nums, &nasize);
565+
int na = computesizes(nums, &nasize, max_idx);
526566
int nh = totaluse - na;
527567

528568
// enforce the boundary invariant; for performance, only do hash lookups if we must

tests/SLConformance.test.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "../VM/src/mono_strings.h"
2222
#include "../VM/src/lapi.h"
2323
#include "../VM/src/lgc.h"
24+
#include "../VM/src/ltable.h"
2425

2526
#include <fstream>
2627
#include <string>
@@ -158,6 +159,16 @@ static int test_integer_call(lua_State *L)
158159
return lsl_cast(L);
159160
}
160161

162+
// Returns (array_size, hash_size) for a table - for testing table sizing behavior
163+
static int lua_table_sizes(lua_State* L)
164+
{
165+
luaL_checktype(L, 1, LUA_TTABLE);
166+
LuaTable* t = hvalue(luaA_toobject(L, 1));
167+
lua_pushinteger(L, t->sizearray);
168+
lua_pushinteger(L, t->node == &luaH_dummynode ? 0 : sizenode(t));
169+
return 2;
170+
}
171+
161172
// Helper callback that enforces reachability-based memory limits
162173
// Reads max_mem and free_objects from the RuntimeState
163174
static int memoryLimitCallback(lua_State *L, size_t osize, size_t nsize)
@@ -976,4 +987,14 @@ TEST_CASE("Metamethods and library callbacks receive interrupt checks")
976987
});
977988
}
978989

990+
TEST_CASE("Table Sizing")
991+
{
992+
runConformance("table_sizing.lua", nullptr, [](lua_State *L) {
993+
lua_pushcfunction(L, lua_table_sizes, "table_sizes");
994+
lua_setglobal(L, "table_sizes");
995+
lua_pushcfunction(L, [](lua_State *L) {lua_setmemcat(L, luaL_checkinteger(L, 1)); return 0;}, "change_memcat");
996+
lua_setglobal(L, "change_memcat");
997+
});
998+
}
999+
9791000
TEST_SUITE_END();

tests/conformance/table_sizing.lua

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
-- Tests for array sizing cap behavior
2+
-- The cap limits "wasted" array slots to at most 255 for large arrays
3+
4+
-- Build array by insertion to trigger rehash (table.create() pre-allocates, skipping resize)
5+
local function make_array(n)
6+
local t = {}
7+
for i = 1, n do
8+
t[i] = true
9+
end
10+
return t
11+
end
12+
13+
local function check_sizes(t, expected_arr, expected_hash, desc)
14+
local arr_size, hash_size = table_sizes(t)
15+
assert(arr_size == expected_arr, `{desc}: expected {expected_arr} array slots, got {arr_size}`)
16+
assert(hash_size == expected_hash, `{desc}: expected {expected_hash} hash slots, got {hash_size}`)
17+
end
18+
19+
-- Basic cap behavior - 600 elements should get 768, not 1024
20+
local t = make_array(600)
21+
check_sizes(t, 768, 0, "600 elements")
22+
23+
t = make_array(512)
24+
check_sizes(t, 512, 0, "512 threshold")
25+
26+
t = make_array(513)
27+
check_sizes(t, 768, 0, "513 threshold")
28+
29+
t = make_array(1500)
30+
check_sizes(t, 1536, 0, "1500 elements")
31+
32+
t = make_array(2000)
33+
check_sizes(t, 2048, 0, "2000 elements")
34+
35+
-- Boundary invariant - sparse table with boundary at high index
36+
-- Element at 1000 doesn't meet 50% threshold, goes to hash
37+
t = {}
38+
t[1] = true
39+
t[1000] = true
40+
check_sizes(t, 1, 1, "boundary invariant")
41+
42+
-- Mixed array/hash - string keys don't affect array sizing
43+
t = make_array(600)
44+
t["foo"] = "bar"
45+
t["baz"] = "qux"
46+
check_sizes(t, 768, 2, "mixed keys")
47+
48+
-- Small arrays (below threshold) - normal power-of-2 sizing
49+
t = make_array(100)
50+
check_sizes(t, 128, 0, "100 elements")
51+
52+
-- Incremental growth
53+
t = make_array(520)
54+
check_sizes(t, 768, 0, "520 elements")
55+
56+
-- Offset array - elements not starting from 1
57+
-- 600 elements at indices 401-1000, meets 50% threshold for 1024
58+
t = {}
59+
for i = 401, 1000 do
60+
t[i] = true
61+
end
62+
check_sizes(t, 1024, 0, "offset array 401-1000")
63+
64+
-- Sparse with gap - should not cap due to high max_idx
65+
t = make_array(1100)
66+
for i = 1500, 1549 do
67+
t[i] = true
68+
end
69+
check_sizes(t, 2048, 0, "sparse with gap")
70+
71+
-- Sequential growth across cap threshold
72+
t = make_array(400)
73+
check_sizes(t, 512, 0, "pre-growth 400 elements")
74+
for i = 401, 600 do
75+
t[i] = true
76+
end
77+
check_sizes(t, 768, 0, "post-growth 600 elements")
78+
79+
-- Index 0 goes to hash, not array
80+
t = make_array(600)
81+
t[0] = true
82+
check_sizes(t, 768, 1, "with index 0")
83+
84+
-- Negative indices go to hash
85+
t = make_array(600)
86+
t[-1] = true
87+
t[-100] = true
88+
check_sizes(t, 768, 2, "with negative indices")
89+
90+
-- Non-integer keys go to hash
91+
t = make_array(600)
92+
t[1.5] = true
93+
t[2.7] = true
94+
check_sizes(t, 768, 2, "with float indices")
95+
96+
-- Exactly at power-of-2 boundaries
97+
t = make_array(1024)
98+
check_sizes(t, 1024, 0, "exactly 1024 elements")
99+
100+
t = make_array(2048)
101+
check_sizes(t, 2048, 0, "exactly 2048 elements")
102+
103+
-- table.create pre-allocates without rehash(), allows creating tables with
104+
-- sizes that rehash() would not normally create.
105+
t = table.create(1535, true)
106+
check_sizes(t, 1535, 0, "exactly 1535 elements")
107+
-- But it'll go to a "normal" size if we overfill it.
108+
t[1536] = 1
109+
check_sizes(t, 1536, 0, "resized to next increment after insert")
110+
t[1537] = 2
111+
check_sizes(t, 1792, 0, "second bigger resize")
112+
-- Double check we didn't muck up the data with either of those resizes
113+
assert(t[1536] == 1)
114+
assert(t[1537] == 2)
115+
116+
-- Exact cap boundary - 1536 elements should get exactly 1536 slots
117+
t = make_array(1536)
118+
check_sizes(t, 1536, 0, "exactly 1536 elements")
119+
120+
-- Just over cap boundary - 1537 elements should grow to 1792
121+
t = make_array(1537)
122+
check_sizes(t, 1792, 0, "1537 elements")
123+
124+
-- Very large array - 10000 elements should get 10240, not 16384
125+
t = make_array(10000)
126+
check_sizes(t, 10240, 0, "10000 elements")
127+
128+
-- Hash spillover triggers rehash and array growth
129+
-- Inserting beyond array size into dummynode triggers rehash
130+
t = make_array(1536)
131+
check_sizes(t, 1536, 0, "before spillover")
132+
-- These will go into hash because they're not sequential with array
133+
t.foo = true
134+
t.bar = true
135+
t.baz = true
136+
-- Should be one space free in `node`
137+
check_sizes(t, 1536, 4, "node insert - node grew")
138+
t[1538] = true
139+
check_sizes(t, 1536, 4, "after num insert - placed in node")
140+
141+
-- This will overflow `t->node`, so it should try to resize array now
142+
t[1537] = true
143+
-- Note that node will NOT shrink, but the values will be moved!
144+
check_sizes(t, 1792, 4, "after spillover - array grew")
145+
assert(t[1538] == true, "1538 still present")
146+
147+
-- System tables (memcat < 2) use power-of-2 sizing, no cap
148+
change_memcat(0)
149+
t = make_array(600)
150+
check_sizes(t, 1024, 0, "system table memcat 0")
151+
change_memcat(2)
152+
153+
return "OK"

0 commit comments

Comments
 (0)