diff options
| author | Taylan Kammer <taylan.kammer@gmail.com> | 2026-08-20 14:12:09 +0200 |
|---|---|---|
| committer | Taylan Kammer <taylan.kammer@gmail.com> | 2026-08-20 14:12:09 +0200 |
| commit | b233a858b800a96ac71d357570b4e72dc5519d34 (patch) | |
| tree | 7f78ec9037f9a990b7ccae3f42c4729b842f59ec | |
| parent | 916bb5b995f1347cb736475982159f2c9bda8268 (diff) | |
Doc refinement.
| -rw-r--r-- | doc/0/A-meta_alloc.md | 152 |
1 files changed, 81 insertions, 71 deletions
diff --git a/doc/0/A-meta_alloc.md b/doc/0/A-meta_alloc.md index e339db7..51f3ff7 100644 --- a/doc/0/A-meta_alloc.md +++ b/doc/0/A-meta_alloc.md @@ -34,32 +34,32 @@ heap memory returned to application code; it's up to the GC. The entire architecture is built on three pillars: 1. The actual heap memory acquired through `mmap()` which is a single - contiguous block divided into N equally sized slabs; one per size - class. + contiguous block divided into N equally sized Slabs; one per Size + Class. 2. A global array of N metadata structures residing in static memory. 3. A per-thread array of N metadata structures in TLS memory. -Note that the slab array is a *virtual memory* block. The operating +Note that the Slab array is a *virtual memory* block. The operating system automatically maps physical memory to it as application code writes data into it. Allocating one massive block of virtual memory in advance and letting the operating system automatically handle the actual mapping to physical memory provides an immense simplification of the allocator implementation. -## Slabs and size classes +## Slabs and Size Classes A single global pointer in static memory is initialized to point to -the massive block of slab virtual memory. +the massive block of Slab virtual memory. This initialization happens by calling the `init()` function once. This must be done before any additional threads are spawned that intend to interact with Meta Alloc. -The vmem block is divided in `N_SLABS` equal slabs, each divided into -`SLAB_LENGTH` many equal `Unit`s of memory, which are addressed via -*indexes* into slabs. Expressed in pseudo-code: +The vmem block is divided in `N_SLABS` equal Slabs, each divided into +`SLAB_LENGTH` many equal `Unit` elements of memory, addressed through +*Indexes* into Slabs. Expressed in pseudo-code: var slabs: *[N_SLABS][SLAB_LENGTH]Unit = undefined; @@ -67,20 +67,21 @@ The vmem block is divided in `N_SLABS` equal slabs, each divided into slabs = mmap(N_SLABS * SLAB_LENGTH * sizeof(Unit)); } -The division of slabs into `Unit`s is simply to allow expressing the -address of an allocation through an index value, such as one fitting -into 32 bits, rather than a full pointer value. +The division of Slabs into Units is simply to allow the expression of +addresses, offsets, etc. through an Index value, such as one fitting +into 32 bits, rather than full pointer-sized values. -Every slab is for a specific *size class*. Since the slabs are equal -sized, this means that for larger size classes there is a lower bound +Every Slab is for a specific *Size Class*. Since the Slabs are equal +sized, this means that for larger Size Classes there is a lower bound of the maximum number of allocations that can be made of that class. -The actual blocks of memory returned are called *slots* which consist -of a multiple of `Unit`s depending on the size class. +The actual blocks of memory returned are called *Slots* which consist +of a variable number of Units, depending on the class. -For example, if the unit size is 8 bytes, then a slot of the 512-byte -size class covers 64 units. Meaning: The slab for the size class 512 -allows up to `SLAB_LEN / 64` calls to `alloc()` before panicking, if -no slot of this size class is ever freed. +For example, if the Unit size is 8 bytes, then a Slot of the 512-byte +class covers 64 Units. Meaning: The Slab for the 512-byte class can +only offer up to `SLAB_LENGTH / 64` Slots. If `alloc()` is called +repeatedly for 512 bytes without ever freeing, the allocator will +panic after that many calls. Current implementation details: @@ -92,9 +93,9 @@ Current implementation details: Therefore: -* Size of each slab: `2^31 * 8 = 16 GiB` +* Size of each Slab: `2^31 * 8 = 16 GiB` -* Total slab virtual memory: `16 GiB * 16 = 256 GiB` +* Total virtual memory: `16 GiB * 16 = 256 GiB` If you notice, after launching the Zisp REPL or a program using the Zisp runtime, that the operating system reports that the process is @@ -123,11 +124,11 @@ wrapper around mmap, calling `munmap()` on `free()`. ## Global metadata The static array `slab_infos` holds `N_SLABS` many structures which -record just two pieces of information associated with each slab: +record just two pieces of information associated with each Slab: -* The current *watermark* of the slab. +* The current *Watermark* of the Slab. -* The head of the global, shared *free-list* for this slab. +* The head of the global, shared *Free-List* for this Slab. Expressed in pseudo-code: @@ -138,7 +139,7 @@ Expressed in pseudo-code: var slab_infos: [N_SLABS]SlabInfo; -The `ListHead` need not be a direct pointer nor plain slot index; it +The `ListHead` need not be a direct pointer nor plain Slot Index; it may be a composite value including an ABA counter to help against the ABA problem: @@ -151,44 +152,44 @@ In other words, it may be implemented as a Treiber stack: The initial value of the `free_list` head is of course some sort of null indicator so we can know it's empty. -Detailed explanations of the watermark and free-list follow. +Detailed explanations of the Watermark and Free-List follow. -### Slab watermark +### Slab Watermark -The watermark represents the point above which, within the slab, the +The Watermark represents the point above which, within the Slab, the memory has not yet been touched at all, or has been explicitly given back to the operating with a call to `madvise()` or similar so the physically backing memory can be released. -Below the watermark is memory which is either currently in use by the +Below the Watermark is memory which is either currently in use by the application, or has been marked for reuse by a `free()` call but is still backed by physical memory. In some circumstances, Meta Alloc may use `madvise()` to tell the operating system that some regions -below the watermark can actually be reclaimed, because they are not +below the Watermark can actually be reclaimed, because they are not currently needed; in other circumstances, otherwise unused (freed) -memory below the watermark may actually hold metadata used by Meta -Alloc itself; this is where the free-list comes into play. +memory below the Watermark may actually hold metadata used by Meta +Alloc itself; this is where the Free-List comes into play. -### Free-lists +### Free-Lists Given that the allocator cannot trust the user to always free memory in reverse order to which it was acquired, it cannot simply decrease -the watermark when `free()` is called. It has to record that memory -slot for reuse somehow, even if it's deep below the watermark. +the Watermark when `free()` is called; it must record the Slot for +reuse somehow, even if it's deep below the Watermark. This introduces a little chicken-and-egg problem: Since we are the allocator, who allocates the dynamic memory required to record the -pointers to these freed slots, of which there could be plenty? +pointers to these freed Slots, of which there could be plenty? Thankfully, a very elegant solution exists: Use the memory of the -freed slots themselves to form a linked list of free slots; where -within each freed slot, we store a pointer to the next. +freed Slots themselves to form a linked list of free Slots; where +within each freed Slot, we store a pointer to the next. Meta Alloc implements an improvement over this common strategy: Given that even the smallest size class is fairly large, and given -that we can use small index values, rather than full pointers, to -represent addresses of slots, each node in the free-list actually +that we can use small Index values, rather than full pointers, to +represent addresses of Slots, each node in the Free-List actually contains the following structure: { @@ -198,7 +199,7 @@ contains the following structure: extra_idx_array: [MaxCount]Index, } -The bit-size of indexes, the maximum allowed count, and the smallest +The bit-size of Indexes, the maximum allowed count, and the smallest size class, must all be defined such that this works. The current values used by the implementation are as follows: @@ -208,18 +209,18 @@ values used by the implementation are as follows: * Padding: 56 bytes -* Maximum count: 16 indexes +* Maximum count: 16 Indexes * Smallest size class: 128 bytes -The extra index array starts after 64 bytes, and requires 64 bytes, +The extra Index array starts after 64 bytes, and requires 64 bytes, since it stores up to 16 32-bit integers; that's a total size of 128, which fits exactly into the smallest size. The 64-byte padding is to allow for efficient bulk memory transfer using up to 512-bit SIMD instructions on modern processors. -Why we would need to bulk-transfer 16 index values will be explained +Why we would need to bulk-transfer 16 Index values will be explained later, as we look into thread-local cache metadata. ## Per-thread metadata @@ -227,18 +228,18 @@ later, as we look into thread-local cache metadata. We don't want to burden code using Meta Alloc with concerns about thread safety; `alloc()` and `free()` should be inherently safe. -If each call to these functions needed to touch the global watermark -or free-list of a size class, it could lead to contention. As such, +If each call to these functions needed to touch the global Watermark +or Free-List of a size class, it could lead to contention. As such, threads use two tricks to decrease their need to access the global, shared metadata: -* Bumping the slab's watermark in chunks to reserve a number of slots - for the current thread every time the global watermark needs to be +* Bumping the Slab's Watermark in chunks to reserve a number of Slots + for the current thread every time the global Watermark needs to be increased. -* Using a free slot cache of static size within TLS memory, which is - emptied into the global free-list in chunks when full, and fed from - the global free-list in chunks when empty. +* Using a Free Slot Cache of static size within TLS memory, which is + emptied into the global Free-List in chunks when full, and fed from + the global Free-List in chunks when empty. Pseudo-code follows; detailed explanations are further below: @@ -253,20 +254,20 @@ Pseudo-code follows; detailed explanations are further below: ### Reserved memory -The reservation of slots is done simply by keeping a thread-local low -and high watermark value: Low is the starting point of reserved but -not yet used memory, and high is the endpoint. When low meets high, -we need to check the global watermark again (it may have been bumped -by another thread) to set our new low, and bump the global, to which -we set our new high. +The reservation of Slots is done simply by keeping a thread-local Low +and High watermark value: Low is the starting point of reserved but +not yet used memory, and High is the endpoint. When Low meets High, +we need to check the global Watermark again (it may have been bumped +by another thread) to set our new Low, and bump the global, to which +we set our new High. -### Free slot cache +### Free Slot Cache When `free()` is called in a thread, it checks whether there's still -room in its local free slot cache. If it's full, it instead moves an -entire chunk into the global free-list. When `alloc()` is called, it -checks if there's slots in its local cache; if not, it checks if the -global free-list has anything, and transfers a chunk from there to +room in its local Free Slot Cache. If it's full, it instead moves an +entire chunk into the global Free-List. When `alloc()` is called, it +checks if there's Slots in its local cache; if not, it checks if the +global Free-List has anything, and transfers a chunk from there to feed some entries into the local cache. This emptying and freeing is done in halves. If the entire cache was @@ -275,20 +276,29 @@ when a thread is repeatedly alternating between `alloc` and `free` calls while just at the boundary: The alloc call fills the whole cache, the free call empties it again, and so on. -For this reason, the maximum size of the free slot cache per thread -can be twice as large as the maximum chunk size in the slab's global -free-list. +For this reason, the maximum size of the Free Slot Cache per thread +can be twice as large as the maximum chunk size in the Slab's global +Free-List. There is one more nuance to be aware of: -Consider a free slot cache of 64 entries. And remember that slabs +Consider a Free Slot Cache of 64 entries. And remember that Slabs have equal size, meaning larger size classes allow for fewer total numbers of allocations. For very large size classes, we don't want every thread that ever called `alloc()` once to immediately hog 64 -slots as a reserve. For this reason, although the free slot cache +Slots as a reserve. For this reason, although the Free Slot Cache array has a static maximum size, larger size classes have a lower maximum element count that they enforce. +Note: While the bumping of the global Watermark happens in fairly +massive chunks for the smaller size classes, the Free Slot Cache +instead has fairly small upper limits throughout all size classes. +This is because reserving a big "hole" in a Slab is totally benign, +since the operating system won't map any physical memory to it until +it's actually used; whereas freed Slots already have physical memory +backing them, making it important not to let too many of them linger +around in some thread's local cache without being reused. + ### Thread destruction When a thread that used Meta Alloc is going to exit, it must flush @@ -299,10 +309,10 @@ ensure that it's called at least once by a thread, before it exits, without any other subsequent calls to `alloc()` or `free()` before exiting. -This immediately transfers the thread's free slot cache into the -global free-list, in multiple chunks if necessary, and also creates -free-list chunks for memory it had reserved by bumping the global -watermark. +This immediately transfers the thread's Free Slot Cache entries into +the global Free-List, in multiple chunks if necessary, then creates +Free-List entries (also in chunks) for any leftover memory that was +reserved from bumping the global Watermark by a chunk. ## Releasing vmem |
