Who cleans up?
I do! You never free anything. There’s no ERASE, no delete, no free.
But it’s worth knowing how I decide something is finished with, because it
tells you how long your things last.
Things last as long as something holds them
A list, a map or an object stays around for as long as anything is holding it: a variable, another list, a field of an object, a function that kept it. The very moment the last holder lets go, it’s gone, and its memory with it.
TYPE Monster
name AS STRING
END TYPE
horde = []
FUNCTION spawn(name)
LOCAL fresh = Monster({name: name})
PUSH horde, fresh // the horde holds it now
END FUNCTION // `fresh` lets go here; the horde hasn't
spawn("slime")
spawn("bat")
PRINT LEN(horde), horde(0).name
gone = DEQUEUE(horde) // out of the horde, into `gone`
PRINT LEN(horde), gone.name
gone = NULL // the last holder lets go: the slime is freed
2 slime
1 slime
That’s the whole rule. To keep something, put it somewhere: a variable, a list. To let it go, take it out, or give the variable something else. Numbers, text and coordinates are simpler still: each variable has its own copy, so there’s nothing to think about for those.
A local variable lets go when its function finishes. That’s why a function can make a thousand little lists to do its work and none of them pile up.
FUNCTION busy()
FOR i = 1 ... 1000
LOCAL scrap = [i, i * 2, i * 3] // made, used, and freed each pass
END FOR
RETURN "all tidy"
END FUNCTION
PRINT busy()
all tidy
Things that hold each other
Here’s the one tricky case, and I handle it too. Sometimes two things hold each other. The player’s target is the enemy, and the enemy’s target is the player. Even when you’ve let go of both, each still has a holder: the other one.
TYPE Fighter
name AS STRING
target AS ANY
END TYPE
FUNCTION duel()
LOCAL ada = Fighter({name: "Ada"})
LOCAL bug = Fighter({name: "Bug"})
ada.target = bug
bug.target = ada // a ring: each holds the other
RETURN ada.target.target.name
END FUNCTION // nobody outside can reach either one now
PRINT duel()
Ada
Counting holders can’t free those two, since neither count ever gets to nothing. So every so often I stop and look for rings like this: groups of things that are held only by each other, that no variable and no running function can get to any more. Those I take apart and free.
You don’t have to break the ring yourself, or avoid making one. A child that remembers its parent, a list whose items point back at it, a map of handlers that mention the map: go ahead.
What this means for you
- Nothing to free, ever. Let go of things by not holding them.
- Ordinary things are freed right away. Memory stays flat while you play; there’s no clean-up to wait for.
- Rings are freed a little later, the next time I look. How often depends on how much you’re making.
- I never take anything you can still reach. If a variable, a list or a running function leads to it, it’s safe — and so is everything it leads to.
- If you want something gone, check who’s holding it. The usual culprit
is a list you forgot it was in: every monster ever spawned, still sitting
in
hordelong after it was beaten.
From Python: The same model as CPython: counted, with a collector for cycles. There’s no
deland nogcmodule to poke.
From JavaScript: No need to wait for a collector for most things — a value is freed when its last holder goes. Rings take a moment longer.
From Lua: Like your collector, but most memory comes back straight away. No
collectgarbageto call.
From BASIC: No
ERASEand noREDIMto shrink things. Give the variable something else —NULL, or[]— and the old value is gone.
Crash-tan’s Tip: At the end of a level, give the big lists fresh empty ones:
horde = []. Everything that was only in the old list goes with it, in one line.