Skip to content

Escape analysis: bytecode skips locks for thread-local objects - #5407

Closed
harivansh-afk wants to merge 1 commit into
brr/escape-analysisfrom
brr/escape-bytecode
Closed

harivansh-afk wants to merge 1 commit into
brr/escape-analysisfrom
brr/escape-bytecode

Conversation

@harivansh-afk

Copy link
Copy Markdown
Collaborator

TLDR

Stacked on #5395. The bytecode consumer of the escape analysis: the interpreter skips container locks and field atomics on objects the analysis proves task-local, and compiles a for over an array the loop alone walks as an index walk instead of an iterator object plus a next() call.

How

  • The emitter selects lock-free opcodes where escape::EscapeFacts proves the object thread-local: LOAD_ARRAY_ELEMENT_LOCAL, STORE_ARRAY_ELEMENT_LOCAL, CONTAINER_LEN_LOCAL, and LOAD_FIELD_LOCAL / STORE_FIELD_LOCAL (Relaxed ordering). All are appended to preserve discriminants.
  • CALL_LOCAL makes the call path select a builtin's $local twin, the same lock-free glue the native backend calls. FunctionKind::Native carries the twin pointer.
  • ARRAY_ITER_NEXT{iterator, index} steps an array walk over a hidden index slot, re-reading the length each step as ArrayIterator.next does. ARRAY_ITER_NEXT_LOCAL skips the lock on a thread-local array.

Results (Spark, aarch64, vm-off, vs the merge base)

Workload Speedup
live-walk 10.2×
generate-sort-8k 4.3×
array-traversal-8k 2.8×
cyclic-churn 2.75×
array-build-sum-100k 2.4×
quick-sort-3way-1k 2.4×
allocation-high-retention, json-parser, merge-sort, allocation-low-retention 1.37–1.46×

Most of this is the index walk, which is structural, not lock removal. cycles-into-live shows 0.81× in a 3-job window, but per job it is 2× faster with 33% less peak memory. A major collection lands inside the candidate's window and outside the base's.

Not included

A bytecode locality mask (parameter accesses that depend on what the caller proved) is designed but not built: 9 opcodes for a small expected gain, since interpreter dispatch dominates. Notes in perf/bytecode_consumer.md.

Testing

  • Bytecode-shape tests for arrays, maps and fields; one snapshot update; rust_backend accepts CALL_LOCAL.
  • This tree is identical to the pre-split branch (brr/escape-analysis-full), which passed the full rust_differential suite with debug assertions, ThreadSanitizer on all escape probes in both modes, and the BAML test suite at O2.

The bytecode consumer of the escape analysis. The emitter picks lock-free
opcodes where the analysis proves an object thread-local:
LoadArrayElementLocal, StoreArrayElementLocal and ContainerLenLocal;
LoadFieldLocal and StoreFieldLocal with Relaxed ordering; and CallLocal,
which makes the call path select a builtin's $local twin
(FunctionKind::Native carries the twin).

A for-loop over an array the loop alone walks compiles to ArrayIterNext
over a hidden index slot instead of an iterator object and a next() call;
ArrayIterNextLocal reads a thread-local array without its lock.
@vercel

vercel Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
developer-docs Ready Ready Preview Oct 25, 2026 3:03pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@github-actions

Copy link
Copy Markdown

Binary size checks failed

❌ 2 violations · ✅ 1 passed

⚠️ Please fix the size gate issues or acknowledge them by updating baselines.

Artifact Platform File Gzip Gated on Baseline Delta Status
✅ baml-cli Linux 🔒 63.7 MB 27.1 MB file 73.1 MB -9.3 MB (-12.8%) OK
❌ packed-program Linux 🔒 41.0 MB 16.7 MB file 29.2 MB +11.8 MB (+40.6%) FAIL
❌ bridge_wasm WASM 28.4 MB 🔒 8.2 MB gzip 5.7 MB +2.6 MB (+45.5%) FAIL

🔒 = the size this artifact is GATED on (ceiling + delta). Binaries gate on file size (installed binary); WASM gates on gzip (download size). The other size is shown for information only.

Details & how to fix

Violations:

  • packed-program (Linux) file_bytes: 41.0 MB exceeds limit of 30.1 MB (exceeded by +10.9 MB, policy: max_file_bytes)
  • packed-program (Linux) file_delta_pct: +40.6% exceeds limit of 3.0% (exceeded by +37.6pp, policy: max_delta_pct)
  • bridge_wasm (WASM) gzip_bytes: 8.2 MB exceeds limit of 5.9 MB (exceeded by +2.4 MB, policy: max_gzip_bytes)
  • bridge_wasm (WASM) gzip_delta_pct: +45.5% exceeds limit of 3.0% (exceeded by +42.5pp, policy: max_delta_pct)

Add/update baselines:

.ci/size-gate/wasm32-unknown-unknown.toml:

[artifacts.bridge_wasm]
file_bytes = 28382010
gzip_bytes = 8239553

.ci/size-gate/x86_64-unknown-linux-gnu.toml:

[artifacts.packed-program]
file_bytes = 40975848
gzip_bytes = 16725065

Generated by cargo size-gate · workflow run

@harivansh-afk

Copy link
Copy Markdown
Collaborator Author

Closing: the interpreter half of escape analysis is dropped. Only the native backend keeps lock elision (#5395, rebased onto canary with the interpreter, bytecode emitter and opcode table byte-identical to canary).

harivansh-afk added a commit that referenced this pull request Oct 28, 2026
Containers gain lock_thread_local / lock_mut_thread_local, which skip the
container lock for an object no other task can reach; debug builds still
try the lock and panic if another holder has it. Instance fields gain
Relaxed load and store. The compiled runtime exposes _local accessors for
element, length and field access.

Builtins declare their memory effects with //baml:effects(...): what they
keep, what they return, whether they compare keys. A declared array or map
method gets a generated $local twin that reads its receiver without the
lock; the map driver behind has/get/set/delete/index/get_or_insert takes
its guards without the lock when a twin started it.

(rebased onto canary) Applied without conflicts. Two comments that said
both backends select the $local twins now say the native backend does,
since the bytecode half (#5407) is not part of this branch:
baml_builtins2_codegen/src/types.rs, bex_vm/src/package_baml/map.rs.

This branch was successfully deployed

1 active deployment
Preview – developer-docs — cccf8d9f Deployed Oct 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant