In ParaSail, heap storage is broken up into regions which are generally deallocated as a whole, rather than one element at a time. The concept of region-based storage management originated a decade or more ago, and was at the heart of the Cyclone programming language. The region-based storage management in ParaSail is a bit different, but is certainly inspired by all this earlier work.
To prevent dangling references, regions in ParaSail are reference counted. The reference counting is done at the region level rather than at the individual object level, since deallocation happens at the region-as-a-whole level. For a similar reason, rather than counting each individual pointer into the region, we (effectively) just keep track of the number of other regions that might contain objects pointing into the given region. Before storing a pointer from an object in region A to an object in region B, a check is made that region B is reachable from region A via one or more region-to-region "handles." The region-to-region handles form a reachability graph.
The programmer decides when to create a region-to-region handle, in anticipation of creating one or more object-to-object pointers. The operation for creating a region-to-region handle is "Allow_Reference(From_Region, To_Region)." After invoking this operation, objects in From_Region are allowed to point at objects in To_Region, or in other words, To_Region is reachable from From_Region. This cannot be revoked -- the region-to-region handles once created are immutable, until the From_Region goes away as a whole. Reachability is transitive, so after this call, anything reachable from the "To_Region" is now also reachable from the "From_Region."
Allow_Reference can create a loop in the reachability graph. Because the region-to-region handles are immutable, once such a loop is created, all regions in the strongly connected subgraph containing From_Region and To_Region will necessarily be deallocated simultaneously (their lifetimes will end at the same moment). From a reference-counting perspective, this means that the reference counting is really best done at the strongly-connected-subgraph level, and the possibly-cyclic region graph can be collapsed into a directed acyclic graph (DAG) between strongly-connected subgraphs of the original region graph.
In addition to region-to-region handles, there are also scope-to-region handles associated with local scopes of the program. To prevent pointers in local variables from ending up as dangling references, a pointer from a local variable to a region is only permitted if the region is reachable from the immediately enclosing program scope: A region is (directly) reachable from a given program scope if there is a scope-to-region handle in that scope targeting that region. Also, a region is reachable from a given scope if it is reachable from an enclosing scope. The transitivity of reachability completes the picture in the usual way, so that if region A is reachable from region B, and region B is reachable from the current scope, the region A is reachable from the current scope.
As with region-to-region handles, scope-to-region handles are immutable, so that once a region is reachable from a given scope, it will remain so until the scope is exited. The fundamental safety rule is that a region must not be deallocated if it is reachable from any still active scope of the program. Coupled with the requirement that local pointers may only point into reachable regions, this ensures that no local pointer can become a dangling reference. Similarly, a pointer in a region A that is reachable from an active scope, cannot become a dangling reference because it can only point at an object in a region B if B is reachable from A and, transitively, from the same active scope.
An alternative way of describing this notion of scope-to-region handles is that each scope has its own local region, and scope-to-region handles are actually region-to-region, the difference being that they go from a local region to a non-local, reference-counted region. Note that local regions would never be reference counted, since upon leaving the scope, they are always deallocated. For similar reasons, one would never allow a handle, nor a pointer from a non-local region to a local region, nor from the local region of an enclosing scope to a local region of a nested scope. So all in all, calling each scope a region may not actually simplify the model very much -- local regions would still have a number of special rules.
In addition to the two kinds of region handles we have described, there are two additional kinds of references that affect region lifetime -- strong references and weak references. Both of these are internally a pair of a region handle and a pointer to an individual object. The pointed-to object is guaranteed to be in a region reachable from the one designated by the handle.
The region handle in a strong reference is reference counted, meaning that as long as the strong reference exists, the designated region and any region reachable from it will not be deallocated. The big difference between the region handle in a strong reference and a normal region handle is that it is not immutable. A strong reference can be a variable, and may be reassigned to point at a different region/object. A normal region handle is immutable because it is "protecting" an arbitrary number of pointers that depend on it to prevent dangling references. By contrast, the handle in a strong reference is only "protecting" the single pointer with which it is bound, so there is no problem allowing it to be reassigned in conjunction with reassigning the pointer. To actually use a strong reference, the reference must be split into its constituent parts, producing a scope-to-region handle at the point of the use, along with a pointer protected by the scope-to-region handle in a local variable. Once the strong reference has been split out, the strong reference could then be reassigned to point elsewhere, or to point nowhere (i.e. set to null).
A weak reference is similar to a strong reference, except that it does not prevent the designated region from being deallocated. Whether the designated region has been deallocated is determined when the weak reference is split out. If the result of splitting the weak reference is a null region handle and a null pointer, then the designated region has at some point been deallocated, presumably due to the underlying storage manager running low on overall available storage.
A weak reference is useful for caching, by pointing to data that can be reconstructed when needed, but where reconstructing it is somewhat expensive -- for example, data coming from a file or a separate database, or data that is the result of a complex calculation. The weak reference can be put in a cache for recently used results, which is checked first when a result is needed. If a weak reference exists for the needed result, and after splitting turns out to be non-null, then the expense of reconstructing the result can be avoided. If is is null, then the data must be reconstructed.
More on regions in a later posting...
All this sounds like a lot of extra work and worry for programmers.
ReplyDeletePresumably, you're aiming to achieve some reliability or real-time benefits and possibly a bit of space performance. I am skeptical that you'll win much on any of these, especially once you start scaling to more complex use-cases (frameworks, coroutines, etc.).
It is more up-front work than pure garbage collection, but much less work than object-at-a-time alloc/free. The expectation is that in the long run it gives better control than garbage collection, and avoids the need to certify a fine-grained garbage collector not only for correctness but for real-time concerns as well.
ReplyDeleteClearly this needs proof by example...
Since this posting, we have simplified ParaSail storage management significantly. All variables are allocated, conceptually, on the stack. Each stack frame has a region from which its local variables are allocated. Objects can grow and shrink in size, which involves local storage allocation and reclamation, but that is essentially all "behind the scenes," from the programmer's point of view. There is no global garbage collection required, since storage reclamation takes place in response to a direct operation on an object. Since there is no storage shared between objects, when an object shrinks (such as removing an element from a mapping), the released storage may be immediately reclaimed. If the equivalent of re-assignable pointers is needed, indexable containers may be used, with indices playing the role of pointers, and the container playing the role of a region.
ReplyDeleteso if you reclaim the released storage immediately on each shrinking of an object, what exactly is the benefit of allocating objects of each function in a separate region, as opposed to using a global heap?
ReplyDeleteThere are at least two reasons. First of all, most objects don't shrink down to nothing before their scope is exited, so region-based storage management allows all objects declared in a scope to be reclaimed all at once by reclaiming the associated region. Secondly, there are major advantages to breaking storage up into separate regions in the presence of parallelism. A single global heap is a bit of a nightmare when there are many parallel computations all using it. Here is a presentation from the Strange Loop "unsession" on region-based storage management, which explains some of these issues:
Deletehttp://bit.ly/QB2o9G
This is also discussed further in the article on ParaSail in embedded.com (starts at the second page):
http://www.embedded.com/design/other/4375616/ParaSail--Less-is-more-with-multicore