Skip to content

Performance ​

This page records Aura's performance measurements. Each one has an exact workload, pinned source commits, raw observations, and content hashes. The measurements guide engineering. They are not language semantic guarantees. The speed targets for native code are ratios to matching Rust programs. The design record lists them.

Current Measurements ​

The numbers come from two sessions:

  • Foundations session. It ran on 7 September 2026, after a reboot. It measured the foundations changes and pinned Rust baselines.
  • Array session. A later quiet-host Array comparison, labeled separately below.

Each report has a grade. A contractual report passes its runner's qualification rules. A diagnostic-grade report has limits, which this page states next to its numbers.

ReportGrade
Release performanceContractual under its runner rules
Numeric ArraysContractual under its runner rules
Standalone integer loops against RustDiagnostic-grade
Per-call profilingDiagnostic-grade
Collections and value readsDiagnostic-grade

The foundations host:

ItemValue
MachineMac14,9 / Apple M2 Pro (10 cores, 16 GiB)
OSarm64 macOS 26.5.2 (build 25F84)
Boot identity1788744561, 7 September 2026 at 02:29:21 BST
CPythonXcode 3.9.6 at /Applications/Xcode.app/Contents/Developer/usr/bin/python3, with NumPy 2.0.2
Rust1.95.0 (59807616e, LLVM 22.1.2)
Concurrent referencestokio 1.53.1

Source Commits ​

The compiler bases are:

  • After: merge 50531b45797ae765ec8d885165c13042617ab567.
  • Before: tag v0.3.3-preview, d3cc6b96104dd597687a98e9624f800a0cb3cf1e.

The original task and TCP inputs pass int64 range values to int32 task parameters, so they fail checking at both bases. The release measurements use clean detached commits with the same two explicitly authorized as int32 corrections:

Release measurementCorrected source commit
After4e1e48c81bae6c62a54af763a4046901820038ec
Beforeddeaddf74301322fc96d8c09742ea12faa1dd8c3

Each corrected commit differs from its base by those two casts only. Compiler and runner sources are unchanged. The Array and standalone integer results were collected earlier in the same boot session, at the original bases. Their unaltered inputs and exact identities are recorded separately. The evidence includes the initial failures, the approval record, identical diffs, and a Git bundle that contains both corrected commits.

Control-Plane Workloads ​

The harness validates exact READY/GO/DONE records and checksums and times GO to DONE. It rotates 11 pairs and excludes one warmup per lane. The before and after reports both have clean detached sources, three empty quiet-host inventories, and successful input and hash rechecks. No override or Rust exclusion was used. Ratios are ratios of medians. Lower is faster.

Exact protocol workloadAura medianCPython medianRust medianAura / CPythonAura / Rust
Naive recursive fib(30)89.359292 ms158.321541 ms3.012292 ms0.56441729.664884
Create and join 10,000 tasks98.692417 ms51.786208 ms3.395167 ms1.90576629.068501
20-client delayed loopback TCP fan-out104.268375 ms108.793000 ms104.597250 ms0.9584110.996856
16-cycle retrying HTTP worker428.814667 ms522.391625 ms472.607250 ms0.8208680.907338

Workload details:

  • TCP uses 20 pre-bound loopback listeners with 100 ms handlers. Aura rejects moving an accepted TcpStream into a handler task with AU3008. A single listener would therefore serialize handler work.
  • Tasks includes creating and joining all 10,000 tasks after GO.
  • Retry runs use the same local fixture, status and delay schedule, 112 requests, and checksum 18112 in all three languages.

Integer Loops ​

The contractual release V6 comparison includes process startup. Python has one arbitrary-precision integer lane, paired with each Aura fixed width.

A startup-adjusted estimate subtracts the startup observation from the same repetition. Nonpositive adjustments stay in the raw evidence and are excluded only from the estimate. All 11 whole-process observations stay in each median.

Exact 10,000,000-iteration comparisonAura whole processCPython whole processAura startup-adjustedCPython startup-adjustedValid adjusted pairs
Aura int32 / CPython integer28.954291 ms328.618208 ms24.342791 ms298.047876 ms11/11
Aura int64 / CPython integer12.772500 ms328.618208 ms6.126791 ms294.897875 ms9/11

Two int64 adjustments are nonpositive in each before and after run. Each CPython adjusted median uses the same valid pairs as its Aura row. The after startup medians are Aura 5.610000 ms and CPython 23.895667 ms. The raw report also records dispersion. Startup subtraction is an estimate, not a pure kernel measurement.

The diagnostic standalone runner compares Aura and Rust separately. It uses 11 alternating pairs, excluded warmups, and exact 10000000 output. It ran at the original after merge 50531b45797ae765ec8d885165c13042617ab567. External inventories before and after were quiet. This runner lacks the release suite's three-phase host inventory and full input and binary hash rechecks. Do not mix its medians with the release V6 observations above when forming ratios.

Standalone checked loopAura medianRust medianAura / RustPaired median ratio
int3224.325417 ms16.429334 ms1.4806091.478258
int649.778250 ms16.337417 ms0.5985190.594855

The Rust loop places an optimizer barrier on each counter update. That cost and process startup are part of this exact reference. These results do not establish a general compiler-speed ranking. The before standalone runner emits only rounded minima. The before and after integer medians therefore come from the qualified release V6 runner, not from those minima.

Numeric Arrays ​

These measurements carry the label "quiet host, not post-reboot; contractual re-measure scheduled with the 0.3.4 release session."

Both runs are contractual under the runner's qualification rules:

  • clean detached sources
  • 11 rotating pairs and excluded warmups
  • exact protocol and checksum validation
  • three empty host inventories
  • successful input and hash rechecks

No override was used. The host is Mac14,9 / Apple M2 Pro / 16 GiB, with Xcode CPython 3.9.6, NumPy 2.0.2, and Rust 1.95.0. Measurements are single-threaded and per operation. Ratios are ratios of medians.

Before is merge base a368dce7e7b335c1c0cb800ba5240501a21f02bc. After is implementation head d9fc79921ba9fed1116634ec9994bcb599aa9f90. Each clean checkout used its own unchanged runner in the same measurement session, after the full local gates. Later publication edits do not change compiler or runtime sources.

Workload (one million float64 elements)Aura beforeAura afterChange in timeBefore Aura / RustAfter Aura / Rust
Fresh owned addition1.244818 ms0.249473 ms-79.9591%5.0393591.006524
Existing-array sum1.149271 ms1.149844 ms+0.0498%1.3389901.339087
ControlBefore medianAfter medianDrift
NumPy add0.253398 ms0.247745 ms-2.2310%
Rust add0.247019 ms0.247856 ms+0.3387%
NumPy sum0.173397 ms0.172073 ms-0.7639%
Rust sum0.858312 ms0.858677 ms+0.0425%

Addition meets the work's 1.5x Rust target at 1.006524x, with 79.9591% less Aura time. Its after Aura/NumPy ratio is 1.006974.

The sum ratio against NumPy is 6.682308. The deterministic reduction-order policy causes most of that gap. Under the same left-to-right order, Aura stays within 1.34x of Rust.

Addition allocates a fresh owned result. Sum reuses its input. These exact workloads do not establish a general language or Array-API performance ranking.

Before raw, after raw, comparison and controls, and SHA256SUMS keep all observations and source and binary identities.

Collections And Value Reads ​

These programs measure dict and set operations, list loops, and field reads in native builds. They ran on a shared four-core Linux x86-64 container with Python 3.13 on the same machine. The runner reports the median of five runs of each whole process. The host was not quiet, so the numbers are diagnostic-grade.

ProgramBeforeAfterPython 3.13
dict_build_get: 20,000 str keys, 200,000 get calls18.47 s0.31 s0.032 s
set_insert_contains: 20,000 inserts, 200,000 in tests24.69 s0.038 s0.008 s
int_list_sum: for over 100,000 int64 values, 20 times1.96 s0.048 s0.048 s
string_list_scan: 10,000 str compared, 100 times1.60 s0.11 s0.018 s
record_field_reads: doc.get("age") on 5,000 dicts, 20 times0.80 s0.15 s0.007 s

Before is 5f68c34. After is the Batch 7a runtime changes. The programs live in benchmarks/runtime_values/, and scripts/bench-runtime-values.py runs them.

The same programs against Rust 1.95 ran later in the same kind of container, at aa52bfc. Both columns time the whole process. The Rust programs use std collections and checked arithmetic.

ProgramAuraRustAura / Rust
dict_build_get321.6 ms21.3 ms15.1x
set_insert_contains42.1 ms6.1 ms6.9x
int_list_sum43.7 ms2.3 ms18.9x
string_list_scan120.3 ms2.7 ms44.8x
record_field_reads145.0 ms9.3 ms15.6x

Process startup is about 2.0 ms for Aura and 1.2 ms for Rust, so the two list rows understate the gap inside the loop.

The AuraDB document database benchmark ran in the same container:

AuraDB operationBeforeAfterPython 3.13
insert_many, 5,000 documents in memory72.6 µs/op9.4 µs/op0.67 µs/op
get by id, 5,000 documents45 µs/op5 µs/op5.8 µs/op
find by field scan, 5,000 documents188 ms54 ms13.9 ms
open and load, 5,000 documents1,252 ms201 ms14.2 ms
update by id, 2,000 documents7,895 µs/op20 µs/op6.0 µs/op
delete by id, 2,000 documents57,990 µs/op6,250 µs/op381 µs/op

A list, dict, or set copy shares its storage until one side writes, so AuraDB's copy of its document list on each update costs constant time. A value that only one task thread can reach is read and written without a lock. AuraDB's delete recomputes every document's position key on each call, and that loop dominates its row.

The AuraDB timer has 1 ms resolution, so the get row is coarse.

The raw reports keep every observation.

Build Times ​

scripts/bench-compile-time.py times aura build with the direct backend from an installed runtime. These medians come from the same container at aa52bfc:

Programaura buildaura checkFinal link
examples/basics/hello_world.au0.32 s0.004 s0.23 s
examples/agents/tool_runner0.62 s0.057 s0.21 s
examples/io/unix_tls_roundtrip.au0.40 s0.009 s0.21 s

Checking takes under a tenth of each build. Native code generation and the host C compiler's link take the rest. aura build writes a new executable every time and does not read the native cache. aura run reuses a cached executable when the program is unchanged.

Optimization Level ​

This comparison is from the foundations session. It predates the Array kernel rewrite measured above.

The same-session before and after comparison includes three changes: Cranelift speed, Cargo release-profile tuning, and user link and strip changes. It does not isolate the causal effect of the optimization flag. Protocol rows use GO-to-DONE medians. Integer rows use whole-process medians. Array rows use per-operation medians. A positive change means slower.

Exact workloadAura beforeAura afterChange in time
Naive recursive fib(30)110.397208 ms89.359292 ms-19.0566%
Create and join 10,000 tasks100.303375 ms98.692417 ms-1.6061%
20-client delayed loopback TCP fan-out104.534666 ms104.268375 ms-0.2547%
16-cycle retrying HTTP worker427.788875 ms428.814667 ms+0.2398%
int32 whole process36.445833 ms28.954291 ms-20.5553%
int64 whole process15.148792 ms12.772500 ms-15.6863%
Array add, per operation1.124738 ms1.205427 ms+7.1740%
Array sum, per operation1.149820 ms1.148677 ms-0.0994%

The foundations changes reduce the fib and integer medians by about 16–21%. The task, network, and retry comparisons move much less. Array addition is 7.1740% slower, and Array reduction is essentially unchanged.

The same-session controls:

ControlBefore medianAfter medianDrift
CPython Naive recursive fib(30)158.086083 ms158.321541 ms+0.1489%
CPython Create and join 10,000 tasks51.524875 ms51.786208 ms+0.5072%
CPython 20-client delayed loopback TCP fan-out108.592667 ms108.793000 ms+0.1845%
CPython 16-cycle retrying HTTP worker521.302291 ms522.391625 ms+0.2090%
CPython int319.649209 ms328.618208 ms+2.8059%
CPython startup22.874500 ms23.895667 ms+4.4642%
NumPy Array add, per operation0.247242 ms0.247190 ms-0.0208%
NumPy Array sum, per operation0.169047 ms0.168981 ms-0.0394%

Every control drifts by less than 5%, including CPython startup. No repeated before and after pair was needed. The measurements and dispersion from this M2 Pro host feed later performance decisions.

Current Performance Gaps ​

WorkloadAura compared with its reference
fib29.664884 times the exact Rust comparison on the foundations host; in 0.6.0, about 5.8 times on Linux and 14.5 times on macOS arm64
Task creation29.068501 times the exact Rust comparison
TCPnear parity
Retryfavors Aura
Array addition1.006974 times NumPy, 1.006524 times the same-session Rust kernel
Array reduction6.682308 times NumPy, 1.339087 times the same-session Rust kernel
record_field_readsabout 11 times Rust
string_list_scanabout 5.3 times Rust
dict_build_getabout 4.4 times Rust
set_insert_containsabout 3.4 times Rust
int_list_sumabout 3.7 times Rust
AuraDB update by idabout 3 times Python 3.13

These gaps are specific to these workloads. Scheduling, runtime implementation, allocation, and optimizer-barrier choices affect how to read them.

Phase 1 Release Check ​

Aura 0.6.0 ran against the Rust references on two hosts: the shared Linux x86-64 container and a macOS arm64 Mac14,9. Each figure is the median of 11 alternating runs. fib(30) times GO to DONE, and the other programs time the whole process. The Rust references build with Rust 1.95.0.

ProgramLinux, Aura / RustmacOS, AuramacOS, RustmacOS, Aura / RustPhase 1 target
fib(30)5.8x42.65 ms2.95 ms14.5x8x
int_list_sum3.7x8.09 ms3.98 ms2.0x5x
string_list_scan5.3x13.05 ms3.65 ms3.6x10x
dict_build_get4.4x84.08 ms15.26 ms5.5x8x
set_insert_contains3.4x13.98 ms5.07 ms2.8x5x
record_field_reads11.0x67.02 ms5.01 ms13.4x8x

Four programs meet their phase 1 targets on both hosts. record_field_reads misses on both. fib(30) meets its target on Linux and misses on macOS arm64. Phase 2 of Batch 7 owns both gaps.

Per-Call Attribution ​

Eleven symbol-preserving xctrace recordings on the foundations host attribute recovered Fibonacci samples as follows. They measure aa52bfc and earlier, before calls used static frame descriptors.

CategoryShare of recovered samples
Call-frame push/pop and metadata validation49.29%
Other runtime work37.38%
Scalar argument and result moves6.28%

The boundary note has all eight categories and the sample exclusions.

Call Frame Cost ​

Each generated function has one static frame descriptor. It holds the function name, source path, and position. The runtime validates the descriptor's text on the function's first call. Each call then records one pointer and the current task's state comes from a cached pointer. A call that does not hand a view back to its caller stores nothing else.

Callgrind counts the instructions of the whole fib(30) process:

BuildInstructionsPer recursive call
aa52bfc1,917,708,454about 1,150
Static frame descriptors568,735,408about 340

On the shared Linux container, 15 alternating GO to DONE runs give a fib(30) median of 129.3 ms at aa52bfc and 34.7 ms with descriptors. The Rust reference takes about 4 ms. Creating and joining 10,000 tasks is unchanged at about 446 ms.

Loop Element Reads ​

A shared for loop over a local list reads each element in place when the element is a str, list, dict, or set and the body uses it only in these ways:

  • as a side of == or !=
  • as the receiver of len()
  • as the receiver of get(key), for a dict element

Any other use, such as passing the element to a function, storing it, or capturing it in a lambda, makes the loop copy each element as before.

Aura
def count_matches(names: list[str], wanted: str) -> int64:
    mut hits = 0
    for name in names:
        if name == wanted:
            hits += 1
    return hits


def main():
    print(count_matches(["ada", "bo", "ada"], "ada"))

On the shared Linux container, 11 alternating whole-process runs give these medians. Before is aa52bfc.

ProgramBeforeAfter
string_list_scan115.0 ms32.8 ms
record_field_reads149.9 ms122.9 ms
dict_build_get298.2 ms283.4 ms
set_insert_contains33.6 ms33.0 ms
int_list_sum44.7 ms45.6 ms

record_field_reads still spends most of its time looking up the key and matching the json.Value it finds.

List Reads ​

Native code reads a list's length and its int64, float64, and bool elements inline. Each read checks the index once against the length. A list that another task can reach, an index out of range, or an integer too large for int64 goes through the runtime, which reports the same error as before. Appending one of those scalars to a list stores it without a box.

Callgrind counts the instructions of each whole process. Before is the loop element reads above.

ProgramBeforeAfter
int_list_sum444,958,513106,060,271
string_list_scan363,824,369365,899,833
record_field_reads1,137,789,3391,154,420,211
dict_build_get2,998,211,9513,049,793,892
set_insert_contains347,380,497349,860,346

int_list_sum went from a median of 47.3 ms to 23.9 ms over 11 alternating runs, about 10 times the Rust reference. The other programs run up to 1.7% more instructions. Each boxed value keeps its list's element address and length, and every write to a box updates them.

A shared for loop over a list or a span reads each element with no bounds check and advances its index with no overflow check. The loop already compares the index with the length, and the list cannot change length while the loop borrows it. int_list_sum runs 78,654,607 instructions instead of 106,060,271.

An Array with one dimension, indexed by one int64, is read inline the same way, with one bounds check. A loop that reads each of 100,000 float64 elements by index, 20 times, runs 63,478,022 instructions instead of 3,445,483,320, and takes about 10.8 ms instead of 367 ms. An Array with more dimensions reads through the runtime.

A list whose elements are all int64, all float64, or all bool values stores them unboxed. Unboxed Scalar Lists gives the details.

Integer Array Kernels ​

The int32 and int64 Array operators, their wrapping_ and saturating_ forms, and sum() run on machine integers in blocks of 256 elements. Checked mode computes a whole block, then scans a block that overflowed for its first failing element. So AU4002 names the same flat index as an element-by-element loop. sum() adds a block at once when its largest value cannot push the running sum out of range. Otherwise it adds that block element by element and reports the first prefix that overflows.

Each program below repeats one operation 100 times on one-million-element arrays. Each time is the fastest whole-process run on Linux x86-64, of three runs before and five after. Times include building the inputs, about 45 ms.

OperationBeforeAfter
int64 left + right3,724 ms138 ms
int32 sum()3,974 ms47 ms
int64 sum()4,080 ms100 ms
int32 wrapping_mul2,935 ms58 ms
int32 max(), unchanged68 ms52 ms

Unboxed Scalar Lists ​

A list whose elements are all int64, all float64, or all bool values stores the scalars themselves. Each int64 or float64 element takes 8 bytes, and each bool takes 1 byte. A boxed element takes 112 bytes. Any other list stores boxed values.

The storage follows the elements. Adding a value of another type, such as a str to an int64 | str list, moves the list to boxed storage. An empty list takes the form of its first element. A program cannot tell which form a list uses: printing, equality, hashing, copies, and errors behave the same.

Native code reads the length and the elements of an unboxed list in place. A list that another task can reach goes through the runtime.

On the shared Linux container, 11 alternating whole-process runs give these medians. Before reads lists as the List Reads section describes, with boxed elements.

ProgramBeforeAfter
int_list_sum21.0 ms9.5 ms
string_list_scan32.5 ms31.6 ms
record_field_reads122.0 ms122.5 ms
dict_build_get326.3 ms316.5 ms
set_insert_contains40.8 ms41.7 ms

int_list_sum runs about 4.1 times the Rust reference. Callgrind counts 73,209,689 instructions instead of 78,657,685, so most of the gain comes from the smaller list fitting in the processor cache. The other programs run within 1% of their previous instruction counts.

Strings, Keys, And Membership Without Boxes ​

Native code builds an f-string in one string. The first literal starts it, and each later part appends in place. A number or bool part is copied when the f-string evaluates it and rendered as it appends, because rendering it runs no Display code. An int64 part appends without a box.

A literal str key, as in doc.get("age") on a dict[str, V], is looked up where it lies in the program image. An int64 needle, as in j in seen on a set[int64], is tested without a box. A for loop that lends each element of a list checks the index against the length once, so the loan begins without a second check. Native code also skips a type tag that repeats the tag the same value received earlier in the block.

On the shared Linux container, 11 alternating whole-process runs give these medians.

ProgramBeforeAfterInstructions beforeInstructions after
int_list_sum11.6 ms10.8 ms73,209,65873,212,011
string_list_scan35.7 ms14.1 ms370,041,432150,970,171
record_field_reads127.3 ms84.4 ms1,173,189,605692,557,847
dict_build_get322.4 ms89.3 ms3,080,859,389939,551,832
set_insert_contains39.6 ms18.0 ms351,223,140148,691,758

record_field_reads still copies each Lookup payload and each enum name, and every box passes through the task's ownership ledger.

Performance Direction ​

Performance work will address task creation and scheduling, native loop and call-boundary optimization, allocation and copying, and numeric kernels. It will keep ownership, checked arithmetic, deterministic reductions, and MIR/direct parity. The Array kernels run Rust runtime code. Replacing the native emitter alone does not solve these gaps.

Evidence And Reproduction ​

The foundations session evidence directory keeps:

  • unchanged raw and summary JSON
  • commands
  • compiler, source, and binary identities
  • interpreter, toolchain, boot, and host records
  • warmups, observations, and dispersion
  • hash rechecks and failure logs

Eleven ten-minute host retries preceded the earlier independent runs. For the approved continuation, the host checks were quiet immediately. No timed observations were collected while the host check refused a run, and no override was used.

The session SHA256SUMS covers every evidence file. The initial partial manifest is kept as initial-SHA256SUMS. Each summary links its raw report by SHA-256. Raw reports keep their original temporary paths. The copied basenames below identify the versioned files.

EvidenceSHA-256
approved/aura-foundations-after-release-raw.jsona2a31328af601c32783519d9658ab164d9b415046b03b5b7856b6e09c4c79441
approved/aura-foundations-before-release-raw.json16aead5c0c9fe73ff2155e66b74edf982c8ec81958ab7ae3b4b2a6ae82498ec7
aura-foundations-after-arrays-raw.json58128b5331777b8a868aae952bbc21b02145a54331f233d02efbb69b4becd22f
aura-foundations-before-arrays-raw.json6f1f1c3b2d3fa288785204d54da2ec507a25a8c20e234584d72f8afe51c950fd
aura-foundations-after-integer-loops.json8747324a496b9280eb10bf54013b5dd06d358d3a4acf11772501b6b7dbf60dfb
SHA256SUMS911dc4a7901357b33679ad260923c56d0c8216440b200a0eb12e426ea60cac67

Get the exact corrected sources from the verified Git bundle. The repository must already contain the two base commits listed above:

bash
git bundle verify work/2026-09-07-foundations-measurements/approved/approved-measurement-commits.bundle
git fetch work/2026-09-07-foundations-measurements/approved/approved-measurement-commits.bundle refs/heads/codex/foundations-measured-after
git worktree add --detach /tmp/aura-foundations-after FETCH_HEAD
git fetch work/2026-09-07-foundations-measurements/approved/approved-measurement-commits.bundle refs/heads/codex/foundations-measured-before
git worktree add --detach /tmp/aura-foundations-before FETCH_HEAD

Build each release compiler with Rust 1.95.0 before timing. In the after checkout, build the Rust references and run:

bash
cargo +1.95.0 build --release --locked -p aura
cargo +1.95.0 build --manifest-path benchmarks/rust_baselines/Cargo.toml --release --locked --bins
npm run bench:release-performance -- --label foundations-after-rust --aura target/release/aura --python /Applications/Xcode.app/Contents/Developer/usr/bin/python3 --pairs 11 --raw-json /tmp/aura-foundations-after-release-raw.json --summary-json /tmp/aura-foundations-after-release-summary.json

In the before checkout, run its own release runner with label foundations-before and before-specific output paths. Compiler and runner sources stay frozen during each run.

The original-base Array and standalone integer commands are:

bash
python3 scripts/bench-direct-integer-loops.py --aura target/release/aura --repeats 11 --raw-json /tmp/aura-foundations-after-integer-loops.json
python3 scripts/bench-numeric-arrays.py --label foundations-after-rust --aura target/release/aura --python /Applications/Xcode.app/Contents/Developer/usr/bin/python3 --pairs 11 --raw-json /tmp/aura-foundations-after-arrays-raw.json --summary-json /tmp/aura-foundations-after-arrays-summary.json

For the before Arrays, use the original tag's own runner with the same interpreter, 11 pairs, label foundations-before, and before-specific JSON paths. The before standalone integer runner supports only --repeats 11. Keep the measurement families and their source identities separate.

Representation ​

These measurements count allocations with the runtime's own counters, not a host allocator. On both backends, the runtime counts every union payload box, closure environment, opaque runtime box, and callable overflow allocation. A process run with AURA_RUNTIME_STATS=1 prints the totals on standard error when its program ends.

The same line reports how often the direct backend retains, releases, and copies a boxed value, as retains, releases, and clones. The direct runtime counts these only when the report is requested. The interpreter reports zero for them.

benchmarks/representation holds the programs. scripts/bench-representation.py records the counters, the direct binary's size, and wall time, with commit provenance. The table shows the counters, which are deterministic for these programs. They were measured at a8aac70a and archived with a checksum manifest in work/2026-09-20-representation-measurements.

ProgramInterpreterDirect backend
union_scalar_local (one million int64 | None injections and tag tests)2,000,000 union boxes0 union boxes
union_class_field (one hundred thousand Point | None class fields)100,000 union boxes0 union boxes
union_string_local (one hundred thousand str | None injections and matches)100,000 union boxes0 union boxes
callable_pack_inline (one hundred thousand one-capture packings)100,000 environments0 environments
callable_pack_overflow (one hundred thousand four-capture packings)100,000 environments0 environments, 100,000 overflow blocks
callable_move (one packing moved one hundred thousand times)1 environment0 environments

Unions. On the direct backend, every concrete union is one tagged value in registers and locals. A runtime-object member rides along as an owned handle word. Injections allocate no union box. The interpreter's box counts are reference numbers for its Value model, not an ABI claim.

Callables. On the direct backend, a callable value is four words.

  • Captures that fit in three words live in the value.
  • A wider environment takes one checked block, counted as a callable overflow allocation.
  • Packing, calling, and moving allocate no closure environment.
  • The direct backend boxes a callable only where it leaves generated code: a container, a task start, a runtime helper, or a generic frame. The closure-environment counter reports these boxes.

Design record: first-class callables and binding contracts

Executable Size ​

A native executable contains the Aura runtime code its program can reach. A program that never uses TLS, WebSocket, TOML, or JSON does not carry their libraries. The counts below are exact bytes from release builds that include unboxed scalar lists. The size gate records them as its baselines.

ExecutableLinux x86-64macOS arm64
Native hello world973,016961,496
Reference agent (tool_runner)1,915,0001,792,816
JSON example1,506,4561,431,448
TLS example3,303,2642,860,000
WebSocket example3,608,7523,137,448

On Linux x86-64, the aura compiler is 17,571,560 bytes and the runtime archive libaura_runtime.a is 62,558,760 bytes. User executables link the runtime archive. It contains no compiler code.

The JSON example is examples/json/dynamic_values.au. The TLS example is examples/io/unix_tls_roundtrip.au, and the WebSocket example is examples/io/websocket_roundtrip.au. Each links only the libraries it uses:

ExecutableLinksDoes not link
Hello worldruntime coreserde_json, toml, http, httparse
Reference agent, JSON exampleruntime core, serde_jsontoml
TLS exampleruntime core, rustls, ringserde_json, http, httparse
WebSocket exampleruntime core, rustls, ring, tungstenite, http, httparse, urlserde_json

Hello world still holds about 11 KB of rustls, ring, and tungstenite cleanup code. The runtime's value type can hold a TLS or WebSocket connection, so the code that drops one stays reachable.

The compiler profile uses level 3, fat LTO, one codegen unit, and no debug data, and strips compiler symbols. It keeps unwinding and Aura diagnostic metadata. User executables use unused-section removal and debug and local-symbol stripping. Each generated function has its own object section, so the linker drops runtime code no function calls.

Hello world and the reference agent ran with Cargo unavailable. Hello world matched its single-print output. The agent matched every byte of its package's pinned stdout.

Linux size provenance records the commit, commands, toolchain, linker, source identities, and executable SHA-256 hashes. The macOS counts come from the native-binaries CI job on the same tree. That job builds every subject in benchmarks/native_binaries/contents.json on both platforms. It fails when a library appears in a program that should not link it, or when hello world or another subject grows more than 10% over its recorded size.

Foundations Size Comparison ​

This comparison is from the foundations session and predates the Array kernel rewrite. The older tag has no equivalent tool_runner package, so the table claims no before-size reduction for the reference agent.

ExecutableBefore: v0.3.3-preview, Cargo defaultAfter: Cargo defaultAfter: tuned releaseReduction from before
aura compiler15,424,03215,866,00810,897,39229.35%
Native hello world23,646,7921,702,0881,586,96893.29%
Retrying-network-worker example23,715,8164,049,2403,666,60084.54%

The after-default control sets the documented release values through CARGO_PROFILE_RELEASE_* in a separate target directory. It keeps Cranelift speed and the user-executable link and strip steps. The before ref is v0.3.3-preview (d3cc6b96104dd597687a98e9624f800a0cb3cf1e). The original size evidence keeps the exact earlier commits, hashes, and commands.

Both size sessions use:

  • Mac14,9 / Apple M2 Pro, arm64 macOS 26.5.2
  • Rust 1.95.0 (59807616e, LLVM 22.1.2)
  • Apple clang 21.0.0 (clang-2100.1.1.101)

Reproducing The Size Table ​

Reproduce the hello world, reference agent, and compiler sizes from a clean checkout of the desired ref:

bash
python3 scripts/bench-binary-size.py \
  --after-ref "$(git rev-parse HEAD)" --after-only \
  --output /tmp/aura-executable-sizes.json

Measure every subject and check its contents against a local release build:

bash
cargo build --release -p aura
python3 scripts/check-native-binaries.py

Add --attribute to print the symbol bytes for each library.

To collect the before, default, and tuned profile comparison, omit --after-only and pass --before-ref v0.3.3-preview. The script records that the older reference-agent subject is absent. It never substitutes a different program. The hello input is identical single-print source. It is staged outside old source trees that predate its maintained path.

Cargo 1.95 Default-Profile Check ​

The pinned Cargo reference lists strip = "none" in the release defaults, not debuginfo. Under the session's conditional decision, the size column, source JSON, and size script are kept without remeasurement.

The column sets strip=none explicitly. It is not an experiment with the setting omitted. When the option is omitted and no compiled package needs debug information, Cargo's deferred strip implementation can strip pre-existing debug information automatically. That difference limits how far the column works as a default-profile control.

The reference check records Cargo's exact version, the reference URL and source hash, the relevant lines, and this decision.

Aura 0.6.0 technical preview. Implementation baseline: 4d7e076de9ff6e2e056cdec10881d4bf421358de.