Completed compiler performance and SIMD work
Completed-work index · Remaining work
These are delivery/investigation records, not current benchmark guarantees or an optimization queue. Workloads, methods and current measurements belong to profiling; remaining B19/B23/B74 scope belongs only to the backlog.
B19: Generated code and storage
- The CPU audit (
examples/cpu_codegen_audit/, local PR #431) led to issues #360–#392.--dumpexposes operations/CLIF/assembly (issue #360). Unborrowed locals stay in registers, narrow helpers replace dispatcher calls in borrowed loops/byte/text operations, last owned reads move, held text avoids retain/ release, and SSE2 rounding is inline (issue #386). - Separate
plenty_op_<N>symbols permit native section GC. The measured stripped hello executable fell from 790 KB to 356 KB (issue #501, local PR #577). Default stripping and lean runtime linkage are also recorded under B17. - Dense integer/tag matches use bounded jump tables at 256+ distinct values (issue #395; measurements). String matches with 16+ literals binary-search one sorted table (issue #565; measurements); other shapes retain chains.
- Inline class field copies/address rewrites and destructor-free release for up to eight owned fields shipped (issue #544, local PR #663). One-field destructor-free classes use their field representation (issue #547, local PR #666); class fields are flat and strings release in place (local PR #668).
- Inline copies/clears up to 192 bytes, in-place moved arguments and descriptor string-offset cleanup plans shipped (issue #550, local PR #669). Class components of tuples/multifield variants are flat; single-field Option/Result payloads keep the address layout (issue #551, local PR #670). See flat-layout evidence.
- Release builds inline small constructors/
__init__/new, avoid release of fields initialized first and omit provably zero releases (issues #548/#549, local PRs #665/#667). - Tail
list.pop()avoids survivor relocation; other positions repair only the suffix (issue #52). Numeric/bool rows use natural 1/2/4/8-byte widths and lists omit the dictionary value slot (issue #531). - Borrowed integer/float/bool list rows use inline reads/writes with unchanged bounds reporting. The recorded 3k-line dissimilar diff fell from 265 to 100 ms (issue #509, local PR #598; measurements).
- Collection access/removal, equality and string indexing work shipped (issues #18–#21). Layout and runtime constraints still follow the reference, not these snapshots.
B19: Compiler scale and caching
-
Warm single-file executable hits skip bundled-runtime extraction (issue #699). The measured no-op shebang median fell from 16.35 to 6.56 ms while preserving source/toolchain/native-input checks. See the startup measurements and method.
-
Cached collection element facts (issue #6), linear declaration/large-enum match checking (issues #53/#71), parallel native emission and the large-function single-pass allocation fallback (issues #55/#56) shipped. More than 2,048 branch arguments select the fallback with a code-size/speed tradeoff.
-
Operation checking/native IR construction/compilation run across cores with byte-identical output for any worker count (local PR #131); lookup, frame validation, tail-call preparation and adapter scans also parallelize (local PR #136). Declarations resolve once (issue #59, local PR #158).
-
Compiler peak memory was measured at about 1.8 KB/source line for checking and 2.8 KB for object emission after exact-length syntax lists/40-byte tokens, duplicate-body removal and earlier release of tokens/operations (issues #422/#54). Whole-program check work is linear in instructions and std imports no longer enable per-function whole-program work (issue #57).
-
Content-addressed compiled-function reuse shipped (issue #144, local PR #176). Buffered cache hashing/shared decoded stencils reduce repeated work (issue #316). Whole-object cache validation includes module text/import resolution (issue #317 stage 1, local PR #385).
-
Generic naming is linear; template/program instance limits and dependency depth bounds permit finite declaration chains (issues #428/#430/#435, local PR #516). Templates release when no pending lowering needs them, reducing generic/ordinary checking memory from 1.92× to 1.37× in the recorded 200×200 case (issue #434, local PR #521). Instance-time substitution, moved template bodies, template-ID cache keys, symbol-keyed requirements and cheaper parser tokens reduced the checking instruction ratio to 1.37× (local PR #534).
-
Cranelift 0.137’s reused regalloc2 context reduced debug compile instructions 4.2–4.7% in recorded workloads (issues #153/#421, local PR #528). Reusing cleared Cranelift Functions reduced ordinary construction allocation from 20.3 to 4.9 KB/function (measurements).
-
Batched codegen results preserve deterministic output with fewer messages (local PR #368). Mimalloc removed glibc arena-lock contention, so the measured 16-thread run was no slower than eight (issue #315, local PR #386). Linker preference chooses mold/lld while preserving overrides (issue #314, local PR #367).
Compiler architecture assessment
A source-based refactoring proposal
records the compiler boundary assessment, file/symbol maps, alternatives and
validation gates. Its six steps are delivered: structured diagnostics (issue
#691), scoped recorder and teardown state (#692), the op and syntax-layer
splits (#693, #694), grouped pipeline state (#695) and a frontend lookup
profile that changed no representation (#696). The legacy stack syntax is
removed (#698). No speedup is claimed; remaining scope stays in the backlog.
B19: Profiling and concluded investigations
--features profile exposes hotpath timing/calls/allocations per phase
(issue #150, local PR #188); finer lowering/ownership/native rows and isolated
compile_bench workloads supported investigations #301–#305 (local PR #343).
| Investigation | Recorded outcome |
|---|---|
| #301 | Avoiding 128-bit slot split/join shifts halved AOT instructions and reduced register-allocation work 70% (local PR #351). |
| #303 | Borrowed loan-graph operations and liveness-buffer reuse cut ownership-check instructions 40% (local PR #352). |
| #302 | Shared token/nominal names first cut checking instructions 8.8% and allocations 25% (local PR #357); shared structural/callable children and FxHash tables brought the broader check-workload reduction to about 20% (local PRs #383/#389). Investigation closed. |
| #305 | The mixed-workload checking ratio was attributed to cache/TLB misses, not algorithmic work. |
| #304 | Warm-cache work was measured at roughly 45% frontend and 18% linking; linking experiments record levers. |
| #59 | Parallel parsing, speculative lowering and declaration parallelism were evaluated but not adopted: small gains and higher peak memory. Declaration reuse landed. |
| #321 | Mimalloc and skipped command-line teardown were retained; arenas/bump allocation were measured and not adopted (local PR #388). |
| #317 | Stored syntax trees/separately compiled module artifacts did not pay; the investigation identified imported-interface lowering as a prerequisite for a useful lowered-module cache (local PR #392). See module-cache investigation and module-edit baselines. |
| #436 | Substitution-environment lowering offered at most 2.6% and was set aside. Instance registration/header tokens explain the remaining generic gap. Closed. |
| #437 | Code placement affects SIMD measurements; retained placement research, no compiler fix. Closed as investigated/deferred. |
| #440 | Cranelift alias regions gave no gain and no sound general partition; reference targets can span frames/rows/fields/payloads. test_memory_order guards against unsound regions (local PR #529). |
| #441 | Cranelift 0.137 dead-store elimination was qualified against call/atomic cross-thread handoffs; release concurrency/coverage tests and >1 GiB frame diagnostics guard the contract (local PR #530). |
These outcomes are not active claims. Any revisit follows B19 in the backlog, with new evidence.
B19: Traps and arithmetic lowering
One object-wide trap-site table and cold function check blocks reduced the
recorded 200×200 object size 34% and single-core emission 9%
(issue #420, local PR #505). Debug cmp; jcc overflow checks reduced that
benchmark’s text 5.5% (issue #425, local PR #525). Cranelift 0.137 fuses unsigned
add/all multiply checks and keeps pooled float-mask lanes in registers
(issue #439, local PR #528).
PC-relative trap tables and executable ud2/SIGILL resume-to-report helpers
reduced recorded text 14% debug/11% release and compile instructions 7–9%
(issue #425, local PR #531). Libraries/fuzz/coverage retain helper calls.
Issue #425 was closed as deferred to upstream changes, not as proof that every
check/frame/spill cost disappeared. The runtime reference
owns the contract; upstream revisit gates are in B19 of the backlog.
The Linux foreign-handler fixture repeats signal delivery until the tested read
completes, instead of assuming a scheduling delay puts the reader in the syscall
(issue #689). Restarting reads receive a pipe byte after repeated signals;
non-restarting reads must report EINTR, with a bounded watchdog. Delayed-entry
regressions retain the later checked-overflow report in debug and optimized builds.
This changes test synchronization, not runtime handler behavior.
B23: SIMD
Automatic integer-column filter/count
uses baseline four-lane i32/u32 SIMD with scalar tails in optimized builds.
One/two total comparisons joined by and, inclusive/exclusive ranges, reversed
operands and up to two immutable same-type scalar captures are supported.
The initial std.simd module provides four concrete numeric vector shapes, masks, checked/wrapping arithmetic, selection, safe list access and scalar fallback. Bounded lowering/local promotion avoid repeated materialization in supported kernels. Direct integer wrapping reductions avoid the spill reproduced in issue #444. These are delivered subsets, not automatic packed generic arrays or a backend switch. Remaining extensions stay B23/B76 in the backlog. This delivers the initial SIMD stage of the memory and parallelism assessment, which remains partly implemented for its public allocator design.
B74: Collection hashing
Named enums with supported key payloads now use structural hashing in sets and dictionary keys. Variant tags and all payload fields participate; nested named enums, tuples and classes, including custom cleanup, are supported. Anonymous sums with supported members also qualify; their active member tag and structural value define identity. Floats, boxes and recursive types remain excluded. These general key types use keyed runtime hashing. Supported finite enum and tuple domains with at most 256 values use the bitset specialization described here. See collections.
Collections use SipHash-2-4 with a per-process OS key (issue #329; issue #747
later moved to SipHash-1-3 with direct str, bytes and scalar key paths), and record
traversal performs linear field-offset work per pass (issue #330). The built-in KeyType constraint now exposes the same key admission rule on
functions, methods, classes and enums; see generics.
Custom-cleanup keys use the same structural identity without making their
cleanup obligations copyable.
Class keys hash their fields rather than their addresses. Move-only class keys,
and enums containing them, transfer on insertion and owned extraction; lookups
and views borrow them. Set algebra and key-view to_set() create independent
copies, and affine keys_snapshot() requires an owned temporary, matching
values_snapshot(). Keys containing custom cleanup support borrowing,
transferring updates, in-place filtering and owned extraction. Non-mutating
algebra, to_set() and explicit copying reject them. Duplicate insertion keeps
the existing key owner and destroys the incoming owner. Regression tests cover
nested cleanup, generic boundaries, duplicate ownership and allocation failures.
Small enum sets
The initial specialization used one membership word for payload-free named enums with up to 64 variants, without entry/order/hash buffers or entropy reads. The wider implementation extends this to 256. The collection owner still allocates once. Membership, length, insertion/removal, same-type set relationships and algebra use bit operations. Shared iteration uses immutable canonical enum values, while normal source loans remain enforced. Mixed operations with dictionary key views retain their existing semantics.
Allocation-failure tests cover the remaining owner allocation and allocation-free
mutation; boundary tests cover 1, 63, 64 and 65 variants. These initial
measurements cover payload-free enums at revision f4aba2de. Reproduction at
that revision is
cargo build --locked --bin plenty (without instrumentation features), then
python3 scripts/bench_enum_sets.py target/debug/plenty --output /tmp/enum-sets.json:
the same first 64 variants are exercised in a 64-variant bitset and a 65-variant
hash set. It checks equal results and alternates execution order. Native programs
use --release; measurements are observations, not performance assertions.
Measured on x86-64 AMD Ryzen 7 7840HS, with 200,000 rounds and median of five runs per case. Each lookup round probes 64 values; each mutation round adds and removes all 64. Union combines two disjoint 32-member sets; copy duplicates a 32-member set. These timings describe these workloads on this machine, not a general speed guarantee.
| Workload | Bitset (seconds) | Hash fallback (seconds) | Speedup |
|---|---|---|---|
| Lookup | 0.081969 | 0.357319 | 4.36× |
| Add and discard | 0.339045 | 1.598463 | 4.71× |
| Union | 0.012670 | 1.210755 | 95.56× |
| Copy | 0.013672 | 0.461085 | 33.72× |
Finite payload enum sets
The finite-payload stage (82bd5231) extended bitsets to named enums and tuples
whose complete domains contain at most 64 values, recursively composed of booleans and named enums/tuples.
The compiler checks sums and products against the limit before generating
canonical values and variant offsets. Membership encodes the active payload with
mixed-radix indexing; algebra still operates directly on the mask. Immutable,
16-byte-aligned metadata includes nested inline records, preserving stable shared
references without runtime caches or allocations. The runtime type descriptor
now carries an optional finite-domain descriptor.
Tests cover nested multi-field enum/tuple values, every member of a 64-value
domain, the 65-value hash fallback, singleton/Option payloads, mixed dictionary views,
copying and iteration after source destruction, and allocation failure. The
benchmark at that revision also supports --payload to compare a six-boolean variant (64 values)
with the same variant plus an unused extra case (65 values). Integer/string
payloads and classes keep hash storage; the wider implementation
extends the finite-domain threshold to 256.
On the same AMD Ryzen 7 7840HS, 200,000 rounds and medians of five runs with
--payload produced the following observations. The workload counts match the
payload-free benchmark, but each key now carries six boolean fields.
These are measurements of these workloads, not general speed guarantees.
| Workload | Finite bitset (seconds) | Hash fallback (seconds) | Speedup |
|---|---|---|---|
| Lookup | 0.584369 | 1.961403 | 3.36× |
| Add and discard | 1.594221 | 7.373249 | 4.62× |
| Union | 0.011405 | 4.927148 | 432.02× |
| Copy | 0.015694 | 1.980686 | 126.21× |
Wide enum bitsets
Finite enum and tuple domains select their mask width from the complete type domain: one 64-bit word for 1–64 values, two words for 65–128, and four words for 129–256. Operations dispatch to the selected width, including scalar membership, popcount and algebra for the smallest domains. Clearing or draining preserves the selection; sparse membership never narrows it. All alternatives fit within the existing insertion-order storage, so collection owners do not grow. The canonical payload-free tag table grows from 64 to 256 entries (one shared 4 KiB table); finite payload metadata remains proportional to domain size and inline layout. Wider masks do not change key admission or dictionary storage. No target-specific SIMD is needed.
Boundary tests cover 64/65, 128/129 and 256/257 values, including hash fallback. Randomized membership and mixed-end iteration tests exercise every word; native checks cover nested enum/tuple payloads, set algebra, dictionary views, copied values, owner failure and allocation-free mutation.
Requested heap storage measured with the runtime’s actual buffer capacities (excluding allocator overhead and immutable type metadata):
| Members | Bitset bytes | Payload-free hash-set bytes |
|---|---|---|
| 0 | 176 | 176 |
| 4 | 176 | 496 |
| 64 | 176 | 4,272 |
| 128 | 176 | 8,368 |
| 256 | 176 | 16,560 |
Both owners are 176 bytes on this x86-64 build. The hash comparison uses a
257-variant enum and inserts the same tags without pre-reservation. Payload
hash rows can require more storage; canonical payload data is shared per type,
not part of these per-owner heap figures. For example, one eight-boolean variant
emits 256 × 16-byte canonical slots plus 256 × 128-byte payload records: 36 KiB
of shared value data, plus descriptors and alignment. This can outweigh the
heap saving for a small number of sparse sets; the optimization trades static
data for faster operations and smaller per-instance storage. Reproduce with
cargo test -p plenty-runtime wide_enum_set_storage_measurements -- --nocapture.
The benchmark accepts --size 64|128|256, --payload, --members, and
--workloads. Without a baseline compiler it adds unused variants to force a
257-value hash domain. --baseline-compiler instead compares identical programs
against an older compiler; this also checks the existing 64-value specialization.
Every result is checked, execution order alternates, and JSON records individual
wall and child CPU times as well as medians.
The wide-domain comparison uses the pre-change 64-value compiler, native
--release compilation without runtime instrumentation, and an AMD Ryzen 7
7840HS. Reported ratios use median user-plus-system CPU time over five runs.
Other work was running on the host; CPU time excludes scheduling waits but
cannot remove effects on clock speeds and caches. These are workload-specific
observations, not speed guarantees.
For the full-domain cases, 50,000 rounds each probe all values, add and remove all values, union two disjoint half-domain sets, or copy a half-domain set. Payload cases use seven or eight boolean fields. Sparse cases use four values spread across the 256-value domain (two per input set) and 1,000,000 rounds. Reproduce a full-domain case with:
cargo build --locked --bin plenty
python3 scripts/bench_enum_sets.py target/debug/plenty \
--baseline-compiler /path/to/pre-change/plenty \
--size 256 --payload --rounds 50000 --runs 5 --output /tmp/wide-enums.json
Use --members 4 --rounds 1000000 for sparse cases, and omit --payload for
payload-free enums.
Speedups over the older hash-backed implementation:
| Domain / workload values | Lookup | Add/discard | Union | Copy |
|---|---|---|---|---|
| 128, payload-free | 3.75× | 4.23× | 136.24× | 115.01× |
| 128, boolean payload | 4.15× | 4.46× | 696.99× | 410.92× |
| 256, payload-free | 3.56× | 4.12× | 198.95× | 192.41× |
| 256, boolean payload | 4.38× | 4.48× | 1342.00× | 849.87× |
| 256, four payload-free values | 2.91× | 3.64× | 4.47× | 8.90× |
| 256, four payload values | 3.64× | 3.79× | 26.00× | 22.92× |
Raw measurements retain every wall/CPU sample, medians, settings, compiler hashes and the 64-value comparison. The gains at both wider sizes and in sparse cases support the 256-value cutoff.
The 64-value comparison uses seven alternating runs: 2,000,000 rounds for union and copy, 200,000 for lookup and mutation. Relative throughput changes against the old scalar implementation were lookup +0.3%, add/discard +3.5%, union +1.4%, copy +163.6%. These are measured medians, not guarantees of changes this small. Width is encoded once in the storage alternative, with scalar operations for small domains. Copying reuses the validated representation and allocates only the new owner, avoiding storage reclassification and unnecessary reservation or membership counting.