diff options
| author | Taylan Kammer <taylan.kammer@gmail.com> | 2026-06-26 00:08:22 +0200 |
|---|---|---|
| committer | Taylan Kammer <taylan.kammer@gmail.com> | 2026-06-26 00:22:11 +0200 |
| commit | 9151c8bb6f401bf9939765b0daa01b680a25a473 (patch) | |
| tree | 7086440e0fe5ebd3873c9abbdfb06778b949c567 /notes | |
| parent | aaaa9d9eb290991b9b6d732c375472c891ddaf29 (diff) | |
fixup
Diffstat (limited to 'notes')
| -rw-r--r-- | notes/260625-optimize.md | 51 |
1 files changed, 33 insertions, 18 deletions
diff --git a/notes/260625-optimize.md b/notes/260625-optimize.md index d43ddc6..cdb9644 100644 --- a/notes/260625-optimize.md +++ b/notes/260625-optimize.md @@ -2,6 +2,8 @@ <!--TOC--> +_2026 June_ + I might be deluded, but I think it should be possible to make a tree walking AST interpreter pretty much as efficient as a bytecode VM, if we're smart enough about the AST representation and a well-selected @@ -9,7 +11,7 @@ set of optimizations performed on it. Here's what I came up with so far. -## Abandon linked lists (cons cells / pairs) +## No linked lists Using traditional cons cells to represent code forms as linked lists means you're paying a memory overhead of 50% to store lists. That's @@ -26,14 +28,19 @@ AST walker will be going through the list one way or another, so it doesn't really matter; having the elements always start at zero is simpler.) -## Tight packing of short arrays +## Tight packing To further ensure minimal memory footprint of AST nodes, we make sure that they're allocated tightly in blocks, without any padding between them. See `src/zisp/gc/ListPool.zig` for how I've implemented this. -Cache locality and memory density should now be perfect. +Cache locality and memory density should now be perfect: + +[ListPool.zig](https://git.tkammer.de/zisp/tree/src/zisp/gc/ListPool.zig) + +(I like to refactor code and move around source files a lot, so if +this link becomes a 404 and I forget to update, I'm sorry.) -## Help the prefetcher (ABANDONED) +## Help the prefetcher? Memory prefetching refers to a modern CPU feature in which the CPU notices that you're accessing memory addresses sequentially, and @@ -53,7 +60,7 @@ call that's not inlined is encountered. So yeah, scrap that. Hopefully it makes no difference. -## Quoted data directly in the AST +## Quoted data in AST This may just be a stupid micro-optimization, because data that could be confused for code to evaluate (lists and identifiers) but is meant @@ -132,14 +139,14 @@ I think I like that. There could still be a way for other modules to bindings of that module into local bindings within the importing module, thus making the constants eligible for folding anyway! -## Lexical closure references by flat index +## Lexicals by flat index Now we're getting to the hot stuff. Zisp won't allow capturing mutable variables by reference in closures; you'll need to copy the variable's current value into a non-mutable variable if you want to capture it. (There's precedent for this in -other languages, like Java; nothing newly invented here.) +other languages, like Java; nothing new invented here.) You can still simply capture a reference to a mutable heap object, like a one-element array aka "box" object type, so you're not really @@ -229,7 +236,7 @@ to replace "local variable reference" objects with their values. Those "local variable reference" objects can also just be flat array indexes, thanks to other optimizations... -## Eager stack frame allocation +## Eager stack frame alloc This may just be a micro-optimization; I've not thought about it that much yet. But basically, the idea is to walk the body of a lambda, @@ -253,7 +260,7 @@ Oh, and function parameters are of course just the first few entries in this (conceptual) locals array (which may just be a stack pointer) and get populated at the call-site with the passed arguments. -## Lexical and local reference representation +## Lexical / local references Above, I've said that lexical and local variable references literally become `lexicals[i]` and `locals[i]`, but that was a bit of a lie. @@ -280,7 +287,7 @@ Realistically, you don't need 48 bits for a local or lexical variable index. Even 16 bits would be more than enough. So, there's a bit of waste here compared to what we could do with bytecode. -## VM opcode in expression head +## AST walker opcodes I've already explained this above as part of the lexical references optimization: @@ -309,14 +316,22 @@ There's no question that bytecode would be more compact than this AST despite all the optimizations. Wasting 56 bits on an unused opcode payload here, 32 bits on an overly large locals index there, and the generally pointer-rich structure of the tree... These will add up, -and bytecode could fix all that. But the beauty of this strategy is -that you literally just work directly on the data structure returned -by the parser, doing only in-place mutations and a small number of -auxiliary allocations. Even after optimizations, the AST should be -almost trivial to serialize in a pretty-printed format to get either -some nice insights into what the optimizer did, or, if allocation of -debug metadata was enabled, it may even be possible to turn it back -into original source form. +and bytecode could fix all that. Further, the "almost everything is +NaN-packed" strategy means we need frequent shifting and bit-masking +to get some of the actual "opcodes" like lexical and local variable +references. I'm not sure how impactful all this is likely to be on +performance, but it seems obvious that there should be a measurable +difference. Maybe up to 2-3x slower execution? I'd actually be +content with that, because... + +The beauty of this strategy is that you directly work on the AST data +structure returned by the parser, doing only in-place mutations and a +small number of auxiliary allocations. Even after optimizations, the +AST should be almost trivial to serialize in a pretty-printed format +to get either some nice insights into what the optimizer did, or, if +allocation of debug metadata was enabled, it may even be possible to +turn it back into original source form sans comments, whitespace, and +a few other minor one-way transforms on the way from text to AST. So, the low latency and the REPL/debug experience will be impeccable. Not to mention the incredible simplicity of it all: We don't need a |
