ERights Home e-impls / e-on-c 
Back to: ENative: Ground Rules On to: ENative: Compiling Variables

Fat Pointers


An object is classically considered to be a combination of state and behavior, and this perspective is directly reflected in the Fat Pointer implementation technique. A Fat Pointer designates an E object, and is composed of two pointer-sized machine words rather than one. The first word is always the "pointer-to-Script". The Script holds the various pointers-to-C-functions that constitute the E object's behavior. The second word is data to be interpreted according to the above Script.

Fat Pointers can be seen as a unification of the "tagged pointer" technique common to dynamic symbolic language implementations, such as Lisps and Smalltalks, and the "vtable" (or "class pointer") technique common to object languages with run-time polymorphism, such as Smalltalks and C++. In addition, Fat Pointers more directly accomodate multi-faceted objects, an essential programming style for capability-based computation.

For a simple language implementation on the scale of ENative or Python, it isn't clear whether Fat Pointers are a net performance win. We haven't measured, but our best guess is that they cause code to run somewhat faster, but data to take more memory. Both effects should be less than a factor of two.

Boxed and Unboxed Data

When the data word holds the entirety of the E object's state information, we say the E object is unboxed. When the data word is interpreted as a pointer to the state information, we say the E object is boxed. Boxed state is typically heap-allocated.

  • For an object defined in the E language, the data word would be a pointer to a heap-allocated array of Fat Pointers for the instance variables. This array is the designated E object's state. In the ENative implementation, objects written in the E language are always boxed.

  • For a primitive scalar object that fits in one machine word, like a boolean or a char (a Unicode character), the data word is the data, and the Script knows how to interpret that data. Such scalars are unboxed.

  • For primitives that don't fit into a machine word (like E's "float64", a 64-bit IEEE double precision floating point number, assumed for now to correspond to C++'s "double"), primitive E Lists, or primitimes that are already heap allocated (like C's "FILE *"), the data word is a pointer to the state. Such primitives are boxed. Primitive E Lists of fixed-size scalars, will be implemented by pointing at a heap-allocated C++ array of this data type with no further fat-pointers or boxing. For example, an E ConstList of float64s would be designated by a pair of a Float64ConstListScript and a pointer to a heap-allocated C++ array of double.

    Because float64s are boxed, and E provides no smaller floating-point data type, floating point code written in E and run on ENative will perform abysmally. (Though note that Python also boxes floating point arithmetic, and people don't scream too much.) We hope that floating point code that needs to be high speed will typically be in the form of separable loops stepping over Lists of float64s. These could then be implemented manually as C++ native code running over C++ arrays of double.

  • For Integers, since these are typically small enough to be unboxed, we split them into two cases. Assuming a 32-bit word, when the Integer can be represented as a 31-bit two's complement number, we designate the Integer using the SmallInteger Script paired with a data word holding the number directly. Otherwise, we pair the (not yet written) BigInteger Script with a pointer to the heap-allocated integer data, probably as an array of 32-bit words. We use only 31 bits of the unboxed representation, rather than the available 32 bits, since C++ gives us no portable way to access the carry or overflow bits. Rather, we check for overflow by seeing if the two most significant bits are different. Where did this technique originate? I'd like to give credit where due.

Historical Analogs

In distinguishing between the kinds of scalars, and to distinguish whether something is scalar, the Fat Pointer technique clearly corresponds to traditional (Lisps, Smalltalks) tagged pointers. Instead of trying to squeeze a few extra bits into a single machine word, and masking them off to get the data, we use a full word each for the tag and the data. This allows the tag (the Script pointer) to distinguish among vastly more cases, and allows access to both tag and data without masking. Of course, the price we pay is that our pointers are twice as big. As we will see, this less than doubles total memory use.

Object languages with run-time polymorphism (Smalltalk, C++) are normally implemented by a heap-allocated record holding both the instance variable state and a pointer to a data structure (vtable or class pointer) representing the shared behavior of all instances of the same class. Since our Fat Pointer's tag-equivalent is now a full word, it can point at this behavior directly, freeing the object's state record from doing so itself. In this analogy, the Script pointer acts like the vtable pointer.

Without static type analysis, a conventional vtable-based implementation would always invoke an object by a double indirection: one to get to the object record, and one to get to the vtable. To an object's clients, the vtable is farther away than the data, but the client only ever invokes the vtable, leaving it to the behavior to deal with the data. The Fat Pointer technique saves us both the storage of the vtable pointer in the object record, and the extra indirection. Better, it saves us an indirection in the fast-path, which is where it counts.

On modern processors, it can be more informative to count expected cache-line faults than supposed memory accesses. If we can arrange for our Fat Pointers to be 64-bit aligned, then a single cache-line fault will bring both halves of a Fat Pointer into the data cache, whereas the vtable technique requires two faults for the same job. On the other hand, fewer Fat Pointers fit into a cache line, so the net effect is unclear.

Facets and Fat Pointers

In E, when multiple object expressions appear in the same scope, the resulting objects share (subsets of) the same state, but each implements a different behavior using that state. In the getter & setter example, the shared state is the variable named "value". ENative implements this situation by having a single heap-allocated state-array per instantiation of such a composite, rather than a separate heap-allocated record per object. The Fat Pointer designating the getter object pairs the Script representing the getter behavior with the array representing this instantiation of the composite's state. Similarly, the Fat Pointer designating setter object pairs the setter Script with this same state.

A vtable-based implementation would be unable to achieve this economy without resorting to complex techniques, such as that used by C++ multiple inheritance. (Note: the Orbit compiler does this kind of aggregation, but it doesn't look ugly there.) Without resorting to these techniques, a vtable-based implementation would place the vtable into the object-record rather than the pointer. To associate the same state with multiple vtables would require allocating multiple objects.

By pairing different behavior with a common state, these two objects also provide differing authority over common state. In capability programming style, one often needs to define and hand out differing authority to common state.

Remember that old adage, "an object is a combination of state and behavior"? Conventional object-oriented multiple-instatiation shows the flexibility of combining multiple states with same behavior. Facets show the flexibility of combining multiple behaviors with the same state. Our getter/setter is an example of a multiply-instatiated multiple-facet composite, and so does both in combination. Fat Pointers, by directly reflecting the truth of this adage, supports this implied flexibility of object programming in a simple and direct manner. At level of ambition of implementations this simple, they also support this flexibility is an efficient manner.

 

 
Unless stated otherwise, all text on this page which is either unattributed or by Mark S. Miller is hereby placed in the public domain.
ERights Home e-impls / e-on-c 
Back to: ENative: Ground Rules On to: ENative: Compiling Variables
Download    FAQ    API    Mail Archive    Donate

report bug (including invalid html)

Golden Key Campaign Blue Ribbon Campaign