Object model#
The Object Model is the main difference that allows Skybison to be faster than
CPython. In CPython, every object is a heap-allocated PyObject* -- and to
speed this up, they use free lists
to avoid frequent malloc/free and have some static objects for small integers.
(You can and should read more about the CPython Data
Model when you get a
chance).
Skybison is making this more efficient by having immediate types. All managed
objects are represented by RawObject, which is a piece of memory of word
length. It can either directly include the actual data (immediate objects),
or point somewhere on the heap where the data is located (heap-allocated
objects).
NOTE: This document was written during early project planning stages. Parts of it may be out of date.
Object tagging#
The least significant bits of uword RawObject are used as a tag to determines
the value type. At the conceptual level, we have two stages for resolving tags:
a 3-bit low tag and a 5-bit high tag. This separation is only conceptual; in
the code, we check against the full 5-bit tags using prefix masks.
3-bit Tags#
**0-- Small integers -- all bits except the last are used for the actual value001-- Heap-allocated objects -- the value of RawObject minus 1 is the actual address011-- Header (a special internal type, see below)101-- Small sequence types and Error -- escape into the 8-bit tag111-- Immediate objects -- escape into the 8-bit tag
5-bit Tags (101)#
00101-- Small bytes -- the 3 next least significant bits are used for the length -- the remaining bytes of the value are used to store the actual bytes in little endian order01101-- Small string -- the 3 next least significant bits are used for the length -- the remaining bytes of the value are used to store the actual string in little endian order10101-- Error -- the 3 next least significant bits store a tag indicating the kind of Error. SeeErrorKindinruntime/objects.h. Remaining bits are all0.
5-bit Tags (111)#
00111-- Bool -- the next least significant bit is the value, true/false -- remaining bits are all001111-- NotImplemented -- remaining bits are all010111-- Unbound -- remaining bits are all011111-- None -- remaining bits are all1-- this is so we canmemset(obj.address(), -1, sizeof(obj))to fill a heap-allocated object withNone
With this design, many frequent objects are stored as immediate values and many operations don't need the extra indirection.
Why this tagging scheme? Having small integers tagged with an extra zero at the
end makes it possible to use many arithmetic instructions without additional
work. To sum up two small integers, you just add the two RawObjects and the
result is automatically the right small integer -- you just need to check for
overflow. For heap-allocated objects, memory address always ends with 00
because of default memory alignment, so we don't lose any bits by adding this
tag. Dereferencing does not have any extra overhead because all modern CPUs can
support dereferencing with offset.
Heap-allocated objects#
The first word of every heap-allocated object contains the Header - a special structure with metadata about the object. Headers are tagged in the same way as other objects, which makes it easier for the GC to identify the start of an object when scanning the heap. The metadata includes type identification (layout id) and object hash.
The rest of the allocated memory are attributes. A few object have immediate
attributes (Float has an attribute of type double, LargeStr contains a
byte array), but most attributes are simply other RawObjects.
While small integers and small strings can be stored as immediate objects,
heap-allocated counterparts are needed for larger values. LargeStr is
basically a byte array with a header; LargeInt is an array of digits of word
length, representing the number in two's-complement form (again, with a
header). Special abstract types Int and Str are needed to hide this
implementation detail -- managed code needs to work with normal int and str
types.
Attribute access#
TODO: document
Subtypes#
TODO: document