Goose

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 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, by running with at least 3 different data sizes for each algorithm, where the smallest one focuses on pure CPU, but the 2 bigger sizes will stress cache and memory subsystems increasingly, to see if the Goose advantage appears or widens.

We will want to test all reasonably idiomatic forms per language, e.g. for an algorithm that works on variant/polymorphic data, for Goose we'll want to see both fixed and variable enums if possible, for Rust just fixed enums, and for C++ we'll want to compare subclasses (most idiomatic but slow), std::variant (faster but unergonomic) 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 it is exactly the workaround cost we are trying to delete.
  4. Load/save and cross thread paths, where Goose does no work at all and the others serialize or pointer fixup.

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, one construct each, 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: lru (a cache: the reusable pool, free-then-alloc out of scope order, the mutation-heavy shape the stack discipline is worst placed for), scene (a scene graph with inline child arrays of relative references, mutated every frame), calc (many small parses into per-input local pools, with return from taken as a real error path a quarter of the time), bintrees (the Benchmarks Game's allocator benchmark, for recognisability), respond (a web handler's DTO with a string and a list of records, built and rendered per request), and blur (an image stencil: flat scalar work, bounds checks and no aliasing information, chosen to test a likely weakness). 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 that file ends with lives in 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, because the language admits such a wide range of implementation techniques that one row would misrepresent it. For Rust, the benchmarks compare safe, idiomatic implementations. Where a benchmark has two idiomatic Rust representations they are both listed, but the reason is always a semantic difference (owned versus borrowed strings) or a structural one (owning Box nodes versus the Vec-plus-indices arena the community recommends for pointer-heavy data), never just that one is faster. The prediction made above -- that the arena, not Box, is the real Rust comparison point -- 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.