Function to force table to shrink?

104 views
Skip to first unread message

Rett Berg

unread,
Jan 28, 2026, 11:16:56 AM (4 days ago) Jan 28
to lu...@googlegroups.com
I've just released a library to create immutable types which works by moving all the values to a separate proxy table. As another programmer pointed out to me however, the original table will continue to use all of its memory despite the fact that it is now guaranteed to be forever empty.

Is there an API (C or Lua) which can force a table reference to be shrunk/empty? Can one be added to force such a condition?

I think the current default is better for most cases BTW, but it would be good to let the programmer decide whether they will need the memory.

- Rett

Rett Berg

unread,
Jan 28, 2026, 1:09:35 PM (4 days ago) Jan 28
to lu...@googlegroups.com
Just some thoughts.. if this were difficult, an alternative (for my use-case at least) is to allow just "clearing" a table's contents in-place.

A more general solution (but obviously quite dangerous) might be to swap the contents of two tables but preserve their IDs.

Jure Bagić

unread,
Jan 29, 2026, 6:42:10 AM (3 days ago) Jan 29
to lu...@googlegroups.com
> Is there an API (C or Lua) which can force a table reference to be
> shrunk/empty? Can one be added to force such a condition?

AFAIK there is no such C API function to shrink the table memory, either in
array or hash part. And that is why there is no such Lua function in any of the
Lua standard libraries.

If you just want to clear the table but re-use it, just set all of the entries
to nil:

If you want to free the table object, make sure the table is unreachable (not
referenced by any local/global/field variable, upvalue, etc...) and then call
'collectgarbage' to invoke the garbage collector which will collect your
unreachable table thus freeing it's memory.
Small example:
> local t = { "test", a = "bla", 5, 7 };
> t = nil; -- table no longer reachable
> collectgarbage(); -- table memory freed by the garbage collector

Lua internally re-sizes the array/hash part of the table via 'luaH_resize' but
such a thing is not exposed via C API.

-- Jure
signature.asc

Martin Eden

unread,
Jan 29, 2026, 7:38:09 AM (3 days ago) Jan 29
to lu...@googlegroups.com
On 2026-01-28 18:16, Rett Berg wrote:
> the original table will continue to use all of its memory despite the fact
> that it is now guaranteed to be forever empty.

Well, I checked it, that's correct. Kinda strange for hash-centric language.
Does PiL mentions "don't use table as buffer" idiom?

I wrote snippet to write to Buffer[S]..Buffer[S + N]. And then clean
data by writing nils there.

I did three calls with big chunk and one call with small chunk.

I did expect memory consumption be plateau with drop at end for
small chunk. However I observe first mountain when Lua stores
data as array. For second call it starts to store data as hash and
I see second mountain with plateau. Last call with small chunk
has no effect. Inflated memory just stands there.

Posting code and mem graph.

-- Martin

P.S. collectgarbage() won't help, table is reachable.

-- ( Code
--[[
  Table memory allocation test

  Writes data to new block in Buffer. Clears that data.

  You may expect that Buffer size is proportional to size of last block.
  But in Lua 5.4 it's proportional to maximum size of block ever written.

  -- Martin, 2026-01-29
]]

local Buffer = {}
local AllocMem
do
  local StartNum = 1
  AllocMem =
    function(N)
      local EndNum = StartNum + N - 1
      print('StartNum', StartNum, 'EndNum', EndNum)
      for i = StartNum, EndNum do
        Buffer[i] = i
      end
      for i = StartNum, EndNum do
        Buffer[i] = nil
      end
      StartNum = EndNum + 1
    end
end

local BigChunk = 5 * 1e8
local SmallChunk = 1 * 1e8
AllocMem(BigChunk)
AllocMem(BigChunk)
AllocMem(BigChunk)
AllocMem(SmallChunk)

print('Done')

while true do end
-- )

Rett Berg

unread,
Jan 29, 2026, 9:46:14 AM (3 days ago) Jan 29
to lu...@googlegroups.com
Thanks for the confirmation. I think I have an idea of how to solve this.

Best,
Rett

--
You received this message because you are subscribed to a topic in the Google Groups "lua-l" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/lua-l/vAcAMy7GFJY/unsubscribe.
To unsubscribe from this group and all its topics, send an email to lua-l+un...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/lua-l/b5b24ff3-68b7-4e69-a34d-bed6ca15ad15%40disroot.org.

Lars Müller

unread,
Jan 29, 2026, 10:11:25 AM (3 days ago) Jan 29
to lu...@googlegroups.com
This is an unfortunate consequence of the requirement that items can be deleted from a table while it is iterated. This means that you more or less need to avoid a rehash, because that would change the iteration order while you are iterating.

A trick to permit a (shrinking) rehash is to rawset a *new* entry, e.g. using a fresh table as key (by reference), then deleting that entry afterwards. But this need not work, it depends on whether the implementation decides to shrink.

I'm also not a fan of this behavior because it means that the "garbage collection" is unintuitive: There is no guarantee that after a full collection, the memory usage is in any way proportional to the actual observable size of the data being stored.

The solution Lua currently wants you to use is to replace the table if you drop most of its contents and need the memory back. Alternatively, Lua could fix this, even while allowing the deletion of elements during iteration, but a more sophisticated, likely less efficient table representation would be required for this.

I wrote at a bit more length about this in a blog post: https://luatic.dev/posts/2025-04-12-lua-misconceptions/#setting-elements-to-nil-frees-up-space

- Lars
--
You received this message because you are subscribed to the Google Groups "lua-l" group. To unsubscribe from this group and stop receiving emails from it, send an email to lua-l+un...@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/lua-l/b5b24ff3-68b7-4e69-a34d-bed6ca15ad15%40disroot.org.

Martin Eden

unread,
Jan 29, 2026, 12:48:54 PM (3 days ago) Jan 29
to lu...@googlegroups.com
On 2026-01-29 16:38, 'Lars Müller' via lua-l wrote:
> This is an unfortunate consequence of the requirement that items can
> be deleted from a table while it is iterated. This means that you more
> or less need to avoid a rehash, because that would change the
> iteration order while you are iterating.
>
> A trick to permit a (shrinking) rehash is to rawset a *new* entry,
> e.g. using a fresh table as key (by reference), then deleting that
> entry afterwards. But this need not work, it depends on whether the
> implementation decides to shrink.
>
> I'm also not a fan of this behavior because it means that the "garbage
> collection" is unintuitive: There is no guarantee that after a full
> collection, the memory usage is in any way proportional to the actual
> observable size of the data being stored.
>
> The solution Lua currently wants you to use is to replace the table if
> you drop most of its contents and need the memory back. Alternatively,
> Lua could fix this, even while allowing the deletion of elements
> during iteration, but a more sophisticated, likely less efficient
> table representation would be required for this.
>
> I wrote at a bit more length about this in a blog post:
> <https://luatic.dev/posts/2025-04-12-lua-misconceptions/#setting-elements-to-nil-frees-up-space>
>
>
> - Lars

Thanks for link to your essay Lars, I enjoyed reading it much!

However I don't understand how and why your proposed hack can force
table rehash:

  local allow_rehash
  do
    local key = {} -- can't be in the table, because table equality is
by reference
    function allow_rehash(t)
      rawset(t, key, true) -- following this, lua is allowed to rehash t
      rawset(t, key, nil) -- restore t to original state
    end
  end

Tried it and saw no effect.

And I share opinion that collectgarbage() is not intuitive. It does
nothing for tables with nils occupying all memory.

I would join to Rett's proposal for table garbage-collect/shrink/rehash
function.

Maybe add one more ad-hoc string argument to collectgarbage()?

  collectgarbage('table', t)

  Forces rehash of table <t>. Frees memory by not storing empty slots.
  Side effect is keys sequence reordering so do not mix with
next()/pairs().

-- Martin

Rett Berg

unread,
Jan 29, 2026, 1:53:31 PM (2 days ago) Jan 29
to lu...@googlegroups.com
Just want to quick say that I attempted an approach which returned a new (empty) table when a table was frozen, rather than freezing the table "in-place".

Of course this in-theory works, but the problem is something like the following

extra = {}
t = {
  e = extra,
}
t, extra = freeze(t), freeze(extra)

Note that you wouldn't directly do the above, but you _would_ (1) build a table, (2) use the table for one or more fields in another table, (3) try to freeze one or both.

Basically, when you freeze(t) it will create a "frozen" version for `t.e`, but the  `extra` var will NOT be frozen. In my implementation I attempted to use the raw table as the proxy value - which had other issues I won't go into here.

To protect against this I would need to make a copy of the values, and for values used multiple times I'd be making multiple copies. I'd strongly prefer being able to GC the empty tables 

Best,
Rett


--
You received this message because you are subscribed to a topic in the Google Groups "lua-l" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/lua-l/vAcAMy7GFJY/unsubscribe.
To unsubscribe from this group and all its topics, send an email to lua-l+un...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/lua-l/cb1ab78e-3b0e-49c6-8332-541f4ea424aa%40disroot.org.

Lars Müller

unread,
Jan 29, 2026, 7:57:07 PM (2 days ago) Jan 29
to lu...@googlegroups.com
Glad you liked it!

Sorry if I made it sound like this hack can "force" a rehash. It can not. It can only "allow" it, meaning a space-conscious Lua implementation would be allowed to rehash without worrying about breaking iteration.

I was under the impression that PUC Lua does actually shrink tables sometimes, at least the hash part, but I will check the sources and test again (unless the authors would like to answer off the top of their heads ;)) so I can make some reasonable statements about what is likely to happen and when.

By the way, as for the discussions for how Lua could be extended: I think a "table.rehash" function (name TBD) which shrinks the table would not be unreasonable, and not dissimilar to existing features in many other languages, like C++'s std::vector::shrink_to_fit (though that shrinks a bit more aggressively than we would like in Lua). LuaJIT already has a "table.clear", but this is explicitly supposed to keep capacities intact.

An extension of collectgarbage would feel a bit dirty.

- Lars
--
You received this message because you are subscribed to the Google Groups "lua-l" group. To unsubscribe from this group and stop receiving emails from it, send an email to lua-l+un...@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/lua-l/cb1ab78e-3b0e-49c6-8332-541f4ea424aa%40disroot.org.

Martin Eden

unread,
Jan 30, 2026, 3:09:04 PM (2 days ago) Jan 30
to lu...@googlegroups.com

On 2026-01-30 02:56, 'Lars Müller' via lua-l wrote:
> By the way, as for the discussions for how Lua could be extended: I
> think a "table.rehash" function (name TBD) which shrinks the table
> would not be unreasonable, and not dissimilar to existing features in
> many other languages, like C++'s std::vector::shrink_to_fit (though
> that shrinks a bit more aggressively than we would like in Lua).
> LuaJIT already has a "table.clear", but this is explicitly supposed to
> keep capacities intact.
>
> An extension of collectgarbage would feel a bit dirty.
>
> - Lars

Hello Lars, well I disagree here.

Stock "table" module functions (concat, pack, unpack, sort, insert,
remove, move) do produce results observable in code. You can write
test for them.

Empty table slots (t['a'] = nil) are not observable. pairs() will never
return you key with nil value.

For that "dark matter" we have "shaman" function collectgarbage().
Which you also can't independently test.

It deals with mysterious "garbage". And when your code replaced
main tables it may come there and say: "Oh collectgarbage()!
We with guys just switched flow to new data tables. We kindly ask
you to free memory from the old ones."..

And so it will come there again and say similar thing regarding
table at which they discharged gatling and so now it has many holes.

-- Martin

Reply all
Reply to author
Forward
0 new messages