Cone's region-based memory management is one of its more unusual features.
By decorating managed references with regions, you obtain precise control over how memory is allocated and freed on a per-object basis. Coupled with the use of borrowed references, regions make it possible to speed up the throughput and responsiveness of your programs, while still preserving memory safety.
How is this possible?
When the Language Chooses the Memory Management Strategy
Most languages completely decide for you the strategy they will use to manage memory. For example, you might use the "new" keyword to allocate memory for a new object:
var person = new Person("Tako");
Underneath, the language completely decides exactly how to allocate heap memory for this new object. It also decides when (and how) it will free allocated memory once the object is not longer needed.
The memory management strategy chosen by most languages (e.g., C# and Java) is some flavor of tracing garbage collection. Other languages (like Python and Swift) choose to use an enriched form of reference counting. In many ways, these are both excellent choices. They are flexible across a wide range of use cases, and their use can be largely invisible to the programmer.
However, their runtime mechanisms often make these two strategies the slowest in throughput (and unpredictable in responsiveness), especially when memory is constrained. Just imagine how many CPU cycles are spent allocating, freeing, copying, and liveness tracking thousands or millions of small data objects found in many programs today. Since these operations mutate memory along the way, the whole process is further slowed by needing to sync memory changes in cache back to much slower memory. By contrast, the arena (bump pointer) and pool (free list) strategies can sometimes be 10-20x faster. Even single-owner (Rust and C++) is often faster.
The truth is simple: there is no perfect memory management strategy. Each strategy carries different trade-offs in terms of throughput, responsiveness, safety, convenience, memory consumption, runtime size, and data type restrictions. So, when a language restricts programs to a single strategy, that may work out well for some programs, but it could easily be a poor fit for others.
This is where Cone's regions come into play. Each region is an importable library package that implements some specific memory management strategy.
When you create a new object in Cone, you specify the region responsible for allocating (and ultimately freeing) heap memory for that object:
imm person = +so Person("Tako");
This uses the 'so' (single-owner) region to allocate and free memory. You could just as easily pick the 'rc' (reference count) or 'gc' (tracing garbage collection) region instead. For each new object, the choice of region is yours. All are safe, but each strategy carries a different balance of advantages and disadvantages. You pick the one best aligned with how the person object is used by the program.
Even better, every heap-allocated object can select a different strategy, based on its needs.
Since Cone offers a broad variety of memory management strategies, you can optimize for throughput and/or responsiveness by carefully selecting the right mix of strategies that best fit the program's data use requirements:
- Take advantage of the superior speed of arenas, when it is okay to temporarily leak some memory on short-lived scratch pads.
- Take advantage of fast object pools when you have quite a few objects of similar size.
- Use single-owner when you can guarantee an object is only referenced by one other object (which happens often).
- Then use ref-counting or tracing GC otherwise. (And by the way, if you don't select tracing GC, you won't pay any of its run-time costs).
Furthermore, if none of the existing region packages meet your needs, you can easily define and use your own region.
Cone's versatile regions give you the power to architect your program's use of memory for optimal throughput and latency, without sacrificing safety, convenience, or data structure flexibility.
Click here to learn more about: region-managed references. Follow-on pages dive into more detail about owning references, weak references, and region strategies.
Region Polymorphism and Borrowed References
At this point, you might be concerned that having so many different regions is going to be a nightmare for developers of popular library packages: which one should they choose? Will several different versions have to be built to accommodate different regions?
Fortunately, this is rarely a concern, because regions are types and modules support polymorphism. For the few packages that actually need to allocate (and free) objects, they can either pick an innocuous region (like single-owner or arena) or can be written to be polymorphic over the region selected by the module that imports the package.
For packages that do not need to allocate objects, but do need to use references to objects, they can make use of Cone's borrowed references. A borrowed reference can be obtained from any region-managed reference and then passed along to any package. Borrowed references have "forgotten" what region the object belongs to, but are still always safe to use for reading (or modifying) some object's state. It is the lifetime-constraint placed on borrowed references that makes them memory-safe.
Borrowed references are not just useful for polymorphism. Their use also speeds up many programs. Since borrowed references are free from region oversight, they consume no cycles on liveness bookkeeping activities, such as tracing or reference counting. Typically, the more your program uses borrowed references, instead of region references, the faster and more flexible it becomes.
Click here to learn more about: borrowed references. Follow-on pages dive into more details.