From ef24c951089999a39e0e39e6ac0e32d9ebe2b27951e2aeba6ab903ed26de9642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Bj=C3=B6rnstam?= Date: Wed, 14 Oct 2026 21:38:05 +0200 Subject: [PATCH] Performance: choosing between List and Vec, and Map and OrderedMap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new subsection under Performance with two tables from the benchmarks in bench/collections. It says which operations each collection does cheaply, and names the traps: vec-add in a loop, binding the rest of a vec in a recursive match, and indexing into a list. 🤖 Generated with [ECA](https://eca.dev) Co-Authored-By: eca-agent --- _02_.Writing-programs.sz | 51 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/_02_.Writing-programs.sz b/_02_.Writing-programs.sz index 4f37937..ea56320 100644 --- a/_02_.Writing-programs.sz +++ b/_02_.Writing-programs.sz @@ -1592,3 +1592,54 @@ Most of the time you should use the @code{loop} form. It will be the fastest, an The absolutely fastest collection to loop over is, unsurprisingly, the Array. In bjolang it should be just as fast as in c# (again, without spans). The loop form gets turned into a kind of goto soup. For the single level case, it is just as efficient as a c# @code{while} loop, but for really complex cases with many levels and subloops with clauses the JIT in dotnet may struggle. In that case you either live with the clarity and simplicity it gives, or you reach for the glorified gotos of names lets. This is probably not going to be a problem though. I tried to rewrite the worst loop form in bjolang (a 16 line monstrosity with one large multi-level claused subloop and a regular subloop) and there was no difference. + +@subsubsection{Choosing a collection} +Bjolang has two pairs of collections that do mostly the same job: @code{List} and @code{Vec}, and @code{Map} and @code{OrderedMap}. Which one you pick matters less for small collections than you think, and more for large ones than you would hope. The tables below come from the benchmarks in @code{bench/collections/} in the bjolang repository (run them with @code{./run.sh}). They were measured on one machine with .NET 10 on Linux, so trust the ratios more than the exact times. + +@strong{List or Vec} + +@read-table{ +| Operation | List | Vec | +|--------------------------------------------+--------------------------+---------------------------| +| memory per int | 32 B | about 7 B | +| first item | 1 ns | 2 ns | +| drop the first item | 1 ns | 21 ns | +| add at the front | 1 ns | 14–200 ns | +| add at the back | copies the whole list | 10 ns | +| last item, length | 1 ns per item | 1–2 ns | +| item at an index | 1 ns per item before it | 1–2 ns | +| read every item (@code{loop}, fold) | 1 ns per item | 0.6 ns per item | +| build with an accumulator, 1000+ items | 1 | 5–8 times faster | +| map, reverse, append, slice, 100+ items | 1 | much faster | +| recursive match that binds the rest | 1 ns per item | 30 ns and 230 B per item | +| match the first items, ignore the rest | 2 ns | 2 ns | +} + +Use a @code{List} when the work happens at the front and goes item by item: a stack, a result built with @code{cons}, or a recursive function that matches @code{(Cons h t)} and continues with @code{t}. That is what a list does in constant time, and there it is 3 to 30 times faster than a vec. + +Use a @code{Vec} for everything else. It knows its length and last item, it reads any index in a couple of nanoseconds, it is faster to iterate, and it uses about a fifth of the memory. Some things to keep in mind: + +@read-list{ + - Build vecs with an accumulator (@code{vecing} in @code{loop}, or a vec builder), not with @code{vec-add} in a loop. Each @code{vec-add} copies the tail of the vec, which makes a @code{vec-add} loop 3–4 times slower than a @code{cons} loop. + - Do not bind the rest of a vec in a recursive match. @code{[a rest ...]} slices the vec at every step. @code{[a b _ ...]} ignores the rest and does not slice. To walk a vec, use @code{loop}, a fold or an index. + - @code{list-ref}, @code{length} and the last item of a list read the list from the start. 64 random reads in a list of 100,000 items take 3 ms. In a vec they take 240 ns. +} + +@strong{Map or OrderedMap} + +@read-table{ +| Operation | Map | OrderedMap | +|--------------------------------------+---------------------+------------------------------------| +| lookup, int keys | 1 | 1.6–3 times slower | +| lookup, string keys | 1 | 2–6.5 times slower | +| update, insert, remove | 1 | 1.2–1.9 times slower, 2× the allocation | +| build | 1 | up to 2.9 times slower | +| read every entry, 10,000+ entries | 1 | 2–6 times faster | +| smallest key | reads every entry | 2 ns | +| 64 entries in a key range | reads every entry | about 400 ns at any size | +| every key, in order | collect and sort | about 4 times faster | +} + +Use a @code{Map} to look up, update, insert and remove by key. It wins at all of those at every size. The difference is largest for string keys, because a map hashes the key once and an ordered map compares it at every step down the tree. + +Use an @code{OrderedMap} when the order of the keys matters: the smallest or largest key, the entries between two keys, or the entries in key order. A map can only answer those by reading every entry. An ordered map is also faster to iterate in full, so a program that walks a large map more often than it looks things up in it may prefer one.