The JET project is based on a dataflow engine, which needs to run on a variety of hardware, in particular fairly low-end µCs.
But dataflow requires passing messages around, and I want these messages to support a variety of data structures, from simple integer values to small vectors, and preferably also strings. The messages will have varying sizes and they need to be produced and consumed dynamically, with different recipients and lifetimes. That’s not trivial in the limited-RAM context of a µC.
This post describes a design I’m currently working on, aimed at minimal RAM use, while keeping the code simple, I hope.
The idea is to use chunked memory: all the data, including vectors and strings, is kept in small chunks, allocated from a (fixed-size) pool. Larger vectors and strings are chained together through a“next” field:
The next field at the end of the chain is special: it’s a small int indicating how much of that last chunk is used. With chunks of 8 bytes, these values can be 0..8, with 9 and up being the first “valid” next chunk index. In other words, chunks 0..8 are never used as part of allocated memory.
Chunks (or more precisely: chunk chains) have a type, for which I’ve reserved 3 bits. For now, the types are OBJ/STR/INT/FLT:
- OBJ - this is a vector of 2-byte “values” (see below)
- STR - this is a 0-terminated string
- INT - a 32-bit signed int
- FLT - a 64-bit double float (maybe)
There can be at most ≈ 4000 chunks, so a chunk index only needs at most 12 bits. When using 8-byte chunks, the memory pool size can be at most roughly 32 KB.
A value is 2 bytes, and consists of a 15-bit signed integer plus a 1-bit flag. When the flag is zero, the int 15 bits are used as small inline int. When the flag is set the 15 bits are used as chunk index.
So an OBJ is simply a vector of arbitrary items, consisting of ints, doubles, strings, and other objects: objects can be nested!
Note that small integers (-16384..+16383) require no extra chunk in an object.
The last important aspect of this design is that all memory is managed with garbage collection. This allows passing around structured messages which will clean up after themselves once no longer referenced.
The garbage collector usesmark & sweep, which requires little code and just a single “mark” bit per chunk during GC.
Memory is split into two separate arrays, chunks and “tags”. Chunks are for the actual data, and tags are for the rest. They are both indexed in the same way:
With the tags containing all the “metadata” for each chunk, as a packed 16-bit entry:
So although physically this information is stored in two different places,logically they belong together as entries of 10 bytes each. The garbage collector only needs the tags.
There are many design trade-offs, as usual:
- with a storage granularity of 8 bytes, other sizes will waste 1..7 bytes
- there is no memory fragmentation, or need to move / copy / coalesce areas
- determining the exact object size requires traversal to the end of the chain to pick up the final “next” field
- accesses beyond the first 4 items of an object require traversing the chain
- chunk sizes could easily be changed to 16, 32, or even more bytes
- with larger tags, more memory could be managed with the same design
I’ve prototyped a basic version in C (see this
gist), to see how
it feels and what the edge cases are. This code takes text files as args, and“parses” each line into a data structure. Objects, i.e. vectors, are delimited
with “[
” and “]
” (each with surrounding spaces). So far, things are looking
good.
I’m currently looking into supporting a mix of RAM- and ROM-based objects (treating negative indices inside values as references to (non-chunked?) items in flash memory.
Ok, ok, so there may be some masochism involved in picking a limited µC context and then trying to implement “big” ideas in it, but it’s also a great way to keep pushing for maximum simplicity and squeezing out all redundancy.Limited RAM is fun!