Goose

Deprecated language features: reusable and reusable[]. Use slot_pool<T> for slots and slice_pool<T> for contiguous runs. Builtin behavior and historical examples below remain relevant to existing code; removal is planned separately, after the remaining migration gaps.

Goose tests whether inline data structures and language-level memory management can outperform C++ and Rust while preserving memory safety.

The benchmarks cover common algorithms that may benefit from this design, as well as workloads where Goose is likely to struggle. The aim is to measure both the benefits and the limitations and identify possible language improvements.

The separate slot_pool comparison measures a root-struct stdlib allocator against the existing reusable builtin with identical allocation policies and workloads; run it with bench/slot_pool.py. The slice_pool comparison covers first-fit span reuse, coalescing and relocation against reusable[]; run bench/slice_pool.py.

The suite tests the effects of these features:

We will want to test against performance conscious (but still idiomatic) C++ and Rust. C++ can reproduce many of these layouts through manual pointer management and data packing. Those techniques belong in the expert comparisons; Goose aims to make them convenient and memory safe in ordinary code.

Besides speed, we will want to test memory usage, and the combination of both. Each algorithm runs with at least 3 different data sizes: the smallest one focuses on pure CPU, and the 2 bigger sizes stress the cache and memory subsystems increasingly, to see if the Goose advantage appears or widens.

We should test all reasonably idiomatic forms in each language. For an algorithm that uses variant or polymorphic data, compare Goose's fixed and variable enums where possible, and Rust's fixed enums. For C++, compare subclasses (most idiomatic but slow), std::variant (faster but awkward to use), and C-style unions (fastest, but very unsafe).

If an algorithm does a lot of resizable array pushing, we'll want to be fair to C++ and compare both what happens if the programmer optimally chooses reserve, and what if they don't bother to use it.

We'll also want to find examples of code that Goose is currently weak at, and some where performance is similar because the languages use the same compiler backend.

Where the gains should compound

Most of the advantages above are the same advantage seen from different angles: they shrink the bytes touched per unit of work. An algorithm's benefit should therefore be roughly proportional to how pointer rich and allocation rich its data is, and should widen with data size as the working set falls out of each cache level in turn. The biggest expected wins, in rough order:

  1. Anything that builds many small linked things: parse trees, ASTs, s-expressions, scene graphs. Allocation cost, node size (relative refs, varints, u8 tags, no vtable pointer) and teardown cost all stack up in the same benchmark.
  2. Anything holding a collection of variable size things: lists of strings, lists of records, jagged arrays. One block versus 1+N allocations.
  3. Anything that grows while holding pointers into what it grew. In C++/Rust this forces the programmer into indices, a chunked arena (deque-like, extra indirection), or reserving a hard upper bound. All three are worth benchmarking as separate rows, because that workaround cost is what we are trying to eliminate.
  4. Load/save and cross thread paths, where Goose does no work and the others have to serialize or fix up pointers.

If a benchmark's data is a flat array of scalars, expect no advantage. That is a useful control case.

Where we should expect Goose to lose

The suite should measure these potential limitations:

Candidate algorithms

Familiar, widely written code, each chosen to hit specific axes above:

Measurement notes

The suite as built

bench/goose/, bench/cpp/ and bench/rust/ hold sixteen benchmarks against this design. The first ten are small, each testing one construct, from sum (the control) to sexp (the flagship). The six added after that are a little larger and were chosen to fill the gaps the first ten left:

bench/run_bench.py builds and runs them at three sizes each, checks that every implementation of a benchmark prints the same checksum, and writes bench/results.md. The commentary at the end of that file comes from bench/notes.md.

The suite does not benchmark saving and loading relative-reference structures or sending them through thread queues. Serialization has since been implemented with validation (see docs/design/serialization.md). Queues can carry the serialized byte image, but cannot directly carry values with relative references because those values are not flat (TODO 9).

C++ is measured at two tiers, idiomatic and expert. Its range of implementation techniques is too wide for one row to represent it. For Rust, the benchmarks compare safe, idiomatic implementations. When two Rust representations are listed, they differ in semantics or structure: owned versus borrowed strings, for example, or owning Box nodes versus a Vec-plus-indices arena. A faster time alone is not a reason to include another representation. The community recommends the arena for pointer-heavy data, and the prediction above held: it is 2.8x to 5.9x faster than Box on the three benchmarks that have both.

The comparison also has a usability dimension that timing does not measure: when Rust uses u32 indices and Goose uses typed, nullable references, similar performance still comes with different programming interfaces.