Think about the memory in a game. The terrain is loaded once and has one obvious owner. A rock mesh is drawn by hundreds of models, and should go away only when the last of them lets go. Creatures appear and vanish every second, and one may be chasing another at the moment it dies. And every frame makes thousands of little values, sparks and debris and scratch results, that are all finished with a frame later.
Each of these wants its memory handled a different way. In Cone, each one can have it.
// The terrain: one owner, freed the moment that owner goes
let terrain = Uni[Mesh]("terrain", 90000);
// A rock mesh that many models share: counted, freed with its last owner
let rock = Rc[imm, Mesh]("rock", 1200);
// Creatures come and go: a pool
var world = Pool[Npc].empty();
let orc = world.add(Npc(10, Ref[Npc].none()));
// This frame's scratch: an arena, everything in it freed at once
var frame = Arena.empty();
let debris = frame.alloc(Mesh("debris", 40));
draw(terrain);
draw(rock);
draw(debris);
Each allocation names its region, the strategy that will manage the value's memory, right in the type: Uni for a single owner, Rc for a counted one, a Pool for values of one kind that come and go, an Arena for a burst of work that ends all at once. Where data is a tangle of cycles, there is a tracing collector too, and every actor can keep one of its own. You choose per value, not per program.
Now look at the function that draws them:
fn draw(m: &Mesh) {
print <- "drawing ", m.name, ": ", m.triangles, " triangles\n";
}
&Mesh is a borrowed reference: draw may use a mesh for as long as the call lasts, but it owns nothing. So it neither knows nor cares how the mesh's memory is managed. One function serves the single owner, the counted mesh and the arena's debris alike, and most of a program's functions can be written this way. Only the code that makes and keeps values needs to know where they live.
Why it's safe
Every strategy lends the same kind of borrow, and one set of rules checks every borrow, whoever lent it: a borrow never outlives what it borrows. Try to hand back a spark from a function whose arena is about to go, and the program never gets built:
fn burst(n: i64) &mut Spark {
var scratch = Arena.empty();
let s = scratch.alloc(Spark(n, 2));
s // Error 1064: Returned borrowed reference outlives the local value it points to
}
The rest follows the same way. A single owner moves rather than copies, so the compiler refuses any use of it after it has moved. A counted value is freed only when its last owner goes. And a pool's reference, a Ref, asks each time whether its creature is still there: world.get(r) answers Some creature or None, even after another creature has taken its slot. Whatever the strategy, nothing is freed twice, and no reference reads memory that has moved on.
Why it performs
Each strategy pays only for what it does. A single owner counts and traces nothing, and frees its value the moment it goes. An arena allocates by moving a pointer along a block, and frees everything in one step, with nothing done per value. A pool reuses a slot as soon as its value leaves, and keeps values of one type side by side, quick to walk every frame. When we measured these strategies, allocating from an arena or a pool came within about two nanoseconds of a loop that only writes the same bytes; the system's general-purpose malloc added 20 to 45.
And the borrow costs nothing extra at all. Inside draw there is no count to update and nothing to trace. The more a program works through borrows, the less any strategy's bookkeeping shows up in its inner loops.
Why it matters as the code changes
A strategy is not a decision you are stuck with. Name it once:
alias MeshRef = Rc[imm, Mesh];
Months later a profile shows the counting costs something, and it turns out few meshes are really shared. Change one line:
alias MeshRef = Uni[Mesh];
Where nothing was shared, that is the whole change: every construction, every list of models, and every borrow read the same. Where something was, the compiler shows you exactly where, at the second model that wanted the same rock:
scene.push(Model(rock, 2)); scene.push(Model(rock, 3)); // Error 1048: This variable's value has been moved out.
The strategies themselves are ordinary library code. Uni, Rc and the collectors are written in Cone, not built into the compiler, so a strategy the library lacks can be written the same way, and allocated with like any other.
Try it
These links point at the Cone manual:
- Memory management: choosing a strategy, a tour of every strategy and what each costs.
- Regions and owning references, and Borrowed references.
- Reference counting, Arenas, Pools and Garbage collection.
- Making your own region.
Why Cone · Next: Shared, mutable data