TYCHO Search

Search documentation

    ↑ ↓ to move · Enter to open · Esc to close

    Performance

    Current reviewed claim and gate: bench/PERFORMANCE.md. Some historical tables below predate the multi-size suite and the Java/Swift ports.

    and memory

    Every program in this file runs on tychoc, its output checked against a golden. Cross-language *ratios* matter more than absolute times, which depend on the machine.

    The generated multi-size suite is bench/RESULTS.md, produced by bench/suite.py; it is the source of record for the size/tier measurements. This page is the analysis and historical context. The generated suite now compares both idiomatic and expert manual-memory rows: on binary, the expert C arena beats Tycho (1.47x at the large GCC run), so the older single-tier tree headline is not a claim against the best manual implementation.

    A real program at flat memory

    examples/json.ty is a 220-line recursive-descent JSON parser over a recursive Json sum type — real recursion, zero malloc/free/refcount/GC in the source. Parsing 5,000,000 documents in a loop holds at a flat 10 MB: each document's tree is reclaimed when its loop iteration's arena resets. Clean under ASan + LeakSanitizer.

    Historical memory snapshot

    This memory table predates the Java/Swift ports; the current managed-memory time comparison is in the GC/ARC section below. The older Koka column is kept only as history.

    Per-workload timings are in bench/prongB/RESULTS.md. Peak resident memory (MB) — every binary computes the same checksum; lower is better. Figures below are rounded per-round snapshots; the precise log, with wall times and hardware, is that RESULTS file.

    workloadtychoCRustGo (GC)Koka (Perceus)
    binary-trees1333333614
    tree-rewrite6139227
    array-pipeline633614
    string-pipeline11232

    On the allocation-heavy tree workloads Tycho uses the least memory of the five — 40% of C's on binary-trees, half on tree-rewrite — with no GC and no reference counting, only lexical arenas and value semantics. Memory is the thesis metric, and it is reached with zero manual management.

    Current GC / ARC rows

    Six core workload ports are built for OpenJDK 25 (GC) and Swift 6.3.2 (ARC); the service rows below extend the same comparison. Wall time, same outputs, best-of-three:

    workloadTychoJava (GC)Swift (ARC)
    binary-trees93 ms335 ms2375 ms
    array-pipeline20 ms72 ms50 ms
    dispatch5 ms66 ms156 ms
    json-parse601 ms698 ms3072 ms
    winagg82 ms277 ms374 ms
    latency134 ms411 ms163 ms

    Tycho is faster than both managed-memory runtimes on every port. Java's JVM baseline makes its peak RSS uninformative for the small rows; the table is a time comparison. Koka results elsewhere in the repository are historical and are no longer the headline managed-memory comparison.

    Service-row times (ms; same checksums, best-of-three where the runner batches). This is a single-size snapshot from the dedicated service harness; the three-size generated suite in bench/RESULTS.md is the current source of record.

    rowTychoCRustGoJavaSwift
    winagg8297176296277374
    latency1341271031031411163
    invindex growth252255282334381631
    invindex count-fill333383449626460893
    invindex map growth160187206213277372
    invindex map count-fill308307364367363661
    window string2957505851111
    window int2102165
    gcscan4782104125101135
    dbquery231233-254--

    This snapshot predates the final 2026-10-22 codegen and expert-tier run; use bench/RESULTS.md for current row-level claims. The current generated suite has Tycho faster than C and Rust on latency at the large GCC size.

    In the final generated suite, Tycho is faster than Rust on latency at the large GCC size. The earlier Rust loss was tied to the pre-unroll codegen and is no longer the current claim.

    Relative service scorecard

    This is the same single-size snapshot. bench/RESULTS.md carries the generated three-size version and the core rows.

    Tycho / competitor; below 1.00 means Tycho is faster. These are the same service runs above, plus latency and gcscan from their own harnesses. Java/Swift memory is excluded from memory claims because their peak RSS includes the runtime baseline; window int, window string and other rows under 10 ms include process startup and are directional only.

    workloadvs Cvs Rustvs Govs Javavs Swift
    winagg0.85x0.47x0.28x0.30x0.22x
    invindex growth0.99x0.89x0.75x0.66x0.40x
    invindex count-fill0.87x0.74x0.53x0.72x0.37x
    invindex map growth0.86x0.78x0.75x0.58x0.43x
    invindex map count-fill1.00x0.85x0.84x0.85x0.47x
    window string0.51x0.58x0.50x0.57x0.26x
    window int2.00x-1.00x0.12x0.40x
    dbquery0.99x-0.91x--
    latency1.06x1.30x0.13x0.33x0.82x
    gcscan0.57x0.45x0.38x0.47x0.35x
    geometric mean0.91x0.72x0.52x0.45x0.39x

    The aggregate is useful but not a claim that every row wins: Tycho loses to The older single-size snapshot above is historical. The generated suite now separates idiomatic and expert tiers; see bench/RESULTS.md for the current losses. Missing cells remain -.

    The empty-process floor on this Linux host is <1 ms for Tycho/C/Rust/Go, 11 ms for Java and 1 ms for Swift (make bench-floor). Rows near that floor are not evidence about throughput.

    The core seven-row prongB scorecard gives the complementary view (geometric mean over its complete rows): time 0.63x vs C, 0.38x vs Rust, 0.28x vs Go, 0.28x vs Java, 0.11x vs Swift; memory (C/Rust/Go only) 0.80x, 0.74x, 0.28x. The tree rows pull that average below 1.0; the flat rows are the honest controls where C/Rust can win.

    Backend sensitivity

    Tycho emits C, so a Tycho-vs-C number is only meaningful when both are built by the same backend. The current headline rows use GCC. Re-running the latency harness with CC=clang changes the shape substantially:

    backendTychoCRust (rustc/LLVM)
    GCC127 ms124 ms100 ms
    clang159 ms102 ms105 ms

    Tycho is C-class under GCC but loses to C and Rust under clang; this is a code-generation/backend issue, not a reason to hide the row. Use sh bench/backends.sh for the same-source comparison and report the backend with every number. A clang-specific Tycho codegen fix is a separate target.

    Service-row coverage

    Y means the row is implemented and run by the service harness; - means it is explicitly unported.

    rowTychoCRustGoJavaSwift
    winaggYYYYYY
    latencyYYYYYY
    invindex growthYYYYYY
    invindex count-fillYYYYYY
    invindex map growthYYYYYY
    invindex map count-fillYYYYYY
    window stringYYYYYY
    window intYYYYYY
    gcscanYYYYYY
    dbqueryYYYYYY

    The current generated suite has all six dbquery ports; older tables below record the period when Rust/Java/Swift coverage was still missing.

    The service-shaped winagg row (per-window map of growing lists) now runs at 82 ms versus C 97 ms, Rust 176 ms, Go 296 ms, Java 277 ms and Swift 374 ms; peak RSS is 7.7 MB versus C 5.9 MB. It is the row the map/list hash and read-only-borrow work targeted.

    Arena internals, measured

    The peak-RSS tables above are the memory model's *output*. What the model does underneath — how much it reserves, how much it reuses, where the live set sits — is observable directly: TYCHO_ARENA_STATS=full <program> prints a one-shot summary at exit (runtime/tycho_rt.c@stats_dump): peak live bytes (the working-set high-water), total bump-allocated bytes, the free-list recycle rate, OS-reserved bytes over blocks, the block-pool reuse rate, and a per-function — now per-statement, see below — residency table. At the default 64 KiB block size:

    workloadpeak liveOS reservedslackblock reusefree-list recycle
    interp (5.6M-node tree interpreter)250.6 MiB250.8 MiB0.08%99.8%0 of 59.4M (held tree)
    json (recursive-descent parse)37.6 MiB38.1 MiB1.3%99.8%~0 of 5.5M (held tree)
    gcscan (large held set of small objects)63.5 MiB64.1 MiB0.9%37.3%0 of 6.0M (held)
    trie (per-node arenas)57.2 MiB57.5 MiB0.5%0.1%0 of 1.5M (held)
    dijkstra (index-graph)44.3 MiB44.4 MiB0.2%0.3%16 of 300k
    lru (delete-heavy map churn)33.0 MiB33.3 MiB0.9%0.0%16 of 89 (18.0%)
    strarr_build (loop-carried rebuild)224 B64.0 KiB—50%1.0M of 22.0M (4.5%)
    optstr_build (loop-carried rebuild)144 B64.0 KiB—50%1.0M of 6.0M (16.7%)
    loop_scratch (per-iteration scratch)40 B64.0 KiB—50%0 of 10.0M

    Three things the numbers say that the RSS tables cannot:

    1. At scale the model is tight. The big workloads hold their live set within 0.1–1.3% of what they reserve from the OS; the blocks fit the bump almost exactly. The 64 KiB block only shows up as slack on toy programs (a 40 B peak over a 64 KiB block).
    2. Free-list recycling is workload-shaped, and now countable. The liveness-driven recycle is zero where the working set is held (interp/json/gcscan/trie — trees stay live), and up to 17% of allocations where loop-carried rebuilds hand dead buffers back (optstr_build, strarr_build, structarr_build, lru). Before the counter existed the only evidence was bench-RSS deltas; the per-workload rate is now an instrumented number.
    3. Loop scratch peaks near zero. The per-iteration arena_reset keeps a loop's live bytes at one iteration's worth: loop_scratch bumps 10M allocations and peaks at 40 B. Per-statement attribution shows it: on json, a loop labeled main:loop0 bumps 16.4 MiB across 850k allocs with a 344 B peak — the per-iteration reset, now visible per statement instead of folded into the function total.

    Block size is a memory-only knob. TYCHO_BLOCK=<bytes> overrides the 64 KiB default per run (read once at startup). A sweep on the three big workloads:

    blockinterp peak/OSjson peak/OSlru peak/OSwall (interp/json/lru)
    8 KiB250.6 / 251.5 MiB37.6 / 37.8 MiB33.0 / 33.0 MiB1.11 s / 0.14 s / 0.64 s
    64 KiB250.6 / 250.8 MiB37.6 / 38.1 MiB33.0 / 33.3 MiB1.10 s / 0.13 s / 0.66 s
    256 KiB250.6 / 251.0 MiB37.6 / 38.9 MiB33.0 / 34.3 MiB1.11 s / 0.14 s / 0.65 s

    Peak *live* is identical across sizes — the block size never moves the working set, only the slack (≤4% either way here), and wall time is flat. The 64 KiB default is not retuned.

    Speed

    Against idiomatic per-node C/Rust allocation, Tycho wins the allocation-heavy tree workloads on time and memory. The generated expert-tier controls change the binary result: an expert C arena is faster than Tycho, while Tycho keeps the memory edge. Tycho trails C and Rust on flat array pipelines (per-element bounds checks, not the memory model). The in-place append (an acc = acc + x loop growing one buffer) is guarded by make bench: ~1.5 MB where the un-optimized path is ~825 MB at the same N, so a 32 MB bound sits firmly between working and broken.

    Where it costs

    The full loss column, with the idiom for each case, is in docs/internals/value-semantics-limits.md. The short version: pointer-shaped data (graphs, doubly-linked lists) costs more unless you use the flat-pool idiom — hold all nodes in one array and link them by integer index, which is also the cache-friendly layout data-oriented engines choose on purpose. Arenas reclaim at *scope exit*, not incrementally, so a long-lived scope holds its transients until it returns — scope them in an inner function.