

What happens in the cart above
The program iterates over an array of tables moving values from the previous table to the current one tt[i] = pp[i]
and displays current memory consumption and cycle counter
Expected behavior
Memory consumption does not grow given the fact that no new objects are created and the previous table is cleared by setting pp[i]
to nil
;
Actual behavior
Memory consumption grows and eventually crashes the program.
Doing a manual table reset t[counter - 1] = {}
solves the issue, but in an actual game this manual GC call is very expensive(Example)
Trivia
The bug was discovered during particle cloud development and its optimization via a grid system with lots of tables and lots of particles changing cells.



Oh dear, this is a doozy.
So, this isn't a pico-8 thing so much as a Lua thing, and one that breaks the intended immersion within the abstraction.
To explain what's going on: you're creating an array of 10000 references to table objects with no starting allocation. Then, you go through each one and allocate an array with memory for 500 object references that sticks with the table even after the GC passes through. This is because the GC can't tell that you don't intend to continue using that memory. Meanwhile if you eliminate the reference to the entire sub-array by doing the "manual table reset" you're replacing the reference to the 500 object array with a reference to a table object that hasn't allocate anything. After that, the GC frees the entire 500 object space since it's no longer accessible.
This may seem like a huge bug in Lua. However, there's several reasons when it's done this way. The main thing is that in most cases this behavior speeds up table access by quite a bit. More relevantly to your case though, it's also not triggered unless you do very specific and nearly always sub-optimal things. In this case, you can avoid the behavior by either 1. using string keys rather than integer keys (because string keys cause a hashtable to be used, which doesn't try to avoid memory re-allocations quite as much), or 2. don't try to clear the table during the loop. There's no reason to.
If you want an empty table at the end, then just set the table to be empty all at once. If you don't want an empty table, then maybe consider why you would repeatedly move all the elements of a subtable to a new slot rather than setting the new slot equal to the existing subtable. If the reason is that you need to continuously update the data while still keeping the old version around for a while, then you shouldn't be deleting the old version at all. you should just be changing it.



@kimiyoribaka let's say we have a grid and we are moving objects inside of this grid. Each object has a unique id and speed. When an object moves it changes its cell. This is a common optimization when dealing with lots of interacting objects to simplify calculations by moving from each-to-each object interaction to neighboring cells interaction. As each object has different speed almost no cell at a time can be reseted completely, they all have one or more objects inside. And table reset can be done only by doing for in pairs
loop to copy the table which is obviously not a cheap operation.
Changing keys to strings does not help at all.
So, it is an intended behavior(even if it is lua behavior)? is it fixable? Can the dev team modify deli()
to deal with this kind of problems?



So, before considering this a "problem", consider this: the behavior in question is a major factor in how fast your operations can be done at all. Thus, eliminating the behavior would be a trade off for how much can be done in the needed amount of time.
Regarding what can be added to pico-8 itself, modifying deli()
would be a bad idea, since it's expected to work in the usual way for what it does. A better idea would be
to make a function that sets the capacity of a table's array to a given amount. However, that'd require some pretty deep and careful changes in how pico-8's version of Lua works.
That said, I just took a look at your particle cloud code. It's a pretty extreme case, where resetting the tables makes sense. However, at the very least you don't need to reset tables that aren't going to change at any point during your simulation.



It seems that the problem is not that nil does not delete, but rather that the memory area of the table, which has increased in size with the increase of elements, is not resized appropriately smaller when the number of elements decreases. I haven't checked this myself, but it seems that normal Lua automatically resizes memory allocations appropriately by default (as a table memory management mechanism, not GC).



I tested it with just Lua and the behavior is the same: memory consumption constantly grows, so maybe it is not a bug but a feature xD
Code snippet:
counter = 1; function _init() t = {}; for i=1,10000 do t[i] = {}; end tt = t[counter]; for i=1,500 do tt[i] = {}; end counter = counter + 1 end function _update() for i=1,500 do pp = t[counter - 1] tt = t[counter]; tt[i] = pp[i]; pp[i] = nil; -- manual table reset -- t[counter - 1] = {} end counter = counter + 1; end function _draw() local mem = collectgarbage("count") print(counter..") RAM: "..mem); end _init() for i=1,9999 do _update() _draw() end |



I tried running that code on my local lua (Lua 5.4.7). The last line of output is
10001) RAM: 81490.924804688
Next, uncomment the next line of "manual table reset".
10001) RAM: 1036.7685546875
Leave that code commented and instead add the following two lines to the end of the _update function to try to induce a resize.
pp.abc=1 pp.abc=nil |
10001) RAM: 1238.09765625
Resizing now appears to occur.
If this resizing is also triggered in PICO-8, the situation could be improved.



Tried it with local lua(same version) and got almost exactly your result. The memory consumption still grows but not that fast.
Unfortunately, it does not work in PICO-8 :(
The program still crashes.
[Please log in to post a comment]