Copy coalescing, 2026-11-05

The retained change reduces hot CPU time by 5.84–11.23% on the eight loop workloads and 7.57–7.70% on the three collection transformations. Every workload improves in every paired round. Executed instructions decrease by 9.30–19.15%; all measured artifacts and their linked stack requirements also shrink.

The experiment lets the existing typed register allocator remove a Move when its source dies at that instruction and no other program boundary makes the source and destination interfere. It measures newly compiled artifacts on one unchanged native interpreter. The experiment is narrower than general copy propagation across control flow.

Implementation and safety

TemporaryRegisters.compact already builds interference from the exception-aware ValueLiveness boundaries. The candidate omits the source/destination interference edge only at a dying-source Move. Every other boundary, including a later overwrite, continues to contribute conflicts. The ordinary greedy allocator still requires exact semantic type and physical shape; parameters, caught exceptions and unreachable operands keep dedicated slots. A copy is deleted only when the final mapping makes its source and destination the same register.

Move cannot allocate, throw or suspend. Other instructions retain distinct inputs and results, including calls that can retain a pre-instruction GC root map while materializing a result. Multi-component values use the same physical layout and reference-component root rules. No instruction is moved across a call, branch or exception.

Deleting instructions also relocates debug positions. The last source entry at a collapsed boundary survives, its source path/line/column remain attached, and inline-parent IDs become dense again. A copy that anchors an inline debug parent remains as an instruction so its frame has a distinct valid position. Root maps, module/import hashes, manifest requirements and encoded instruction costs are rebuilt by their existing owners. The bytecode encoding, VM, quota rules and native ABI are unchanged. Existing compiled programs need recompilation to benefit.

The real while-indexed condition illustrates one removed copy (register IDs change after allocation):

before: CHECKED_CAST r12, r7; MOVE r13, r12; CALL_INTERFACE r0, size(r13)
after:  CHECKED_CAST r9, r6;                 CALL_INTERFACE r10, size(r9)

Its main loop body still contains copies which this conservative rule cannot prove redundant. Across the complete artifact, moves fall from 101 to 66 and instructions from 393 to 358. Entry registers fall from 16 to 14; the linked stack requirement falls from 4096 to 3584 bytes. All 11 measured artifacts and their stack requirements shrink; these are artifact/storage results, rather than a claim about total computer memory. See the complete before inventory and after inventory.

Paired native results

Baseline compiler: 233fcdbe6b64047d9add5031bc6cc3ac10366f4c. All 11 regenerated baseline artifacts are byte-identical to the earlier frozen baseline. Candidate: that compiler plus the archived source patch. Guest source files, case IDs and expected checksums match across variants. Runtime stays at VM 188699548d6c747772b10e0e758bf8988d22c2e6 (0.24.1). Both variants use the same frozen guest-benchmark binary, verified against the earlier binary SHA-256 and its recorded source/build provenance.

Ryzen 9 9950X3D, Linux x86-64, release Rust with thin LTO, one codegen unit and CARGO_PROFILE_RELEASE_DEBUG=1. Each invocation is pinned to logical CPU 6 (SMT sibling 22). Three rounds of seven samples produce 21 samples per workload and variant, 462 samples total. Variant order reverses in round two; suite order reverses too. No builds, tests or profiling from this task overlap timed trials. The core is not reserved against other processes; frequency and external host activity remain uncontrolled. Environment snapshots accompany commands. CPU times below are pooled medians; negative changes mean less time.

Loops use a 16 KiB Guest heap, collection transformations 256 KiB. Admission, verification, session construction and teardown are outside phase timers; construction has its own timer. The hot phase includes the final scan and output. The harness checks ready output, expected checksum, normal termination, repeated-sample work accounting and agreement between traced control and untraced execution. Counters are stable across rounds within each variant, and host request and response counts agree across variants. Removed instructions intentionally change metered work counters.

Workload Baseline CPU, ms Candidate CPU, ms CPU time change Executed instruction change
while-indexed-256-300 45.832 43.052 -6.07% -9.30%
for-list-256-300 74.221 69.277 -6.66% -11.75%
fold-256-300 77.574 69.553 -10.34% -17.55%
fold-val-256-300 76.965 68.570 -10.91% -19.15%
fold-read-var-256-300 77.121 68.461 -11.23% -19.15%
fold-write-var-256-300 81.291 76.294 -6.15% -11.27%
fold-ref-256-300 78.607 70.768 -9.97% -17.55%
fold-prebuilt-256-300 78.176 73.613 -5.84% -11.44%
map-not-null-1024-100-fresh 260.355 240.305 -7.70% -12.44%
map-not-null-1024-100-reuse-list 269.787 249.362 -7.57% -12.12%
map-not-null-1024-100-reuse-objects 282.821 261.245 -7.63% -12.05%

Raw samples retain wall and thread CPU times, construction times, per-round summaries and all eight work counters. The comparison includes individual round changes. These measurements cover the native Int/list/object workloads only. They do not establish Minecraft tick latency, fleet capacity, Double/vector PID performance or compilation speed.

Verification and reproduction

The candidate passes 202 compiler-artifact tests, 271 compiler-k2 tests (6 skipped) and 11 forked-worker tests, plus the selected Kotlin-to-Rust scenarios exceptions, transparent-call and mfvc. The corresponding module checks include lint, isolation and worker payload/licensing checks. Focused allocation tests cover copy removal, later overwrites, loop back edges, exception-only reads, suspended results, multi-component roots, debug collisions, dense inline parents and retained parent anchors. No new Minecraft client, GameTest or release-readiness verification is claimed.

Validate the durable data from the Compukters root:

python3 docs/benchmarks/copy-coalescing-2026-11-05/validate.py

The validator recomputes all 462 sample summaries/comparisons and verifies static inventories and archived file hashes. Full measured .cpkt inputs, the native binary, complete build/test logs and test XML are retained outside the task checkout under the stand’s ignored build/agent-logs/guest-copy-propagation-2026-11-05/. Their hashes are recorded; the durable validator does not pretend to re-execute those binaries.

To reproduce, prepare isolated checkouts from the baseline compiler revision, with the pinned VM. In the candidate apply candidate.patch, then run in both checkouts:

./worktree-task doctor
./gradlew-sandbox-dev-parallel-summary \
  :compiler-k2:generateTransientAllocationBenchmarkArtifacts \
  :compiler-k2:generateCollectionReuseBenchmarkArtifacts --offline --no-build-cache --max-workers=4

Copy each suite from modules/common/compiler-k2/build/generated/benchmarks/ into a new measurement directory as inputs/baseline/<suite> or inputs/candidate/<suite>. Build guest-benchmark once at the recorded VM revision:

cd host/compukter-vm
CARGO_PROFILE_RELEASE_DEBUG=1 CARGO_BUILD_JOBS=4 cargo build --release --locked --offline \
  --features guest-benchmark --bin guest-benchmark

Place that binary as guest-benchmark beside the archived measure.py, with the input layout above. Run python3 measure.py measure on the recorded CPU, then python3 measure.py validate. Keep new results separate from this immutable archive. The provenance, commands and input hashes identify the measured inputs and build.