Skip to content

Use FxHashMap for the VM's per-op caches - #5431

Merged
antoniosarosi merged 1 commit into
canaryfrom
antoniosarosi/vm-fxhash-caches
Oct 27, 2026
Merged

antoniosarosi merged 1 commit into
canaryfrom
antoniosarosi/vm-fxhash-caches

Conversation

@antoniosarosi

@antoniosarosi antoniosarosi commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

The VM looks up three caches on every LoadType and VirtualCall: the LoadType cache of the interpreter, the one of compiled code, and the static virtual-call cache. They used std HashMap, which hashes with SipHash. They now use FxHashMap, like impl_cache.rs already does.

SipHash protects a map from keys an attacker picks. These keys are function addresses, constant indexes, call sites, type tags and program types. Program input never becomes a key, so Fx is safe here. The VM's other SipHash maps are not per-op caches: the structural == and hashing drivers use per-call scratch sets of heap pointers, and the rest are keyed by names and used outside the op loop. They stay as they are.

workload canary this PR change
interfaces::polymorphic-dispatch-100k 58.34 53.87 −7.6%
interfaces::match-dispatch-100k 56.63 52.24 −7.8%
interfaces::default-method-100k 103.64 96.54 −6.9%
interfaces::to-string-interpolation-10k 49.01 46.39 −5.3%
compute::generic-apply-inferred-1m 516.88 489.69 −5.3%
programs::richards-50 500.03 485.62 −2.9%
interp::long-plus (12-hole line, + and to_string()) 35.35 33.60 −5.0%
interp::long (12-hole line, template) 110.07 107.58 −2.3%
interp::string (`${a}/${b}`) 19.61 19.46 −0.8%
interp::int (`id=${k}`) 9.59 9.51 −0.8%
string::concat-loop-10k 1.60 1.59 −0.3%
string::split-short-literal-100k 46.12 45.30 −1.8%
compute::fib32-recursive 724.06 728.08 +0.6% (noise)
compute::pure-call-1m 113.00 113.88 +0.8% (noise)
classes::method-call-100k 23.16 23.72 +2.4% (noise, spread 2.5%)

ms per job, median. AMD Ryzen 9 5950X, Linux 7.0, release builds (fat LTO, rustc 1.98.0), speedtest VM row, telemetry off, pinned to CPU 7, 5 interleaved rounds. interp::* are temporary workloads from the interpolation investigation, each building one string per iteration, 10,000 iterations per job.

Summary by CodeRabbit

  • Refactor
    • Updated internal caching for type and virtual-call lookups. These implementation changes do not alter lookup or dispatch behavior, and no new user-facing features or changes are included in this release.

The LoadType caches (interpreter and compiled code) and the static
virtual-call cache are looked up on every LoadType and VirtualCall.
Their keys are function addresses, constant indexes, call sites and
program types, never program input, so the DoS-resistant SipHash buys
nothing there. impl_cache.rs already uses rustc_hash.
@vercel

vercel Bot commented Oct 27, 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 27, 2026 4:40pm UTC

Request Review

@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.

@coderabbitai

coderabbitai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: bdad5d69-2b6e-4707-abed-5c247864a04a
Commits

Reviewing files that changed from the base of the PR and between 243ab96 and 243ab96.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 23609495-afc1-4fd1-a0b7-cd94688db820
Commits

Reviewing files that changed from the base of the PR and between 3d20df8 and 243ab96.

Files selected for processing: 2
  • baml_language/crates/bex_vm/src/vm.rs
  • baml_language/crates/bex_vm/src/vm/compiled_rt/state.rs

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Walkthrough

The VM’s static type and virtual-call caches, plus the compiled runtime’s static type cache, now use FxHashMap. Their initialization uses FxHashMap::default(). Cache lookup and dispatch behavior remain unchanged.

Changes: VM Cache Maps

Layer / File(s) Summary
Cache types and initialization
baml_language/crates/bex_vm/src/vm.rs, baml_language/crates/bex_vm/src/vm/compiled_rt/state.rs
The VM’s static type and virtual-call caches and the compiled runtime’s static type cache now use FxHashMap. Their test and runtime initialization uses FxHashMap::default(). Cache lookup and dispatch behavior are unchanged.

Priority: Low

Estimated code review effort: 2 (Simple) | ~7 minutes

Change: Refactor

Possibly related PRs

Suggested reviewers: harivansh-afk

Merge Risk: Minimal · up to 243ab

The cache representation change is mergeable; normal build and test validation remain appropriate.

Security architecture risk: Low · up to 243ab

The change does not expand permissions or public interfaces, and ordinary runtime payloads do not become cache keys. No concrete security regression was established. Exposure to adversarial program or type definitions remains deployment-dependent.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The direct changed surface is cache lookup behavior within a VM instance. The comparison establishes no additional service, tenant, credential, or infrastructure authority.

Security Findings and Attack Paths

  • inferred — No direct runtime-payload-to-cache-key attack path was established. Program and type identities remain relevant inputs, so this conclusion does not establish collision resistance when adversaries control complete program definitions.

Trust Boundaries and Controls

  • observed — Static virtual-cache insertion remains restricted to static implementations. Runtime package and registered rules continue through the existing dynamic resolver rather than becoming newly eligible for static caching.

Resilience and Maintainability Implications

  • observed — The compiled cache still allocates before publishing an entry and preserves rooted values across suspension and GC. Source tests cover repeated yield, collection, and resume; those tests were inspected, not executed. Neither revision exposes a recoverable allocation-failure branch in this path.
Pre-merge checks: Passed 4 · Inconclusive 1

Failed Failed checks: 1 inconclusive

Check name Status Explanation Resolution
Docstring Coverage Inconclusive Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Documentation-only edits and deletions within survi… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: replacing standard hash maps with FxHashMap for the VM's per-operation caches.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 1 functions across 1 files. (1 skipped: 1 too large.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • 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.

A rabbit checks the cache at dawn,
Where FxHashMap now hops along.
The keys stay put, the calls stay true,
The defaults take their places too.
Then off the rabbit bounds, content.

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

@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 🔒 64.1 MB 27.3 MB file 73.1 MB -9.0 MB (-12.3%) OK
❌ packed-program Linux 🔒 41.4 MB 16.9 MB file 29.2 MB +12.2 MB (+42.0%) FAIL
❌ bridge_wasm WASM 27.5 MB 🔒 8.1 MB gzip 5.7 MB +2.4 MB (+42.8%) 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.4 MB exceeds limit of 30.1 MB (exceeded by +11.3 MB, policy: max_file_bytes)
  • packed-program (Linux) file_delta_pct: +42.0% exceeds limit of 3.0% (exceeded by +39.0pp, policy: max_delta_pct)
  • bridge_wasm (WASM) gzip_bytes: 8.1 MB exceeds limit of 5.9 MB (exceeded by +2.2 MB, policy: max_gzip_bytes)
  • bridge_wasm (WASM) gzip_delta_pct: +42.8% exceeds limit of 3.0% (exceeded by +39.8pp, policy: max_delta_pct)

Add/update baselines:

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

[artifacts.bridge_wasm]
file_bytes = 27531416
gzip_bytes = 8086041

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

[artifacts.packed-program]
file_bytes = 41387112
gzip_bytes = 16862811

Generated by cargo size-gate · workflow run

@antoniosarosi
antoniosarosi added this pull request to stack #5434 October 27, 2026 17:14
@antoniosarosi
antoniosarosi added this pull request to the merge queue Oct 27, 2026
Merged via the queue into canary with commit 4dbe99a Oct 27, 2026
96 checks passed
@antoniosarosi
antoniosarosi deleted the antoniosarosi/vm-fxhash-caches branch October 27, 2026 18:28
meefs pushed a commit to meefs/baml that referenced this pull request Oct 27, 2026
`join` was written in BAML. A rest pattern copied the array, then every
element went through a generic `string.from` call and a `+` that built a
rope node. It is now a native that appends into one buffer.

A string element is appended as is. Any other element is rendered
exactly as `string.from` renders it, through the override-honoring
walker, one element at a time. So `ToString` overrides still apply at
every depth and run in element order, as in the BAML loop. The separator
semantics are unchanged: an empty array joins to `""`, one element to
its own rendering. New tests pin those cases plus override classes,
structural classes, a structural class holding an override, enums, maps,
nested lists, and a garbage collection inside an override. The 12-hole
`join` below joins the speedtest suite as
`string::join-request-line-10k`.

This is the top of the stack (BoundaryML#5431 → BoundaryML#5432 → this PR), so the table
also compares with canary before any of the three.

| workload | canary | PR 2 | this PR | vs PR 2 | vs canary |
|---|---:|---:|---:|---:|---:|
| 12-hole request line, `join` | 513.14 | 432.20 | 22.87 | 18.9× faster
| 22.4× faster |
| `[a, "/", b].join("")` | 80.55 | 70.48 | 7.70 | 9.2× faster | 10.5×
faster |
| 12-hole request line, template | 117.77 | 30.27 | 31.02 | +2% | 3.8×
faster |
| `` `${a}/${b}` `` | 20.06 | 4.17 | 4.33 | +4% | 4.6× faster |
| `interfaces::to-string-interpolation-10k` | 51.39 | 32.89 | 33.34 |
+1% | −35% |
| repro `` `${a}${b}` `` (ns/op) | 1,563 | 100 | 98 | −1% | 15.9× faster
|
| repro `` `/v1/items/${a}/tags/${b}` `` (ns/op) | 1,703 | 104 | 104 |
−1% | 16.5× faster |
| repro `` `n=${k}` `` (ns/op) | 764 | 244 | 230 | −6% | 3.3× faster |
| repro `a.to_string() + b.to_string()` (ns/op) | 826 | 454 | 432 | −5%
| −48% |
| repro loop `` out = `${out}${c}` `` (ns/op) | 2,508 | 540 | 556 | +3%
| 4.5× faster |
| repro `a + b` (ns/op, control) | 114 | 114 | 113 | −1% | −1% |
| loop, no string built (control) | 2.43 | 2.46 | 2.64 | +7% | +9% |
| `a + "/" + b` (control) | 4.07 | 4.09 | 4.37 | +7% | +7% |
| `programs::richards-50` (control) | 510.13 | 496.96 | 515.00 | +4% |
+1% |
| `compute::generic-apply-inferred-1m` (control) | 527.90 | 492.42 |
530.13 | +8% | 0% |
| `compute::fib32-recursive` (control) | 744.02 | 739.12 | 740.95 | 0% |
0% |
| `compute::pure-call-1m` (control) | 115.37 | 115.58 | 116.12 | 0% |
+1% |
| `string::trim-padded-literal-100k` (control) | 33.89 | 33.93 | 35.77 |
+5% | +6% |

The controls that move run no `join` and no template. Against PR 2 they
execute the same number of instructions (`perf stat`: `richards-50`
18.3266 vs 18.3265 billion, `generic-apply` 18.43 vs 18.43 billion).
Only front-end stall cycles grow (`richards-50` 415M → 685M). That is
code layout in the fat-LTO binary, not extra work. The same controls
also swing ±3% between canary, PR 1 and PR 2, which don't touch them
either.

ms per job (median of 5) unless marked ns/op (median of 10). AMD Ryzen 9
5950X, Linux 7.0, release builds (fat LTO, rustc 1.98.0), `speedtest` VM
row, telemetry off, pinned to CPU 7, canary and the three stacked PRs
interleaved in 5 rounds. Rows without a `::` name are temporary
workloads that build one string per iteration, 10,000 iterations per
job.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Array joining now honors custom string representations for values,
including nested values, while preserving element order and separators.
* Joining supports empty and single-element arrays, primitive values,
enums, classes, unions, and generic calls.
* **Bug Fixes**
* Improved consistency when custom string representations run during
garbage collection.
* **Tests**
* Expanded coverage for joining behavior and added a performance
workload for large batches of request-line strings.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

This branch was successfully deployed

1 active deployment
Preview – developer-docs — 243ab966 Deployed Oct 27, 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