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.
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.
**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
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. SeeErrorKind
inruntime/objects.h
. Remaining bits are all0
.
00111
-- Bool -- the next least significant bit is the value, true/false -- remaining bits are all0
01111
-- NotImplemented -- remaining bits are all0
10111
-- Unbound -- remaining bits are all0
11111
-- 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.
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 RawObject
s.
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.
TODO: document
TODO: document