Scalar frame-copy experiment and architecture review

Passing only the frame-data offset to scalar execution did not improve the current VM. The candidate removes the 64-byte frame copy in machine code, but all eight loop workloads take more CPU time in every paired round. The candidate is rejected; the retained VM remains 188699548d6c747772b10e0e758bf8988d22c2e6 (Runtime 0.24.1). No production VM, compiler, Guest language, artifact, quota, checkpoint, or native ABI change is retained.

Experiment

The baseline’s Machine::run_slice_inner copies a complete Frame when calling execute_scalar. Scalar execution uses only its base; function metadata is passed separately. The candidate passes a u32 base to scalar execution and frame-value helpers. Other callers pass their existing frame base, and the GC test helper follows that signature. Register bounds, physical component access, runtime value checks and arithmetic behavior are unchanged.

Disassembly confirms that the baseline’s four 16-byte frame loads and corresponding stack stores disappear from the scalar call path. This proves removal of the copy, not a reduction in complete-operation CPU time. Initial profiles attribute approximately 19–24% of sampled cycles to the source line containing the frame copy. Sampling attribution can include stalls and skid; that percentage is not the cost removable by rewriting the line.

The original untraced profiles are archived with the data. Their events start only after warmup, the traced control and the first timed sample, using a perf control pipe. They include subsequent admission and construction, not just the hot operation. Main-loop self cycles are 66.24–71.12%; read_register is 6.77–8.34%. No lost samples are reported.

Paired measurements

The current compiler generated the frozen Guest inputs at Compukters a8795985fe8939eb8d06467cb2b80db653c91ca9. The baseline and candidate use the same input bytes, benchmark source, budgets and release settings. The candidate is the archived patch applied to VM 1886995; its SHA-256 and both binary hashes are in provenance.json.

Ryzen 9 9950X3D, Linux x86-64, rustc 1.99.0 / LLVM 23.1.1, thin LTO, one codegen unit and CARGO_PROFILE_RELEASE_DEBUG=1. Every invocation is pinned to logical CPU 6, whose SMT sibling is 22. The governor remains powersave with boost enabled; frequency and unrelated host activity are uncontrolled. Our builds, tests and CPU profiling do not overlap the paired timed execution. Lightweight source and disassembly inspection continued while waiting for trials; the benchmark core was not reserved against other host work.

Three rounds of seven samples give 21 samples per workload and variant, 462 timed samples total. Version order is baseline/candidate in rounds one and three, candidate/baseline in round two. Suite order also reverses in round two; the harness alternates case order internally. No compilation or CPU profiling overlaps these paired trials.

Loops use a 16 KiB Guest heap and transformations 256 KiB, with no fallback. Each invocation checks ready output, checksum, normal termination, traced/untraced work agreement and repeated-sample accounting. Verification, admission, session start and teardown are outside phase timers. Construction is separate; the hot operation includes the final scan and output copy. Both wall and thread CPU clocks are retained.

Workload Baseline hot CPU, ms Candidate hot CPU, ms CPU time change
while-indexed-256-300 45.667 46.269 +1.32%
for-list-256-300 73.935 75.216 +1.73%
fold-256-300 77.404 78.192 +1.02%
fold-val-256-300 76.933 77.830 +1.17%
fold-read-var-256-300 76.672 77.890 +1.59%
fold-write-var-256-300 81.016 82.728 +2.11%
fold-ref-256-300 78.438 79.588 +1.47%
fold-prebuilt-256-300 78.417 79.620 +1.53%
map-not-null-1024-100-fresh 260.424 261.794 +0.53%
map-not-null-1024-100-reuse-list 269.380 270.657 +0.47%
map-not-null-1024-100-reuse-objects 282.504 283.155 +0.23%

Positive changes mean more CPU time. Loops increase by 1.02–2.11% in pooled medians and increase in every individual round. Transformations show small mixed round-level changes and do not establish an improvement. All eight hot work counters match across both variants and all rounds. The changed machine-code layout and register use are possible contributors, but this experiment does not isolate a cause for the timing differences.

The result rejects this particular argument-passing change. It does not establish that every frame-access or instruction-dispatch optimization is unhelpful, or that the frame copy is intrinsically free. No fleet-capacity or Minecraft performance conclusion follows from these native single-thread measurements.

Architecture observations

This review identifies bounded investigations, not approved implementation designs. It makes no changes to the language-support inventory. Current closures, generic specialization, managed exceptions, nominal boxes, multi-field inline values and transparent task suspension must all survive future optimization.

  1. Keep execution access separate from lifecycle ownership. Scalar execution needs frame data, whereas Frame also owns control-flow and return bookkeeping. Narrower dependencies are reasonable, but this experiment shows that an argument-level cleanup alone is not a performance win. Do not retain a measured loop regression for that cleanup, copy or mirror the lifecycle state elsewhere, or add an unchecked second interpreter path.

  2. Investigate work removed by existing compiler passes. TemporaryRegisters.compact and ValueLiveness already provide an artifact-level transformation point and precise exception-aware liveness. Compaction reuses storage; emitted Move instructions can still remain. The measured indexed-loop body contains interface get, several moves and arithmetic; its condition calls the interface size getter. Copy propagation and proven receiver devirtualization are candidates for measurement. Removing a cast, moving a getter or coalescing a register needs proof about side effects, nullability, exceptions and suspension. Compaction deliberately keeps inputs and results distinct where GC or suspension can retain pre-instruction roots. Recompute costs, roots, metadata and identities through their existing owners after a semantics-preserving transform; do not change these independently in the VM.

  3. Measure further admission-time operand preparation before expanding execution metadata. Admission already resolves a FrameValueAccess for each register. Hot scalar execution still looks up those accesses and converts physical values through RuntimeValue before selecting a typed arithmetic operation. More prepared operand or instruction forms might remove repeated work, but can enlarge execution images and pressure caches. Measure image size, admission time and operation CPU together; preserve dynamic bounds/null checks, deterministic budgets, GC roots and partial-operation checkpoints. This is a separate experiment from the rejected frame-copy change.

  4. Separate reusable code from device-specific admission before considering a fleet cache. Session::admit_with_trace currently resolves capabilities and builds an ExecutionImage for that admission. Its immutable inner structure contains code/type tables alongside profile-derived storage plans, limits and resolved host references. Its Arc permits sharing an existing image; it does not by itself cache independently admitted sessions. Reusing the whole image globally could mix admission policy or host bindings. First measure repeated admission and resident metadata for matching programs; any shared preparation layer needs explicit policy keys, bounded retention and independent mutable heaps, frames and tasks. Such a boundary change needs a separate design and roadmap item.

The strongest existing boundaries remain useful: K2 and Kotlin semantics belong to the compiler; verified execution and mutable Guest state belong to Rust; world actions belong to the Minecraft adapter. Future remaining language features should use those owners and cross-layer conformance rather than introducing feature-specific shortcuts in the server scheduler. Add a representative Double/vector PID workload before generalizing these Int/list results to control programs; the existing examples/pid-one-thruster provides a current source starting point.

Verification and reproduction

The candidate passes 754 Rust workspace tests (13 ignored), cargo fmt --all --check and cargo clippy --workspace --all-targets --all-features --locked --offline -- -D warnings. Existing tests cover scalar semantics, inline values/matrix kernels, strings, exceptions, task frames, GC and accounting. No structural test is added for the private argument type. No new Kotlin conformance, JVM integration, Minecraft or release gate is claimed; the candidate is rejected after native behavioral checks and the paired benchmark.

The source patch is reversed only after matching it byte-for-byte to the tested archive. The VM checkout then matches its original commit. All retained repository changes are this report and its evidence.

Validate the archive from the Compukters root:

python3 docs/benchmarks/scalar-frame-2026-11-05/validate.py

For a fresh experiment, generate the two suites using the archived generator command, build the baseline at the recorded VM commit, apply candidate.patch in a separate VM checkout and build the candidate with the same flags:

CARGO_PROFILE_RELEASE_DEBUG=1 CARGO_BUILD_JOBS=4 cargo build --release --locked --offline \
  --features guest-benchmark --bin guest-benchmark
taskset -c 6 PATH_TO_BINARY ARTIFACT_DIR REPORT_DIR 7 HEAP_BYTES untraced

Use 16384 for transient-allocations and 262144 for collection-reuse. Preserve the three paired orders in commands.json and verify newly generated input hashes before attributing any comparison to this archived experiment.

Raw samples and per-round summaries, aggregate comparisons, patch, provenance, commands and host-load observations, timing verification. Full native build logs, binaries, disassembly, original perf data and host snapshots are additionally retained under the stand’s ignored build/agent-logs/vm-scalar-frame-2026-11-05/ and build/agent-logs/vm-baseline-2026-11-05/.