0% found this document useful (0 votes)
224 views

Ownership You Can Count On: A Hybrid Approach To Safe Explicit Memory Management

This document presents an approach called "alias counting" for memory management in programming languages that combines type-based object ownership and run-time reference counting. The key points are: 1. Alias counting uses owning and non-owning pointers at compile-time to ensure there is exactly one owning pointer per object at run-time. 2. Reference counting tracks non-owning pointers, and an object is destroyed when its owning pointer is destroyed if the reference count is zero. 3. The approach is implemented in a language called Gel, which is a safe C#-like language. Benchmarks show Gel uses less memory than Java/C# and performs comparably to C++ in most cases.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
224 views

Ownership You Can Count On: A Hybrid Approach To Safe Explicit Memory Management

This document presents an approach called "alias counting" for memory management in programming languages that combines type-based object ownership and run-time reference counting. The key points are: 1. Alias counting uses owning and non-owning pointers at compile-time to ensure there is exactly one owning pointer per object at run-time. 2. Reference counting tracks non-owning pointers, and an object is destroyed when its owning pointer is destroyed if the reference count is zero. 3. The approach is implemented in a language called Gel, which is a safe C#-like language. Benchmarks show Gel uses less memory than Java/C# and performs comparably to C++ in most cases.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 11

Ownership 

You Can Count On:
A Hybrid Approach to Safe Explicit Memory Management
Adam Dingle David F. Bacon
Google IBM T.J. Watson Research Center
1600 Amphitheatre Parkway 19 Skyline Drive
Mountain View, CA 94043 Hawthorne, NY 10532 
+1 (415) 425­6891 +1 (914) 784­7811
adamdingle@gmail.com dfb@watson.ibm.com

ABSTRACT C++ is inherently unsafe, as there is nothing to prevent a program


from writing to deallocated memory. The most widely used
While many forms of memory management have been proposed, automatic memory management mechanisms include classical
the only ones to achieve widespread adoption have been explicit reference counting and garbage collection [18]. Reference
deallocation and garbage collection. This leaves programmers counting may incur substantial run-time overhead and cannot
requiring explicit control of memory behavior unable to obtain the easily free cyclic data structures. Garbage collected-systems often
software engineering and security benefits of programming in a use substantially more memory than their manually managed
safe language. We present a new approach to memory counterparts. An additional disadvantage of garbage collection is
management called alias counting which combines type-based that it frees memory non-deterministically. Deterministic
object ownership and run-time reference counting of references by destruction is useful in system programming, especially in
non-owning (aliasing) pointers. When an owning pointer is languages which allow arbitrary user code to run when objects are
destroyed, if the target object's reference count is zero, the object destroyed; such code may be used to release system resources
is destroyed; otherwise a run-time error occurs. We have when no longer needed.
implemented alias counting in a safe C#-like language called Gel. In this paper we propose alias counting, an automatic memory
The Gel compiler includes an effective reference count management mechanism based on ownership and run-time
elimination optimization that takes advantage of ownership reference counts. The mechanism can be used to implement a safe
semantics. Our anecdotal experience in writing the Gel compiler language which frees objects deterministically. To show that alias
and some smaller benchmarks in Gel is that the annotation counting is practical, we've constructed a simple object-oriented
requirements and extra programming effort are small, and the type language called Gel which is essentially a subset of C# [14]
system catches most ownership errors at compile time. extended with ownership-based memory management. We've
Quantitatively, we compare microbenchmarks written in Gel, Java, implemented an interpreter and compiler for Gel, written in the
C#, and C++, and versions of the Gel compiler written in both Gel Gel language itself. We've ported a number of common
and C#. In our benchmarks, Gel programs require at most 40% benchmark programs to Gel and have found that the Gel
more memory than the corresponding C++ programs, while C# implementations often perform similarly to their C++ counterparts
and Java generally require at least twice as much memory and and use significantly less memory than their Java and C#
often considerably more. Run-time performance varies equivalents.
considerably across all languages, with no clear winner, but Gel Unlike languages with simple manual memory management
outperforms either C++ or Java/C# in all but one benchmark. Our schemes (such as malloc/free or new/delete), Gel is safe: a bug in
reference count optimization typically eliminates 90% of all a Gel program can never corrupt memory arbitrarily. Unlike
reference counting operations, speeding Gel programs by 20-40%. garbage-collected languages, memory management in Gel is
1. INTRODUCTION deterministic: objects are freed as soon as they are no longer
Today, most system programming is done in languages which needed. Unlike traditional reference-counting schemes, Gel can
are unsafe: program errors may lead to arbitrary or undefined free data structures including cycles of pointers as described
behavior. C and C++ are unsafe languages commonly used for below.
system programming. Unsafe languages have certain fundamental In this paper, we describe only Gel's ownership extensions to
disadvantages over safe languages such as Java, C# and ML. the C# language. A reader familiar with either C# or Java should
Certain classes of bugs such as buffer overruns or reads from be able to follow with no trouble; differences between the core C#
deallocated memory can occur only in unsafe languages. Many and Java languages are relatively minor and are not relevant to the
such bugs are subtle or non-deterministic, and hence expensive in discussion here. For a complete description of Gel, see the
software engineering. In addition, trusted and untrusted code may language reference [24]. The Gel project site [23] contains the Gel
safely share a single address space only in a safe language. interpreter/compiler, released as open source under the MIT
There would thus be many advantages to using safe languages License. Our decision to extend C# rather than, say, Java was
for system programming. But there are challenges involved in arbitrary, and it would be straightforward to construct a similar
making safe languages as efficient as unsafe languages in their language extending Java or another object-oriented language. The
run-time resource usage. In particular, any safe language must Gel language is large enough to write useful programs, such as the
have some sort of automatic memory management mechanism: Gel interpreter/compiler itself.
explicit memory allocation and deallocation as found in C and
Our proposed memory management mechanism has the then present detailed rules for type checking and an argument that
following fundamental characteristics: the single-owner property holds in Gel.
Gel, like C#, includes both value types and reference types.
• Objects are allocated individually using a heap allocator. Value types in Gel are primitive types including bool, int and
Deallocation is automatic; there is no delete statement. float. Reference types include object, string and user-
• The type system distinguishes owning and non-owning defined subclasses of object. Gel extends C#'s type system with
pointers to objects. This is a compile-time distinction only: owning types. The type T ^ (where T is a reference type)
at run time, all pointers are represented as ordinary memory represents an owning pointer to an object of type T. A reference
addresses. type written without a ^ character indicates a non-owning pointer.
• The type system ensures that at run time there is always Like a non-owning pointer, an owning pointer may hold either an
exactly one owning pointer to every object. object reference or null.
• Ownership of an object may transfer as a program runs. In Gel, as in C#, all value and reference types are ultimately
Any owning pointer which loses ownership of an object derived from the top-level object type. Strictly speaking,
becomes either null (if it is a field) or inaccessible (if it is a inheritance applies only to value and reference types; owning
local variable or parameter). types are not themselves considered members of the class
• At run time, the language implementation keeps a reference inheritance graph.
count of the number of non-owning pointers to each object. Local variables, method parameters, method return values and
• When a non-null owning pointer is destroyed, the object it object fields may all have owning or non-owning types.
points to is destroyed as well. If a destroyed object has a In Gel there is always exactly one owning pointer to every
non-zero reference count, a run-time error occurs and the object. As a consequence of this fact, all objects in a running Gel
program is terminated; it is the programmer's responsibility program belong to a set of ownership trees; tree roots include
to avoid this condition. static fields of every class and local variables in every stack frame.
(During the course of expression evaluation, temporary values of
Our goal in this work is to show that a safe language with such owning type may form additional tree roots.)
a type system and memory management mechanism can be Ownership of an object can transfer from an owning pointer P
implemented, and can be practical in the following senses: to an owning pointer Q. In this case we say that P loses ownership
of the object and that Q takes ownership of the object.
• It is easy to write real programs so that they pass the type At run time, the Gel implementation keeps a count of the
checker; implementing common data structures is number of non-owning pointers to each object; this count may be
straightforward. zero or an arbitrary positive value. The count is used only for
• It is easy to write programs so that they never yield run-time ensuring that no non-owning pointers remain to a destroyed object
errors due to non-zero reference counts on destroyed as described in a following section.
objects. Here's a minimal Gel program:
• Programs run efficiently, ideally consuming roughly as
class Foo {
much memory and CPU time as their manually managed public static void Main() {
counterparts. Foo ^f = new Foo();
}
The contributions of this work are: }

The new operator returns an owning pointer. In this program,


• A unique approach to safe memory management combining
the local variable f receives ownership of the newly allocated
static type-based ownership with run-time reference counts;
object. When Main() exits and f goes out of scope, Gel destroys
• An implementation of the approach in a dialect of C# we
the object automatically.
call Gel;
If an owning pointer variable is updated with a new value, any
• Implementation of the Gel compiler and a number of
previously pointed-to object is destroyed immediately:
benchmarks in Gel;
• Intra- and inter-procedural optimizations that take void Fun() {
advantage of ownership properties and make the reference Foo ^f = new Foo(); // allocates a new Foo
counting cost acceptable by eliminating typically 90% of all f = new Foo(); // the first Foo allocated
counting operations; is destroyed here
• An experimental evaluation comparing the performance of f = null; // the second Foo allocated is
one large Gel program (the Gel compiler) to the same destroyed here
program written in C#, and of several microbenchmarks ...
}
written in Gel, C++, Java, and C#.

An assignment to an owning variable causes an ownership


Currently, the major limitation of Gel is that its reference counting transfer:
optimizations can not easily be applied in the presence of aliasing
arising under multi-threading. As a result, Gel currently only void Fun() {
supports single-threaded programs. Foo ^f = new Foo();
Foo ^g = f; // ownership transfers to g
2. THE GEL LANGUAGE }
We proceed as follows. We first give an informal overview of
ownership in Gel, illustrated with a number of code examples. We
It is a compile-time error to read from any local variable public Node ^next;
which may have lost ownership: public Node prev;

void Fun() { public Node(int j, Node ^n, Node p) {


Foo ^f = new Foo(); i = j; next = n; prev = p;
Foo ^g = f; // ownership transfers to g }
Foo h = f; // error: f has lost ownership }
}
As a larger example, the following method takes an integer k
The Gel type checker enforces this restriction by performing and returns a doubly linked list containing the integers 1 through
control flow analysis on each method and ensuring that no owning k:
variable loses ownership on any code path leading to a point
where the variable is read. This is a straightforward extension of Node ^IntList(int k) {
the mechanism used to ensure that every variable is initialized Node ^n = null;
before use. for (int i = 1 ; i <= k ; ++i) {
Gel could, alternatively, cause an owning local variable to // keep pointer to previous head of list
become null if it loses ownership. We've decided against this Node o = n;
behavior since this side effect might not be obviously apparent to n = new Node(i, n, null);
someone reading the code. More fundamentally, this would if (o != null)
destroy the language's erasure property, described in a following o.prev = n; // create backlink
section. }
return n;
2.1 Fields }
As mentioned above, object fields (including both instance
fields and static fields) may have either owning or non-owning In the line which calls the new Node() constructor, the
types. Here is a singly linked list class which holds an integer in owning variable n first loses ownership of the object it points to,
each node: since it passes that value to a constructor parameter with owning
type: the constructor takes ownership of the object passed. After
class Node {
public Node ^next; the constructor returns, n receives ownership of the newly
public int i; allocated object.
public Node(int j) { i = j; } 2.3 Type Casts
}
Type casts in Gel never affect whether a type is owning; a cast
is always from an owning type to an owning type or from an non-
Gel includes an operator take which takes ownership of a owning type to an non-owning type. Syntactically, a cast is always
value from an owning field, and has the side effect of setting the written without the "^", even if the type it affects is owning. Thus:
field's value to null. An expression which reads an owning field
without using take has a non-owning pointer type: void Fun() {
Foo ^f = new Foo();
void main() { Bar ^b = (Bar) f; // f loses ownership to b
Node ^n = new Node(5); }
n.next = new Node(4);
Node ^p = n.next; // compile-time error: no
conversion from Node to Node ^ Gel, like C#, includes operators is and as which perform run-
Node ^p = take n.next; // okay: ownership time type checks. The expression E is T evaluates E to a value
transfers; now n.next is null v, and returns true if v is of type T and false otherwise. The
} expression E as T evaluates E to a value v, and returns v if it is
of type T and null otherwise. Like type casts, the is and as
Gel could conceivably take ownership from and null out operators are always written with non-owning types, but may
owning fields automatically whenever they're read in an owning operate on either owning or non-owning types:
context; then the take operator would be unnecessary. But then
the side effect of nulling out a field would be much less apparent void Fun() {
to programmers; Gel includes take so that this side effect is Foo ^f = new Foo();
Bar ^b = f as Bar;
explicit (and to preserve the erasure property described below). }
2.2 Methods 2.4 this
A method parameter with owning type takes ownership of the In Gel this is always a non-owning pointer:
value passed to that parameter when the method is invoked. If a
method's return type is owning, then the method's caller takes class Foo {
ownership of the return value when the method returns. static Foo ^f;
Here is a doubly linked list class with an integer in each node; ...
its constructor takes arguments with owning and non-owning void Fun() {
pointer types: f = this; // compile-time error:
//can't convert from Foo to Foo ^
}
class Node { }
int i;
2.5 Strings those objects are destroyed recursively; if any fields are non-
Strings in Gel are not owned; the type string ^ does not owning pointers the reference count of their referent is
exist. In a compiled Gel program, strings are internally reference decremented. Finally Gel checks o's non-owning reference count.
counted; a string is freed when its reference count reaches zero. If the reference count is non-zero, Gel issues a run-time error and
We considered making strings owned in Gel, but strings are so terminates the program; otherwise Gel frees the memory used to
common in real-world programs that we thought it might store o and continues execution.
burdensome for programmers to have to worry about string This simple destruction algorithm can destroy linked data
ownership. There's no problem with using classical reference structures automatically. If each node of a singly linked list
counting for strings: they don't point to other objects so they can contains an owning pointer to the next node in the list, then when
never be involved in a pointer cycle. a pointer to the list head goes out of scope Gel will destroy the
A string may be converted either to an object ^ or to an entire list.
object: Because an object's subobjects are destroyed before the
object's reference count is checked, the algorithm can destroy
void Fun() { many data structures containing pointer cycles. As a simple
object ^o = "a"; // ok example, the following method uses the IntList method
object p = "b"; // ok presented earlier to create a doubly-linked list containing 2 nodes:
}
2.6 Arrays void MakeList() {
Node ^n = IntList(2);
In Gel an array may hold either owning or non-owning }
pointers:

void Fun() { When the method exits, the variable n goes out of scope and
Foo [] ^a = new Foo[5]; // an owned array the list is destroyed automatically. Initially, the first list node has a
of non-owning pointers to Foo objects non-owning reference count of 1 since the second list node points
Foo ^[] ^b = new Foo^[5]; // an owned array to it. Before checking the first node's reference count, Gel
of owning pointers to Foo objects destroys the second list node, which destroys the non-owning
Foo ^[] c = new Foo^[5]; // a non-owned pointer to the first node and hence decrements the first node's
array of owning pointers to Foo objects
b[0] = new Foo(); reference count to 0.
a[0] = b[0]; In general, then, Gel can automatically destroy any data
} structure in which every non-owning pointer from an object o
points to an object which is an ancestor of o in the object
The take operator may operate on array elements: ownership tree.
Gel's object destruction algorithm has some significant
void Fun() { limitations. Most fundamentally, it cannot destroy arbitrary graphs
Foo ^[] ^a = new Foo^[5]; of non-owning pointers; if a data structure contains non-owning
a[0] = new Foo(); pointers to non-ancestor nodes, then the programmer must write
Foo ^f = take a[0]; // a[0] is null after code to null out those pointers before the structure is destroyed.
the take Another problem is that the algorithm may use a large amount of
} stack space when destroying objects due to its recursive nature. In
2.7 Autoboxing the Extensions section, below, we propose a better algorithm for
Gel (like Java 5 and C#) provides autoboxing: a simple value destroying objects in Gel.
such an int or bool may be stored in a variable holding an 2.9 Type Conversions
object. In Gel, a boxed value always has the owning type object In the sections above we've described how ownership interacts
^. For example: with various Gel language features, and presented various
examples where Gel allows or disallows conversions between
void Fun() {
object ^o = 7; // ok owning and non-owning types. In this section we give a more
object p = false; // compile-time error: complete description of Gel's rules for type conversions.
can't convert from bool to object Gel classifies each expression in a program as one of the
} following kinds:

In an owning type T ^, T must not be a value type; the types • A variable: either
int ^ and bool ^ do not exist in Gel, for example. An • a local variable or method parameter variable
unboxing conversion must be explicit, and yields a value type: • (type) E, where E is classified as a variable
• E as type, where E is classified as a variable
void Fun() { • E ? E1 : E2, where E1 and E2 are classified as
object ^o = 7; variables
int i = (int) o; • A field: either
} • an access to a instance field or static field
2.8 Object Destruction • an access to an array element
Whenever an owning pointer to an object o is destroyed • A value: any other expression
without transferring ownership elsewhere, then Gel destroys o as
follows. First Gel destroys all fields in o (in reverse lexical order). Gel, like C# and Java, includes both implicit and explicit type
If any of these fields are owning pointers to other objects then conversions. A number of language constructs implicitly convert a
value to a destination type. Explicit type conversions are pointer retains ownership and a new non-owning pointer is
performed by the type cast operator and by a small number of created. The conversion rules disallow conversions which would
other language constructs. inevitably lead to object destruction errors at run time. As an
Every implicit conversion in Gel takes place in one of the example, consider the following method:
following conversion contexts:
Foo Fun() {
• Assignment context: in an assignment V = E, when return new Foo();
implicitly converting from the type of E to the type of V }
• Argument conversion context: in a method call, when The type conversion from Foo ^ to Foo occurs in a return
implicitly converting a method argument to its context, and is hence disallowed. Gel could theoretically allow
corresponding parameter type this type conversion and generate code for the method. Then at
• Return context: when converting the expression in a return method exit the newly allocated Foo object would be destroyed
statement to the method's return type (since it is owned by a temporary owning pointer, and ownership
• Other context: used for a small number of additional hasn't transferred elsewhere). But the destroyed object's non-
conversions owning pointer count would be 1, representing the non-owning
pointer returned from the method, and so the object destruction
We now present Gel's implicit and explicit conversion rules. would yield a run-time error.
(We omit rules relating to conversions between value types, such
as between int and char; these rules are detailed in the Gel
2.10 Erasure
reference manual and are similar to value conversion rules in C#.) Note that Gel adds only two syntactic elements to C#: the type
constructor ^ and the operator take. Suppose that we apply the
2.9.1 Implicit conversions following erasure transformation to a Gel program P:
Every type T is implicitly convertible to itself.
If a type T is derived from a type U, then T is implicitly • We remove every instance of the ^ type constructor: we
convertible to U. replace T ^ with T everywhere.
The null type is implicitly convertible to any reference type or • For each instance take E of the take operator, let T ^ be
owning type. the compile-time type of E. Then we replace take E with
T ^ is implicitly convertible to U ^ if T is implicitly the function call Take(ref E), where Take is defined
convertible to U. as follows:
In an argument conversion or local assignment context, T ^ is
implicitly convertible to U if T is implicitly convertible to U, and
T Take(ref T p) {
either T v = p;
p = null;
• the conversion is in an argument conversion context, or return v;
• the conversion is in an assignment context, and the source }
expression is classified as a variable.
(Informally, this is a function which works like the take
operator but acts on a non-owning type).
Any value type is implicitly convertible to object ^. In an Then the resulting program P' is a valid C# program. If P
argument conversion context, any value type is also implicitly terminates without error, then P' will also terminate without error
convertible to object. These conversions are boxing conversions. and will yield the same value as P.
In a compiled Gel program, a boxing conversion allocates a boxed Note that the converse is not true: it is possible that P' may
object instance. terminate without error, but that P will yield a run-time error. To
string is implicitly convertible to object ^ and to see this, consider the following Gel function:
object .
2.9.2 Explicit conversions int Fun() {
If T is implicitly convertible to U, then T is explicitly Foo ^f = new Foo();
convertible to U. Foo g = f;
f = null;
If type T is derived from a type U, then U is explicitly return 4;
convertible to T. This conversion will fail if the source value is not }
an instance of T.
T ^ is explicitly convertible to U ^ if T is explicitly convertible This function will yield a run-time error in Gel, since at the
to U. moment f is destroyed there is still a non-owning pointer g to the
object and object ^ are explicitly convertible to any same object. But if we apply the erasure transformation to this
value type T; this is an unboxing conversion. This conversion will function, then the resulting C# function will execute without error.
fail if the source value is not actually an instance of T.
object and object ^ are explicitly convertible to
3. PROGRAMMING IN GEL
string. This conversion will fail if the source value is not We have described the Gel language. We must now address
actually a string. two questions: how easy is it to use Gel's type system for writing
real programs, and how well will Gel programs perform? We
2.9.3 Discussion discuss usability in this section and performance in following
Gel never allows a type conversion to a non-owning to an sections.
owning pointer type. A conversion from an owning to a non-
owning pointer is allowed only in certain contexts as per the rules
above; when such a conversion occurs, at run time the source
3.1 Data structures intentionally create a cycle of owning pointers as in the
We believe that many programmers writing in languages such MakeACycle method above, then break the the cycle manually
as C++ write code which frees objects based on ownership when deciding to destroy the list.
relationships between objects. For example, most C++ 3.3 Experience writing the compiler in Gel
programmers have written destructors which free objects pointed We've only written one large program in Gel, namely the Gel
to by the destroyed object. The popular scoped_ptr template class compiler/interpreter itself. In writing the Gel compiler we found
from the Boost ++ library implements the ownership relationship; that most objects had natural owners; ownership works
when a scoped_ptr is destroyed the object it points to is freed particularly well for trees of objects such as the abstract syntax
automatically. In some sense, then, Gel takes the existing informal tree generated by parsing. Certain objects' lifetimes were
notion of object ownership and encodes it into the type system. indefinite, and we generally chose to allocate such objects from
But how easy is it to find a single owner for every object in a pools as described above; we used non-owning pointers to such
program as Gel's type system requires? Fortunately many objects throughout the program.
common data structures such as lists and trees have a natural The Gel implementation includes both a compiler and an
hierarchical structure. Many programs contain hierarchies of interpreter. In implementing the interpreter, we chose to represent
objects linked in parent/child relationships; in these programs, owning pointers in the interpreted language using owning pointers
pointers from parents to children can be owning, and backpointers in the interpreter itself. This means that the interpreter has no code
from children to parents can be non-owning. to free objects in the interpreted program explicitly; instead,
Some data structures are not naturally hierarchical and may in objects in the interpreted program are freed automatically when
fact involve arbitrary pointer graphs. To represent an arbitrary no longer needed. (This is similar to implementing an interpreter
graph, a Gel program can allocate all graph nodes from an array for a garbage-collected language in a garbage-collected language,
whose only purpose is to hold an owning pointer to each node; we in which case the interpreter itself need not concern itself with
call such an array a pool. The program can then use non-owning garbage collection, which "flows through" the interpreter to the
pointers between graph nodes arbitrarily. When the owning pool interpreted program.)
is destroyed, all graph nodes will be destroyed as well. (The Gel The interpreter contains numerous code paths which must
implementation, in fact, includes a built-in type pool which can manipulate values in the interpreted language which might either
be used for allocations of this sort; for details, see the Gel be owned or unowned pointers. To handle this situation, the
language reference.) Note that a C++ program implementing an interpreter code has a class RValue with two subclasses:
arbitrary graph must also generally keep all graph nodes in a list GValue, which holds a value directly, and Reference, which
in order to be able to free each node exactly once. holds a non-owning pointer to a GValue. Code which
manipulates values in the interpreted language generally
3.2 Circular data structures represents them using a variable of type RValue ^. If this
It's possible for a Gel program to create cycles of owning underlying value is owning, the variable holds the underlying
pointers. The following program creates two Bar objects which value directly; if it is not, the variable holds a Reference
point to each other: pointing to the value. This was slightly awkward. We hope that
this particular coding difficulty was an artifact of implementing a
class Bar { Gel interpreter in Gel itself, and that most programs in Gel will
Bar ^next; not need to manipulate values whose ownership status is unknown
static void MakeACycle() { at compile time.
Bar ^n = new Bar();
Bar ^o = new Bar(); 4. REFERENCE COUNT OPTIMIZATION
Bar p = o; As described above, Gel keeps a count of non-owning pointers
n.next = o; to every object. Gel's reference counts are significantly cheaper
p.next = n; than those in a traditional reference-counted system, for three
} reasons:
}
1. Gel doesn't need to keep reference counts for owning pointers;
These objects will never be destroyed. Gel only destroys an object's reference count doesn't change as it is passed from
structures containing no owning pointer cycles; this is a weaker owner to owner. In a traditional reference-counting system, a
guarantee than that provided by a garbage collector, which can reference count is kept for every pointer to an object.
destroy structures containing arbitrary pointer cycles. 2. In Gel, a reference count decrement is simply an integer
But this hardly matters in practical programming. It's hard to decrement, which may well be a single instruction. A
create an owning pointer cycle by mistake in Gel, because the type traditional system must check whether a reference count is
system guarantees that a programmer can't create an owning zero after each decrement; in Gel this is unnecessary because
pointer cycle and return an owning pointer to it. For example, if ownership, not the reference count, determines when it's time
we modify the MakeACycle method above to return a Bar^ and to free an object.
add the line return n; at the end of the method, we receive a 3. The Gel compiler can use ownership information to optimize
compile-time error: we can't return n because ownership has away many reference counts at compile time in a single-
already transferred away from it (in the assignment to p.next). threaded Gel program.
All common data structures can be coded without using
owning pointer cycles. An inherently circular structure such as a
circular linked list is a special case of an arbitrary graph; as This last point deserves more explanation. The Gel compiler
described above, the Gel programmer can allocate all list nodes implements a reference count elimination optimization based on
using a separate owner object and then create the circular links the following idea. Suppose that, for a type T, a given block of
using non-owning pointers. Alternatively, a programmer can code never mutates any pointer of type U ^, where U is T, any
superclass of T, or any subclass of T. Then the code cannot • self-compile: compiles a 73,000-line Gel program
possibly destroy any object which can be held in a variable of type (consisting of 10 copies of the Gel compiler concatenated
T, and therefore any non-owning variable or temporary of type T together, with classes renamed in each copy to avoid
whose lifetime is contained in the block of code need not hold a duplicate class definitions)
reference count.
As an example, consider the following loop, which iterates We've measured the performance of these benchmarks on a
down a linked list, adding the integer values which are found in computer with a 1.83 Ghz Intel Core 2 Duo processor and 2 Gb of
each node of the list: RAM, running Ubuntu Linux 6.10. The C++ benchmarks (and the
C++ code output by the Gel compiler) were compiled using gcc
Node ^first = construct();
int sum = 0; 4.1.2 using the -O2 optimization level. We used the Sun JDK
for (Node n = first ; n != null ; n = 1.5.0_08 to compile and run the Java benchmarks; we used the
n.next) "server" (not "client") VM from the JDK. We compiled and ran
sum += n.i; the C# benchmarks using version 1.1.17.1 of Mono [22], an open-
source implementation of C#. Note that Mono uses the Boehm
A traditional reference-counted implementation would garbage collector [21] .
increment and then decrement the reference count of each node in For each benchmark run, we collected the following statistics:
the list as it is pointed to by the loop variable n. The Gel compiler
can optimize these reference counts away: the variable n is live • CPU time (including both user and system time) in seconds.
only during the loop execution, and no code in the loop mutates a • Maximum virtual memory. This includes all pages mapped
owning pointer of type T ^, where T is Node or any superclass or into the process's address space, including those not
subclass of it (indeed, the code mutates no owning pointers at all.) resident in physical memory, and including both private and
And so no Node object can possibly be destroyed during the loop. shared pages.
The compiler implements this optimization interprocedurally. • Maximum resident memory. This includes only pages
Suppose that we modify the loop body above to call n.Get(), resident in physical memory. (Both private and shared pages
where the Get() method merely returns the instance variable i. are included.)
Then the optimization still holds, since the compiler recognizes • Maximum private writeable virtual memory. This includes
that the Get() method also mutates no owning pointers of type only pages which are private to the process and are
Node. writeable. (Both resident and non-resident pages are
To implement this optimization, the compiler generates a call included.)
graph of all methods in the program being compiled. Using this
graph, the compiler computes, for each method, the set of owning We collected the virtual memory statistics using ptime, a
types which may be mutated during a call to M. When compiling program we wrote specifically for Linux; its source code is
each method, the compiler generates a graph of control flow in the available on the project site.
method. For each non-owning local variable (or temporary value Here are the benchmark results; a discussion follows below.
in a expression), the compiler examines the control flow nodes
where the variable is live and generates the set of owning types Benchmark: sort 5
which may be mutated during the variable's lifetime, including
types which may be mutated during method calls invoked from CPU (sec) virtual (Mb) resident (Mb) private (Mb)
those nodes. If none of these types are a supertype or subtype of C++ 7.1 18.3 16.5 15.9
the variable's own type, the compiler does not bother to count
references from the variable to any object. Java 5.2 709.5 74.7 651.4
5. BENCHMARK PERFORMANCE C# 3.3 42.3 27.4 30.4
To compare Gel's performance with that of languages using
manual memory management and languages using garbage Gel 4.8 18.3 16.6 15.9
collection, we've implemented several small benchmark programs
in C++, Java, C# and Gel. The source code to these benchmarks is
available at the Gel project site. The benchmarks include the Benchmark: sortstring 5
following:
CPU (sec) virtual (Mb) resident (Mb) private (Mb)
• sort: on each of a series of iterations, generates a list of C++ 4.4 25.4 23.6 23.0
1,000,000 randomly generated integers and sorts them using
a recursive mergesort algorithm. Java 6.5 708.0 127.8 650.8
• sortstring: like the sort benchmark, but uses a list of
C# 6.8 50.8 33.5 38.9
400,000 randomly generated strings.
• binarytrees: allocates and frees a large number of Gel 5.1 34.8 33.0 32.4
binary trees of various sizes. This program is from the
Computer Language Shootout Benchmarks at
http://shootout.alioth.debian.org/.

We have implemented the Gel compiler both in C# and in Gel,


so we use the compiler itself as an additional, larger benchmark:
Benchmark: binarytrees 18 In these benchmarks the optimizer eliminates 89% to 100% of
reference count increments, speeding execution by 10% to 35%.
CPU (sec) virtual (Mb) resident (Mb) private (Mb) Interestingly, in the binarytrees benchmark the optimizer is
C++ 11.4 27.2 25.4 24.9 able to eliminate every reference count in the program.

Java 3.6 707.4 92.6 650.2


6. PROPOSED EXTENSIONS
In this section we sketch a number of possible extensions to
C# 17.6 76.2 65.9 64.3 the Gel language. We have not implemented any of these.

Gel 17.8 27.2 25.5 24.9 6.1 Object Destruction


Gel's object destruction mechanism as outlined above has a
number of limitations:

Benchmark: self-compile • Gel does not let the programmer define a destructor for a
CPU (sec) virtual (Mb) resident (Mb) private (Mb) class, i.e. a method which will run when an instance of the
class is destroyed.
C# 5.3 80.2 67.4 66.5 • Gel can automatically destroy structures in which each
internal non-owning pointer points to an ancestor in the
Gel 5.6 68.7 66.8 65.6 ownership tree, but cannot automatically destroy structures
containing arbitrary non-owning pointers to internal nodes.
Memory usage was remarkably consistent across the • Object destruction in Gel may consume a large amount of
benchmarks. For every memory statistic in every benchmark, Java stack space since it is inherently recursive.
consumed the most memory, followed by C#, followed by Gel,
followed by C++. In every benchmark except for sortstring, To overcome these limitations, we propose adding destructors
Gel's memory consumption was only slightly greater than C++'s. to Gel, and propose a multi-phase object destruction mechanism.
In the sortstring benchmark Gel used about 40% memory Gel could destroy an object graph rooted at a pointer P in the
than C++; this is presumably because Gel represents a string using following phases.
a structure containing a reference count and a character pointer,
whereas the C++ version of the benchmark represents a strings 1. Recursively descend all objects in the ownership tree
using a character pointer alone. In this benchmark Gel below P, running each object's destructor.
nevertheless used significantly less virtual memory than C# and 2. Recursively descend again; for each non-owning pointer
significantly less physical memory than Java. in every object, decrement the reference count of the
CPU usage was not nearly so consistent across benchmarks. In object pointed to.
two benchmarks C++ was faster than both Java and C#; in one 3. Recursively descend again; for each object, check that
benchmark both Java and C# were faster; and in one benchmark its reference count is zero and free the object.
(binary-trees) Java was, surprisingly, over twice as fast as
C++, but C# was much slower. Gel's performance relative to C++
In many cases the compiler should be able to optimize away
also varied from benchmark to benchmark. In sortstring Gel was
some or all of the work to be performed in the first and/or second
about 15% slower; in binary-trees Gel was about 70%
phases. In particular, using static type analysis it will often be
slower. In the sort benchmark Gel was, oddly, faster than C++;
possible to determine that all objects in the graph below an object
this is surprising because the Gel compiler generates C++ code.
of type T will never have any destructors or non-owning pointers;
Perhaps the code generated by the Gel compiler triggers some
in that case the first or second phases need not run at all below
optimization which doesn't occur when the C++ version is
such objects. As a simple example, consider a singly linked list in
compiled; we hope to have an explanation for this by the time the
which each node contains only an integer and an owning pointer
final version of this paper is published.
to the next node; then the first and second phases can be optimized
5.1.1 Effectiveness of reference count elimination away entirely in destroying a list of this type.
To measure the effect of Gel's reference count optimizer, we Note also that in many structures each node contains only a
instrumented our benchmark programs to report the number of single owning pointer; in such structures, the compiler can save
reference count increments which occur in each benchmark run. stack space by eliminating tail recursion in generating the code to
The benchmark results follow: perform the recursive descent in each phase. With these
Effect of reference count optimization optimizations in place, we believe that this generalized destruction
algorithm might outperform the algorithm in the existing Gel
with optimization without optimization
benchmark implementation in many cases.
cpu (sec) incr ops cpu (sec) incr ops User-written destructors might potentially mutate the graph of
objects being destroyed. If a destructor inserts objects into the
sort 4.8 93.4M 5.9 868.2M graph, then these objects might be destroyed in phases 2 and 3
even though their destructors have never run. Similarly, if a
sortstring 5.1 34.7M 5.7 326.6M destructor removes objects whose destructors have already run,
binarytrees 17.8 0 20.2 471.1M then those objects' destructors might run again when they are
destroyed at a later time. This possibility does not compromise the
self-compile 5.6 13.0M 6.9 225.7M safety of the language, but does mean that each object's destructor
is not guaranteed to run exactly once. It might be possible to
detect destructor-induced mutations in some way and report an
error if they occur; we will not discuss this possibility further 7. RELATED WORK
here. Our work represents one of many attempts to find a "sweet
6.2 Polymorphism spot" in the trade-off space between fully manual and fully
The Gel type system does not support any sort of automatic memory management (that is, garbage collection).
polymorphism between owning and non-owning types. This There are many inter-related issues, chief among them being
means that it's not possible to write a method which can receive memory footprint, run-time cost, determinism, and ease of
either an owning or non-owning pointer as an input parameter. It's programming. We compare our work to that of others across these
also not possible to implement a single data structure (such as a various axes.
tree or hash table) which can hold either owning or non-owning Region-based memory management [15] provides bulk-
pointer. This limitation is reflected in the Gel class library, which freeing of objects, often based on a stack discipline. Regions
contains separate ArrayList and NonOwningArrayList generally provide low cost and good determinism, but if provided
classes. in a safe manner either require either burdensome annotation [19]
It would be useful to extend Gel with generic types as found or an analysis which is frequently over-conservative [2]. Region
in Java and C#, allowing a generic type to represent either an systems that follow a stack discipline often suffer from large
owning or non-owning type. As a simple example, suppose that numbers of object being assigned (by analysis or by the
we'd like to implement a class Pair<T> which holds two objects programmer) to the global region which is never collected (or
of type T. We'd like to be able to instantiate the class using either must be collected by a supplementary garbage collector).
an owning or non-owning pointer type. Our generic type system The Real-time Specification for Java [3] provides a stack-
might allow this as follows: oriented region-based memory management based on scopes.
Scopes have neither annotations nor automatic inference; instead
class Pair<T> { it is the responsibility of the programmer to allocate objects in the
T first, second; correct scope. Storing a pointer that violates the stack discipline of
Pair(T first, T second) { scopes causes a run-time exception. Scopes are thus type-safe but
this.first = first; subject to run-time exceptions that are difficult to prevent and are
this.second = second;
} highly dependent on interactions between different components.
T% First() { return first; } In addition, it is often either convenient or necessary to place
T% Second() { return second; } objects in the outermost scope (called Immortal memory), in
} particular when it is necessary to share objects between scope-
managed and garbage-collected memory.
The system includes a type operator % which can be applied Gay and Aiken [12] suggest the use of reference counted
only to a generic type, and which removes ownership from a type. regions that need not follow a stack discipline. Safety is ensured
In particular, if T is Foo ^, then T% is Foo; if T is Foo, then T% is because explicit freeing is not allowed to succeed unless the
also Foo. region's reference count has dropped to zero. They include
A generic type system for Gel should also support covariance: annotations for specifying that pointers are in the same region or
it should be possible to write a single function which can take are pointers to parent regions; updates of such pointers do not
either a Pair<Foo> or a Pair<Foo ^> as a parameter, and change reference counts. Although the granularity and abstraction
similarly it should be possible to write a single function which can level are different from ours, these annotations also allow them to
take either a Foo[] or a Foo^[] as a parameter. optimize away a large fraction of reference counting operations.
We believe that it should not be difficult to design a complete The goals of the Cyclone language [17] are probably most
generics system for Gel including covariance, but we will not similar to those of Gel; its goal is to provide a safe alternative to C
explore the idea further in this work. for systems programming. Cyclone provides several forms of safe
6.3 Multithreading memory management: regions, unique pointers, and reference
As described above, the Gel compiler's reference count counting. Cyclone's regions require explicit annotations in the
elimination optimization will work only in single-threaded source code, and can be both lexically scoped (in the style of
programs; this is a significant limitation. [Tofte 1]) or dynamically scoped. In the latter case, if a region is
It would be useful to extend Gel to be able to execute freed dynamically subsequent attempts to access objects in that
multithreaded programs safely and efficiently. We can imagine region will generate a run-time exception.
two different possible approaches here. First,it might be possible Cyclone's unique pointers are based on linear types [13] and
to extend Gel's optimizer to be able to infer that many data are essentially like Gel's owning pointers, but without the
structures are accessed only from a single thread; then reference capability to reference count non-owning references. Some
counts in such structures could be optimized away using the polymorphism across memory management styles is provided by
existing algorithm. Such inference seems difficult and we are allowing sets of unique pointers to be temporarily treated as
relatively pessimistic about such an approach. though they belong to a region. Cyclone also provides explicit (but
As another approach, we could possibly extend Gel so that safe) reference counting; reference counted objects are treated as
objects are grouped into apartments which may be accessed from belonging to a special region called RC.
only a single thread at once. With this approach, the existing The experience of the language designers in writing the
optimization algorithm could be used to eliminate reference count Cyclone compiler is instructive: first coarse-grained regions were
accesses in code which executes inside a single apartment. It used, which reduced the annotation burden. However, coarse
might be possible to design system which guarantees that only regions did not free memory soon enough and led to excessive
pointers of a certain kind may cross apartment boundaries and space consumption. The compiler was then rewritten using fine-
which allows groups of objects to move efficiently between grained regions, at which point the annotations became overly
apartments; we leave this to further work. cumbersome. Eventually the compiler was rewritten to use
garbage collection [Greg Morrisett, personal communication].
Ownership types have received considerable attention in [5] Boyapati, C., Liskov, B., and Shrira, L. Ownership types for 
recent years. Unlike Gel's concept of ownership, these approaches object encapsulation. In Proceedings of the 30th ACM  
rely entirely on the type system [5] [7] [9]. Ownership is used for SIGPLAN­SIGACT Symposium on Principles of  
a variety of purposes: modularity [8], avoidance of run-time Programming Languages (New Orleans, Louisiana, USA, 
errors, improved alias analysis and optimization [9], prevention of January 15 ­ 17, 2003). ACM Press, 213­223.
data races and deadlocks [4], and real-time memory management
[6] [20]. Generally speaking, these approaches have a high [6] Boyapati, C., Salcianu, A., Beebee, W., and Rinard, M. 
annotation overhead and/or require significant redundant code due Ownership types for safe region­based memory management 
to the coupling of types to ownership contexts. Clarke and in real­time Java. In Proceedings of the ACM SIGPLAN 2003 
Wrigstad [10] also point out that ownership types often require Conference on Programming Language Design and  
external interface changes in response to purely internal changes Implementation (San Diego, California, USA, June 09 ­ 11, 
in data structures; they attempted to alleviate this problem using a 2003). ACM Press, 324­337.
combination of unique pointers and ownership types, but the
resulting language still suffers a high annotation burden. [7] Boyland, J. Alias burying: unique variables without 
Fahndrich and DeLine [11] suggest adoption and focus as destructive reads. Softw. Pract. Exper. 31, 6 (May. 2001), 
constructs for incorporating linear types with an abiity to 533­553.
temporarily create and relinquish aliases, and successfully applied [8] Clarke, D. G., Noble, J., and Potter, J. Simple Ownership 
it to the writing of low-level systems code. This provides very Types for Object Containment. In Proceedings of the 15th 
strong static information, but requires significant and not always European Conference on Object­Oriented Programming 
intuitive annotation.
(June 18 ­ 22, 2001). Springer­Verlag, 53­76.
Real-time garbage collection [1] [16] provides overall timing
determinism and ease of programming, but suffers from the [9] Clarke, D. G., Potter, J. M., and Noble, J. Ownership types 
memory overhead issues associated with garbage collection in for flexible alias protection. In Proceedings of the 13th ACM 
general, and does not provide determinism in terms of the times at SIGPLAN Conference on Object­Oriented Programming,  
which individual objects are freed. Also, during garbage collection Systems, Languages, and Applications (Vancouver, British 
some fraction of the CPU time (on the order of 30%) is Columbia, Canada, October 18 ­ 22, 1998). ACM Press, 48­
unavailable to real-time tasks. 64.
[10] Clarke, D., and Wrigstad, T. External uniqueness is unique 
8. ACKNOWLEDGEMENTS enough. In European Conference on Object­Oriented  
We'd like to thank Linus Upson for his invaluable feedback and Programming (ECOOP). , 176–200.
encouragement. Discussions with Paul Haahr were also useful.
Feng Qian ported the Gel compiler from Windows to Linux. Many [11] Fahndrich, M. and DeLine, R. Adoption and focus: practical 
thanks are due to Google for allowing us to release the Gel linear types for imperative programming. In Proceedings of  
compiler as open source. the ACM SIGPLAN 2002 Conference on Programming  
Language Design and Implementation (Berlin, Germany, 
9. REFERENCES June 17 ­ 19, 2002). ACM Press, 13­24.
[12] Gay, D. and Aiken, A. Language support for regions. In 
[1] Bacon, D. F., Cheng, P., and Rajan, V. T. A real­time garbage  Proceedings of the ACM SIGPLAN 2001 Conference on  
collector with low overhead and consistent utilization. In  Programming Language Design and Implementation 
Proceedings of the 30th ACM SIGPLAN­SIGACT Symposium   (Snowbird, Utah, United States). ACM Press, 70­80.
on Principles of Programming Languages (New Orleans, 
Louisiana, USA, January 15 ­ 17, 2003). ACM Press, 285­ [13] Girard, J. Linear logic. Theoretical Computer Science 50:1 
298. (1987), 1­102.

[2] Birkedal, L., Tofte, M., and Vejlstrup, M. From region  [14] Hejlsberg, A., Wiltamuth, S., and Golde, P. The C#  


inference to von Neumann machines via region  Programming Language. Addison­Wesley, 2004.
representation inference. In Proceedings of the 23rd ACM  [15] Henglein, F., Makholm, H., and Niss, H. Effect types and 
SIGPLAN­SIGACT Symposium on Principles of  region­based memory management. In Pierce, B. ed. 
Programming Languages (St. Petersburg Beach, Florida,  Advanced Topics in Types and Programming Languages, 
United States, January 21 ­ 24, 1996). ACM Press, 171­183. MIT Press, 2005, 87­136.
[3] Bollella, G., Gosling, J., Brosgol, B. M., Dibble, P., Furr, S.,  [16] Henriksson, R. Scheduling Garbage Collection in Embedded  
Hardin, D., and Turnbull, M. The Real­Time Specification for  Systems. Ph.D. Thesis, Department of Computer Science, 
Java. Addison­Wesley, 2000. Lund University, September 1998.
[4] Boyapati, C., Lee, R., and Rinard, M. Ownership types for  [17] Hicks, M., Morrisett, G., Grossman, D., and Jim, T. 
safe programming: preventing data races and deadlocks. In  Experience with safe manual memory­management in 
Proceedings of the 17th ACM SIGPLAN Conference on   Cyclone. In Proceedings of the 4th international Symposium  
Object­Oriented Programming, Systems, Languages, and   on Memory Management (Vancouver, BC, Canada, October 
Applications (Seattle, Washington, USA, November 04 ­ 08,  24 ­ 25, 2004). ACM Press, 73­84.
2002). ACM Press, 211­230.
[18] Jones, R., and Lins, R. Garbage Collection: Algorithms for  tail.next = top;
Automatic Dynamic Memory Management. John Wiley &  tail = tail.next;
}
Sons, 1996. }
[19] Tofte, M. and Talpin, J. Implementation of the typed call­by­ Node ^rest = (a == null) ? b : a;
if (tail == null)
value λ­calculus using a stack of regions. In Proceedings of   return rest;
the 21st ACM SIGPLAN­SIGACT Symposium on Principles   tail.next = rest;
of Programming Languages (Portland, Oregon, United States,  return head;
January 16 ­ 19, 1994). ACM Press, 188­201. }
[20] Zhao, T., Noble, J., and Vitek, J. Scoped Types for Real­Time  static Node ^MergeSort(Node ^a) {
Java. In Proceedings of the 25th IEEE international Real­ if (a == null || a.next == null) return a;
Time Systems Symposium (Rtss'04) ­ Volume 00 (December  Node c = a;
Node b = c.next;
05 ­ 08, 2004). IEEE Computer Society, 241­251. while (b != null && b.next != null) {
[21]  http://www.hpl.hp.com/personal/Hans_Boehm/gc/ c = c.next;
b = b.next.next;
[22]  http://www.mono­project.com/ }
Node ^d = take c.next;
[23]  http://code.google.com/p/gel2/source return Merge(MergeSort(a), MergeSort(d));
}
[24]  
http://gel2.googlecode.com/svn/trunk/doc/gel2_language.htm public static void Main(String[] args) {
l Node ^n = RandomList(1000000);
n = MergeSort(n);
10. APPENDIX: SAMPLE PROGRAM while (n != null) {
Node next = n.next;
The following Gel program generates a list of 1,000,000 random if (next != null && n.i > next.i) {
integers, sorts them using a recursive mergesort and then verifies Console.WriteLine("failed");
that the resulting list is in sorted order. return;
}
n = take n.next;
class Random { }
static int r = 1; Console.WriteLine("succeeded");
public static int Next() { }
r = r * 69069; }
return r;
}
}
class Node {
public readonly int i;
public Node ^next;
public Node(int i, Node ^next) {
this.i = i;
this.next = next;
}
}
class Sort {
static Node ^RandomList(int count) {
Node ^first = null;
for (int x = 0 ; x < count ; ++x)
first = new Node(Random.Next(), first);
return first;
}
static Node ^Merge(Node ^a, Node ^b) {
Node ^head = null;
Node tail = null;
while (a != null && b != null) {
Node ^top;
if (a.i < b.i) {
top = a;
a = take top.next;
} else {
top = b;
b = take top.next;
}
if (head == null) {
head = top;
tail = head;
} else {

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy