My HUD can minimize to one button in the corner. Restoring it repaints a whole page, so every button gets its material and click action back. ll.SetLinkPrimitiveParamsFast does that in one call from one list of rules, with a PRIM_LINK_TARGET before each button's rules. I call that list a paint list. Before I cut it down, my mouth page's paint list held 285 entries.
In SLua, that list is a table. Its memory cost depends on how many slots it has, including empty ones. Appending doesn't add one slot at a time. The table grows in steps, so this list of 285 entries has room for 384. Every slot costs 16 bytes whether it holds a value or not. The 99 empty slots add 1,584 bytes.
table.create(285) reserves exactly 285 slots before the first append. The list fills them and ends with none to spare.
285 entries, 384 slots
Each square is one slot, and every slot costs 16 B whether it holds a value or not
Appending into {}
6,144 B1,584 B empty
table.create(285)
4,560 B
Allocations
10 vs 1
285
Append into {}rules[#rules + 1] = value
Reserve firsttable.create(285)
Grew through
1
2
4
8
16
32
64
128
256
384
Holds a value
Allocated, empty
Past 128 slots, each step adds 128 more, so the empty tail can reach 127 slots, 2,032 B. Reserving exactly leaves none.Array slots only, from SLua's growth rule. The table itself adds 32 B either way, and strings or tables in the list are stored separately. My Second Life tests matched this rule at every length I tried.
To reserve slots without wasting memory, you need to know the count. Reserve too few and the list grows past them. Reserve too many and the extra slots cost memory for as long as the list lives. One fixed reservation in my HUD saved 256 bytes on each of its two largest lists and wasted 1,792 bytes on a smaller one. Cutting entries saved more, as Write fewer entries first shows.
The same sizing rules apply to other lists you build by appending, such as particle rules, lines you join into one message, or keys you collect before a bulk operation.
Reserving pays off when you know how many entries the list holds at its fullest. If you can't know that, let it grow.
Let the list grow when
You can't bound the count, or the bound is far above the usual count
The count lands on or just under a step, such as 128, 256, or 384 entries
The list is small, so its empty slots have little effect on your script's peak memory use
The code that calculates the count costs more memory than the slots it saves
Reserve with table.create when
You know the most entries the list holds at once, including entries you later remove
The count lands just past a step, such as 129 or 257 entries
The list stays loaded, like a cache, so empty slots cost memory the whole time
It's the biggest list your script builds, so its size can set the script's peak
Write fixed lists as literals
When one piece of code knows every entry, write the whole list as one literal: {PRIM_LINK_TARGET, link, PRIM_RENDER_MATERIAL, ALL_SIDES, material}. SLua builds a literal at its exact size, with no table.create call. Write fixed lists as literals has the numbers.
A table keeps its list entries in an array of slots. SLua charges 32 bytes for the table and 16 for each slot. Numbers, booleans, and vectors fit inside the slot. Strings and other tables live separately, with a pointer in the slot.
What one slot holds
One square = one byte · 16 B per slot, used or not
0.75
Value8 B
Vector z4 B
Type4 B
Every number is an 8-byte double, stored in the slot. Whole numbers and decimals cost the same.
Value
Points elsewhere
Type tag
Every slot costs 16 B, full or empty, whatever it holds. Strings and tables cost extra, stored outside the slot. The layout comes from SLua's source.
An empty slot costs the same 16 bytes as a full one. So does a slot that points to a two-letter string. The slot count determines the array's memory cost.
When an append doesn't fit, SLua picks a bigger array and moves the entries into it. Up to 128 slots, the size doubles, from 1 to 2 to 4 and on up to 128. Past 128, Second Life's SLua adds 128 slots at a time, to 256, then 384, then 512. Stock Luau keeps doubling, so it would allocate 512 slots for the same 285 entries.
Growing in steps reduces the number of resizes. Appending 285 entries into {} allocates the array 10 times, not 285. A resize can copy every entry into the new array. Spare slots let later appends avoid that work.
I tested the rule in Second Life at 12 list lengths on both the main grid, Agni, and the beta grid, Aditi. Each test script filled one list of numbers by appending into either {} or table.create(n).
Memory added by filling one list
Measured in Second Life · both grids matched
Entries
257
Into {}
6,208 B
384 slots
table.create
4,176 B
257 slots
Saved
2,032 B
127 empty slots
Append into {}
table.create(n)
One entry past 128, 256, or 384 leaves 127 slots empty, 2,032 B. Landing on a step leaves none. 92, 127, and 235 are list lengths from my HUD, and 240 is its reservation. Lines show SLua's growth rule. Circles show changes in ll.GetUsedMemory() before and after filling one list of numbers. I ran each length 4 times on each grid. Every run matched, and Agni and Aditi matched to the byte. Each result includes a fixed 64 B for the new table and the test's own bookkeeping.
Every result matched the rule. At 129, 257, and 385 entries, reserving saved 2,032 bytes by avoiding 127 empty slots. That's the most empty slots any list past 128 entries can have. At 128, 256, and 384, reserving saved nothing because the list filled its last step exactly.
Cut enough entries to build the list within the previous size step, and you avoid allocating the next one. Before reserving anything, check whether the list needs every entry.
My HUD's tongue prim has eight faces with the same material. The restore used to write a material rule for each face, totaling 24 entries. Now one ALL_SIDES rule uses 3 entries for those faces, and each face keeps its own texture placement.
Across the mouth page, that took the restore from 285 entries to 231. The list needed 256 slots instead of 384, saving one whole step of 2,048 bytes. With both winks and the tongue on, the page reaches 235 entries, still inside that step.
Count the most entries the list holds at once, not how many times you write to it. A restore often appends a PRIM_LINK_TARGET for a button, finds nothing to change, and removes the target again. The figure builds one small restore with SLua's table.append, which adds values at the end, then reserves the most the list held.
Building one restore list
Two buttons change and one doesn't · each square is one slot
Start an empty list
0 entries0 slots · 0 B
Target the first button
grows to 1, then 2
2 entries2 slots · 32 B
Add its material and click action
grows to 8
7 entries8 slots · 128 B
Add the next button
grows to 16
12 entries16 slots · 256 B
Skip a button with no changes
held 14 for a moment
12 entries16 slots · 256 B
Reserve the most it holds
table.create(14) never grows
12 entries14 slots · 224 B
Holds a value
Removed again
Allocated, empty
restore.slua
-- the most entries this list holds at oncelocal=table.(14)table.(,,.)table.(,,,.)table.(,,.)table.(,,.)table.(,,,.)table.(,,.)-- nothing changed on this button, so remove its target[#] =nil[#] =nilll.(,)
A list's size follows its fullest moment, not where it ends. Reserving the final 12 would still grow to 16 when the last target went on, so the listing reserves 14. SLua's table.append adds each value to the end, like rules[#rules + 1] = value.
In the state I captured, my settings restore made 173 writes but never held more than 127 entries. Removing a target leaves its slots allocated, so the list keeps enough room for its highest entry count. In my tests, skipping those targets entirely took the same memory as writing and removing them.
Find the maximum count across every state that can build the list. One captured list isn't enough. I replayed the settings restore in all 16 combinations of my HUD's toggles, and it always ended at 127 entries. A restore can still add a link target and remove it again, so the list can briefly hold 2 more entries. When the build step generates my HUD's script, it uses every control's possible states to calculate a bound of 129, including that target. Reserving 127 would let the list grow to 256 slots if it passed that count.
My HUD uses two reservation sizes. Every expanded page reserves 240 slots, enough for the biggest one, and a collapsed page reserves 94.
Five paint lists from my HUD
Memory added while building each list, measured in Second Life
local rules = {}
Eye aim update
13 entries · 13 writes
352 B16 slots
Collapsed page restore
92 entries · 94 writes
2,144 B128 slots
Settings page restore
127 entries · 173 writes
2,144 B128 slots
Face page restore
206 entries · 244 writes
4,192 B256 slots
Mouth page restore
235 entries · 271 writes
4,192 B256 slots
Bars run to 256 slots · line at 128
Entries
Allocated slots
Reserving 240 saved the two biggest pages 256 B each and cost the settings page 1,792 B. Reserving 94 saved the collapsed page 544 B. I measured with ll.GetUsedMemory() before and after building each list. Each policy ran four times on Agni, with matching results. Each result is 96 B plus 16 B per slot. The lists replay entries captured from my HUD with both winks and the tongue on. They include link targets that the restore later removed. Exact sizes use those captured lengths, so they only fit that state. My build's bound for the settings page is 129. The eye update is the fixed list the literal section rewrites.
The 240-slot reservation helped the two big pages. Appending without a reservation had rounded them up to 256 slots, so 240 saved 16 slots, or 256 bytes, each. It hurt the settings page, which lands just under a step at 127 entries. That list needed only 128 slots without a reservation. Giving it 240 cost 1,792 bytes more. The 94-slot reservation saved 544 bytes on the collapsed page, which appends had rounded up to 128 slots.
Exact sizes saved the most slots, but the code that picks a size for each page stays loaded too. I tried a function and a lookup table in a local replay of my HUD. Compared with two constants, the function added 590 bytes of code and raised the highest peak by 677 bytes. The lookup table added 506 bytes of code and raised the peak by 738 bytes. I kept the two constants and accepted the wasted slots on the settings page.
When one piece of code knows every entry, write the list as a literal. SLua builds it at its final size, with no appends and no table.create call.
My HUD's eye aim update always sends 13 entries. They specify a link target and one rule that positions the eye texture on its face. As a literal, the list looks like this:
eye.slua
-- "" clears that override, so the material's own value appliesll.(,{,.,,.,"",.,.,0,"","","","","",})
One 13-entry list, three ways
Whole test script, against writing into {}
BytecodeMemory
Write into {}
indexed writes
baseline
baseline
Reserve, then write
table.create(13)
+27 B
−21 B
Write one literal
{ PRIM_LINK_TARGET, … }
−232 B
−280 B
The literal saved 280 B. table.create(13) saved 21 B and added 27 B of code. Bytecode comes from SLua's compiler. Memory is ll.GetUsedMemory() after 100 builds on Agni. Each version ran twice in reversed order, with matching results. These figures cover the whole script. The memory reading includes its code and stack. The version with indexed writes starts at 2,512 B of bytecode and 5,445 B of memory.
In a test that built the same 13-entry list 100 times, table.create(13) added 27 bytes of code and saved 21 bytes overall. The literal saved 232 bytes of code and 280 bytes overall. Use literals for fixed layouts. For lists whose length depends on state, reserve slots if you can bound the count.
Most paint lists live for one call. A list you keep between events, such as a reused scratch list or a cache, holds its slots the whole time. Its empty slots use memory between calls too.
table.clear empties a list and keeps its slots for the next fill. Reusing those slots avoids another allocation, but they still count toward your script's memory while the list is empty.
Reuse never allocates during a fill, but it holds 4,144 B more while the list sits empty.
Growing and reserving give their slots back once the list is dropped, and reuse keeps them. Measured with ll.GetUsedMemory() at three points in the live table test, compared with an empty control. The 32 B the first two keep is a field slot in the test's own state table, not the list. Checkpoints can't show how often each version allocated.
Reuse suits a list you refill often, if your script can afford to keep its empty slots. A list shared between a small update and a big restore keeps the big restore's slots during every small update too.
SLua also has table.shrink, which drops a table's spare slots. If you plan to keep a list after building it, finish the appends and then shrink it.
names.slua
local={}for, indo[#+1] =.end-- this list stays loaded, so drop its spare slotstable.()
On both Second Life grids, shrinking a list of 285 appended entries reduced its slots from 384 to 285. That used the same memory as table.create(285). A paint list you send and discard doesn't need shrinking because its slots are freed soon afterward.
The sizes above count list slots. Your script's memory limit also covers its code and everything else loaded at the same moment. If the script peaks during another operation, making this list smaller may leave that peak unchanged.
I replay my HUD in a local SLua host that charges memory the way Second Life does. The reservations added 180 bytes of code and lowered the peak during panel use by 76 bytes. Across all my replays, though, the highest peak rose 180 bytes. It occurred during a reset that doesn't use the reservations. Writing fewer entries saved more, cutting the panel peak by 1,950 bytes.
What two changes did to my whole HUD
Local replay, each against the build before it
CodePanel peakHighest peak
One rule for shared materials
ALL_SIDES
+98 B
−1,950 B
−507 B
Then reserve 240 and 94
table.create(240)
+180 B
−76 B
+180 B
Writing fewer entries cut the panel peak 1,950 B. The reservations cut it 76 B more, and their code raised the highest peak 180 B. I replayed my HUD in a local SLua host that charges memory the way Second Life does. The panel peak is the highest point while using its panels. The highest peak covers every replay, and after these changes it comes from a reset.
Check your own script before and after:
Find the operation where your script peaks before sizing any list
Measure the whole script, including the code that works out the size
Check memory while the list is full and again after you release it
Test the uncommon states that make the list longer
ll.GetUsedMemory() reports memory at the moment you call it. It misses anything allocated and freed between calls, so a checkpoint can't show an operation's peak.
For this 14-field test record, linkset data holds about 660 JSON objects or 6,000 packed buffers. Compare storage savings with script memory and code costs.
Objects··9 min
What does named access cost?
A shared metatable lets code read arrays and buffers by field name. In the test, 100 records use about of the script's memory with named fields, or as buffers with names.