Reussir Language Reference

Reussir’s Performance

Reussir’s Performance

Reussir is designed so that direct functional programs can become efficient native code without requiring the programmer to manually recover mutation. The results below test that claim on allocation-heavy persistent structures and on large aggregate updates. They measure the complete language pipelines, not one isolated compiler pass.

Reading the figures

Every benchmark case has its own figure. The left panel reports absolute mean runtime in milliseconds; the right panel reports absolute peak resident set size in MiB. Both axes are linear and start at zero. Scales are chosen independently for each case, so compare the printed measurements rather than bar lengths across different figures. Lower is better.

Every executable hard-codes its workload, verifies a representation-independent result, and exits unsuccessfully if the check fails. Signed 64-bit keys and elements are used across ports where the payload is part of the workload; indices, metadata, and object layouts remain idiomatic to each language.

The functional cases use the ordinary Haskell source with its default RTS and direct persistent Rust implementations. The large-aggregate cases use Haskell’s mutable Data.Array.STData.Array.ST sources and Rust arrays or vectors. This keeps the representation choice visible instead of hiding it inside a suite-wide average.

Functional data structures

These cases construct immutable trees, lists, symbolic terms, and closure environments. Updates preserve observable sharing, which makes allocation, reference counting, garbage collection, and safe storage reuse central to the result.

Symbolic derivative

derivederive begins with , differentiates the resulting symbolic expression ten times, simplifies algebraic constructors during every pass, and finally checks the right-hand depth of the result. It is a compact but allocation-heavy algebraic-data-type workload: recursive pattern matches consume an expression tree while the next tree is constructed, creating many opportunities for a compiler to cancel ownership operations and reuse dead nodes.

Figure 1: Symbolic derivative. Mean runtime is the ten-run Hyperfine mean; peak RSS is a separate GNU timetime execution. Lower is better.

Persistent finger tree

fingertreefingertree builds 65,536 signed 64-bit elements with snocsnoc, performs one million viewlviewl/snocsnoc rotations, and then drains the tree into a checksum. The structure uses one-to-four-element digits and two-or-three-child internal nodes. Haskell uses the published fingertreefingertree package; the other ports implement the same algorithm. Rust shares persistent paths with RcRc, while Reussir leaves node elimination and reconstruction available to reuse analysis.

Figure 2: Persistent finger tree. Mean runtime is the ten-run Hyperfine mean; peak RSS is a separate GNU timetime execution. Lower is better.

Hood–Melville real-time queue

functional-queuefunctional-queue builds 65,536 elements, then performs one million dequeue/enqueue rotations before draining. A strict Hood–Melville queue divides its contents between front and rear lists and advances a small ReversingReversing or AppendingAppending state incrementally, so no individual operation performs an entire list reversal. The ports use the same operation stream and scheduling steps; the case stresses shared list tails and rapid replacement of small recursive states.

Figure 3: Hood–Melville real-time queue. Mean runtime is the ten-run Hyperfine mean; peak RSS is a separate GNU timetime execution. Lower is better.

Purely functional Braun heap

heap-functionalheap-functional builds a Braun-tree min-heap containing 65,535 values drawn from an approximately Gaussian stream: each value is the sum of twelve MINSTD generator outputs. It then performs 6.5 million unconditional replace-top rounds and checks the evicted values plus final contents. Every update rebuilds a root-to-leaf path. Rust does that path copying through RcRc; it deliberately does not substitute BoxBox plus unique-owner mutation.

Figure 4: Purely functional Braun heap. Mean runtime is the ten-run Hyperfine mean; peak RSS is a separate GNU timetime execution. Lower is better.

NBE with explicit closures

nbe-closurenbe-closure normalizes a Church-numeral term representing one million applications. It runs seven seeded rounds so a compiler cannot share the normal form between repetitions, then counts applications in the result. Function values are defunctionalized records containing a term and an explicit environment. This isolates environment construction, closure dispatch, recursive evaluation, and allocation during quoting.

Figure 5: NBE with explicit closures. Mean runtime is the ten-run Hyperfine mean; peak RSS is a separate GNU timetime execution. Lower is better.

NBE with higher-order abstract syntax

nbe-hoasnbe-hoas normalizes the same one-million-application term for six seeded rounds, but represents semantic functions with host-language closures rather than a term/environment record. It therefore emphasizes higher-order closure allocation and application. Comparing it with nbe-closurenbe-closure shows how each toolchain handles equivalent normalization work expressed through two closure representations.

Figure 6: NBE with higher-order abstract syntax. Mean runtime is the ten-run Hyperfine mean; peak RSS is a separate GNU timetime execution. Lower is better.

Persistent red-black tree

rbtreerbtree inserts 2.5 million descending keys into an immutable red-black tree; every tenth value is truetrue, and a final in-order fold verifies that 250,000 such values remain. The conventional recursive balancing algorithm copies and recolors nodes along the search path. It stresses pattern matching, persistent path reconstruction, and recovery of nodes that become unreachable after an insertion.

Figure 7: Persistent red-black tree. Mean runtime is the ten-run Hyperfine mean; peak RSS is a separate GNU timetime execution. Lower is better.

Red-black tree with a zipper

rbtree-zipperrbtree-zipper performs the same 2.5-million-key insertion and final fold, but records the descent in an explicit zipper and reconstructs the tree while moving back toward the root. The logical result is identical to rbtreerbtree; the different control structure exposes short-lived context nodes and tests whether the compiler can reuse them through a structured reconstruction loop.

Figure 8: Red-black tree with a zipper. Mean runtime is the ten-run Hyperfine mean; peak RSS is a separate GNU timetime execution. Lower is better.

Large aggregates

These cases repeatedly update contiguous arrays or grids. Reussir source still threads aggregates functionally, but uniqueness analysis can prove that a buffer has no aliases and lower successive updates to ordinary in-place stores. Haskell uses the STST variants in these figures; Rust uses explicit mutable storage.

Mutable array heap

heap-arrayheap-array is the contiguous counterpart of the Braun heap. It pushes the same 65,535 generated values with sift-up, then performs the same 6.5 million replace-top and sift-down rounds with the same representation-independent checksum. The paired heap cases hold the algorithm and input stream steady while changing the storage from persistent paths to one mutable array.

Figure 9: Mutable array heap. Mean runtime is the ten-run Hyperfine mean; peak RSS is a separate GNU timetime execution. Lower is better.

Conway’s Game of Life

lifelife evolves a toroidal board for 50,000 generations from a deterministic roughly 34% live-cell soup. Every cell reads its eight wrapped neighbors. Reussir describes each generation as a new rank-two tabulation whose kernel captures the old grid; its optimizer can inline that closure and fuse the fill loops. Mutable ports use double buffering, making this a direct test of whether a functional full-array rebuild becomes a tight loop.

Figure 10: Conway's Game of Life. Mean runtime is the ten-run Hyperfine mean; peak RSS is a separate GNU timetime execution. Lower is better.

Quicksort

qsortqsort runs 100 rounds of Lomuto quicksort over fresh 65,536-element arrays filled by MINSTD, then checks both ordering and a checksum. Reussir threads the array linearly through partition and recursive calls so uniqueness can turn functional setset operations into in-place stores. Rust and Haskell ST use mutable arrays. Koka retains its existing list-quicksort source, so its bar matches the task and verified result but not the contiguous representation.

Figure 11: Quicksort. Mean runtime is the ten-run Hyperfine mean; peak RSS is a separate GNU timetime execution. Lower is better.

Why persistent Rust is slower here

The Rust ports intentionally use the canonical safe representation for a direct persistent translation: immutable nodes behind RcRc, cheap Rc::cloneRc::clone operations for shared edges, and fresh nodes along every changed path. They do not introduce unsafe code, custom arenas, intrusive reference counts, mutation-through-uniqueness, or workload-specific data structures.

An expert Rust programmer could apply those techniques by hand. That would measure programmer specialization, however, rather than what the compiler can recover from the straightforward persistent program. Reussir starts from the same functional intent and uses reference-count cancellation, reuse tokens, uniqueness propagation, and LLVM optimization to eliminate ownership work and reuse dead nodes where it is safe. The aggregate results are the necessary counterpoint: once mutation is explicit, Rust returns to 1.18× Reussir time and uses less peak memory. The figures are therefore evidence about representations and optimization pipelines, not a claim that Rust is generally slow.

Compilation settings

The commands are deliberately visible because flags such as link-time optimization can materially change allocation-heavy code.

  • Reussir: rrc -Oaggressive --reuse-across-callrrc -Oaggressive --reuse-across-call emits LLVM IR, followed by clang -flto=thin -fuse-ld=lld -O3 -march=nativeclang -flto=thin -fuse-ld=lld -O3 -march=native and the static Reussir runtime archive.
  • Koka: koka -O3koka -O3, using the same Clang ThinLTO, LLD, and -march=native-march=native settings so that its runtime can participate in cross-module optimization.
  • Lean: lean -clean -c emits C, then leanc -flto -O3leanc -flto -O3 performs the native link.
  • Rust: rustc -C opt-level=3rustc -C opt-level=3. The plotted build does not add Rust LTO, native-CPU tuning, or a custom allocator.
  • Haskell: ghc -O2 -rtsoptsghc -O2 -rtsopts; the plotted build uses the default RTS settings.
  • OCaml: ocamlopt -O3ocamlopt -O3.

LTO means that optimization continues at link time while the linker can see IR from more than one compilation unit. That visibility enables inlining, constant propagation, and dead-code elimination across application/runtime boundaries. Full LTO combines the program more centrally; ThinLTO exchanges compact module summaries and retains parallel back ends. The benchmark builds Reussir’s static runtime as linker-plugin bitcode, which lets the ThinLTO link see allocation and reference-count entry points instead of treating them as opaque calls. Koka receives the same runtime-level treatment. These settings measure the complete optimized toolchains, and -march=native-march=native also means the generated Reussir and Koka code is specialized to the benchmark machine.

Runtime and allocator controls

The per-case figures use one declared default configuration per language rather than selecting the fastest setting separately for every workload. Additional controls show how much runtime tuning can move the result. FF is the geometric mean over the eight functional cases; AA is the geometric mean over heap-arrayheap-array, lifelife, and qsortqsort. Values in this table are relative to that language’s own primary configuration, not to Reussir.

Configuration / its baseline F time F RSS A time A RSS
Reussir without cross-call reuse / Reussir 1.1× 1.11×
Rust with mimalloc / Rust 0.65× 1.02× 1.01×
Haskell with a 1 GiB allocation area / Haskell default RTS 1.23× 7.4× 1.11× 15×

Cross-call reuse ablation

reussir-nracreussir-nrac keeps -Oaggressive-Oaggressive and the same native link but omits --reuse-across-call--reuse-across-call. It is a narrow ablation: reuse inside a function and the rest of the optimization pipeline remain enabled. Removing cross-call reuse raises functional geometric-mean time to 1.10× and RSS to 1.11× the plotted Reussir build. The effect is concentrated rather than uniform: the persistent red-black tree takes 1.90× as long and the Braun heap 1.33×, while the three large-aggregate cases remain at 1.00× as expected because their important uniqueness is local. This control attributes part of the persistent-structure result to compiler-derived reuse without pretending that one flag explains the entire pipeline.

Haskell’s RTS command

GHC’s -rtsopts-rtsopts permits runtime-system options. At invocation time, +RTS+RTS and -RTS-RTS delimit flags for the runtime rather than arguments for the program. The control configuration can therefore be written as:

benchmark +RTS -A1G -RTS
benchmark +RTS -A1G -RTS

The harness bakes the same -A1G-A1G option into the executable with -with-rtsopts=-A1G-with-rtsopts=-A1G, then invokes the executable without arguments. -A1G-A1G sets a 1 GiB allocation area (often described as the nursery setting). A large area can avoid minor garbage collections in a churn-heavy program: in this snapshot it cuts derivederive time to 0.32× its default-RTS result and nbe-closurenbe-closure to 0.58×. It can also enlarge the active memory footprint and damage cache behavior: fingertreefingertree becomes 4.29× slower, and the functional geometric-mean RSS becomes 7.38× the default. The default RTS is consequently the honest single choice for the per-case figures; -A1G-A1G is useful evidence of sensitivity, not an across-the-board improvement.

Rust and mimalloc

Reussir’s production runtime uses mimalloc and exposes allocator entry points that its compiler and LTO pipeline can specialize. The suite therefore includes rust-with-mimallocrust-with-mimalloc: it compiles the same Rust source with the same flags, then sets LD_PRELOAD=$MIMALLOC_LIBLD_PRELOAD=$MIMALLOC_LIB so general allocations are interposed by mimalloc instead of the platform allocator.

This is an allocator control, not a perfectly identical allocation path. Reussir can call sized and small-object runtime entry points and optimize across them; Rust continues to allocate through its general global-allocation API and retains the same RcRc graph. Mimalloc reduces Rust’s functional geometric-mean time to 0.65× the normal Rust result, changing its comparison with Reussir from 6.61× to 4.32×. It is not uniformly faster: the persistent heap becomes 1.19× slower, while aggregate runtime is essentially unchanged at 1.01×.

Peak RSS includes allocator code, metadata, and retained resident pages. The three small Rust aggregate executables rise from roughly 1.7–2.2 MiB with the platform allocator to about 6.1 MiB with mimalloc; that produces the apparently large 3.04× aggregate RSS factor in the control table. The allocator matters, but it does not account for the full functional-structure result.

How to read these numbers

Test machine

The checked-in benchmark run was produced on the current host; the hardware and system configuration below was reported on 2026-07-31. The workloads are single-threaded, so the core count describes the machine rather than parallel speedup inside a benchmark.

  • Processor: AMD Ryzen 9 9950X, one socket with 16 cores and 32 hardware threads, x86-64, one NUMA node. Frequency boost is enabled. lscpulscpu reports a 5.76 GHz maximum.
  • Cache: 768 KiB total L1 data and 512 KiB total L1 instruction cache, 16 MiB total L2, and 64 MiB L3.
  • Memory: 91.93 GiB visible to the operating system.
  • System: NixOS 26.11 (Zokor) with Linux 7.1.3-cachyos-lto on x86_64.
  • Frequency policy: amd-pstate-eppamd-pstate-epp, governor powersavepowersave, energy preference balance_performancebalance_performance.

The harness does not pin the process to one core, and transient thermal, scheduler, and background-load effects are not encoded in results.jsonresults.json. Those details are another reason to treat small differences cautiously.

Measurement procedure

Timing uses Hyperfine with 5 warm-up executions and 10 measured executions; each runtime panel uses the executable’s mean. Peak RSS is a separate execution sampled once with GNU time -f %Mtime -f %M. Stack limits are removed for every measured process. The Nix flake pins the toolchain and every program verifies its output. Treat the measurements as one reproducible suite snapshot, not as constants that transfer unchanged to every machine or application.

The data on this page comes from the benchmark suite at revision 78ec846764ed (dated 2026-07-12); the checked-in results.jsonresults.json has SHA-256 f6c4765c381c74a51c96879e7614f2e7e9461a6ba20050d4fa1bcb1a297218dbf6c4765c381c74a51c96879e7614f2e7e9461a6ba20050d4fa1bcb1a297218db.