Skip to content

Commit 9f7901e

Browse files
olwangclaude
andcommitted
fix(reg-vm): O(1) hash-Set and linear task_group scheduler
Two runtime hotspots surfaced by the vm-jit baseline suite, both O(n²): 1. hash-Set was a Vec with a linear scan on every insert/contains/remove (set_insert_contains: 1680x native Rust). Back Set with the same FNV ValueMap the Map type uses (value -> Unit), making membership O(1). set_insert_contains now runs ~4.5x reg/rust, on par with map_int (4.2x), a ~290x speedup. Bonus: HashMap equality is order-insensitive, fixing a latent mismatch vs the compiled HashSet backend where two equal sets built in different insertion orders compared unequal under the old Vec backing. Removes the now-dead set_insert_vm/set_remove_vm helpers. 2. task_group scheduler never removed a finished task's slot from its task table, so the per-step satisfy_waiters scan grew O(n) and the whole loop went O(n²) (task_group_spawn: 0.34/9.9/725 ms at 100/1k/10k rounds, and identical under plain eval, confirming a runtime bug not a JIT one). Reap a task slot on join — a handle is awaited at most once (RS0030) — in both the immediate and parked join paths. Now linear: 1.8/17.4/153 ms at 1k/10k/100k. Parity verified: corpus, vm_eval_parity, vm, and metamorphic suites all pass (reg_vm_runs_set_intrinsics_like_interpreter, parity_set_intrinsics, reg_vm_task_group_drains_unawaited_async_let_like_backend, async concurrency parity). The one pre-existing execution_coverage failure (Math/Metal/Tensor fixture annotations) reproduces on clean main and is unrelated. Restore task_group_spawn kernel to size 20000 (now linear, ~30 ms) and refresh baseline-20260620.json + README/plan findings #3/#4 to mark both bugs fixed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6c81153 commit 9f7901e

6 files changed

Lines changed: 153 additions & 137 deletions

File tree

benchmarks/vm-jit/README.md

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -136,23 +136,29 @@ speedup, **<1 = faster than the plain VM**).
136136
the pattern — tiers *regress* ineligible code — is stable.) A cheap early win:
137137
**don't attempt the JIT on functions that will predictably bail.**
138138

139-
3. **`set_insert_contains` is pathological: reg/rust ≈ 1680×** (1796 ms vs
140-
1.07 ms) — and the new `sorted_set_ops` kernel makes it a smoking gun: the
141-
*same* insert+contains workload on an **ordered** set is **2.2×** reg/rust
142-
(1.30 ms). So an ordered membership structure is ~750× faster than the hash
143-
`Set` at the same job. This isolates the cost to the **hash-`Set`
144-
implementation specifically** (almost certainly an O(n) or rehash-thrash bug),
145-
not to dispatch or to set semantics generally. Its own, likely cheap, fix.
146-
147-
4. **Structured concurrency is both slow and super-linear.** `task_group_spawn`
148-
is **337× reg/rust** even at the small size it now runs (2 000 rounds), and
149-
worse, it scales **≈ quadratically**: jit-internal measured 0.34 ms / 9.9 ms /
150-
725 ms at 100 / 1 000 / 10 000 rounds (and identically under plain `eval`, so
151-
it is a **runtime** bug, not a JIT one — completed `task_group` frames look
152-
unreclaimed). The original 100 000-round size was uncapped and hung the suite;
153-
the kernel is pinned to 2 000 and the runner now has a per-mode `--timeout`
154-
guard so a pathological case degrades to `n/a` instead of hanging. Flag for
155-
its own investigation alongside the Set bug.
139+
3. **`set_insert_contains` was pathological (reg/rust ≈ 1680×) — now FIXED
140+
(≈ 4.5×).** The `sorted_set_ops` kernel was the smoking gun: the *same*
141+
insert+contains workload on an **ordered** set was **2.2×** reg/rust while the
142+
hash `Set` was ~750× slower, isolating the cost to the hash-`Set` itself. The
143+
reg-VM was backing `Set` with a plain `Vec` and doing a **linear scan** on
144+
every insert/contains/remove — O(n²) overall. Fixed by backing `Set` with the
145+
same FNV `ValueMap` the `Map` type uses (value → `Unit`), making membership
146+
O(1); `set` now runs **4.5× reg/rust**, on par with `map_int` (4.2×). (Bonus:
147+
`HashMap` equality is order-insensitive, fixing a latent mismatch where two
148+
equal sets built in different insertion orders compared unequal under the old
149+
`Vec` backing.)
150+
151+
4. **Structured concurrency was slow *and* super-linear — now FIXED (linear).**
152+
`task_group_spawn` once scaled **≈ quadratically**: jit-internal measured
153+
0.34 / 9.9 / 725 ms at 100 / 1 000 / 10 000 rounds (identically under plain
154+
`eval`, so a **runtime** bug, not a JIT one). The scheduler never removed a
155+
finished task's slot from its task table, so its per-step `satisfy_waiters`
156+
scan grew O(n) and the whole loop went O(n²). Fixed by **reaping a task slot
157+
on join** (a handle is awaited at most once, RS0030) — it now scales linearly
158+
(1.8 / 17.4 / 153 ms at 1k / 10k / 100k) and the kernel is restored to size
159+
20 000 (~30 ms, comparable to `async_call_loop`). The runner also gained a
160+
per-mode `--timeout` guard so any *future* pathological case degrades to `n/a`
161+
instead of hanging the suite.
156162

157163
5. **Heap-variant & combinator paths are 340–660×** (`option_result_chain` 662×,
158164
`match_option_loop` 362×, `variant_match_loop` 342×, `nested_struct_field`
@@ -171,9 +177,9 @@ speedup, **<1 = faster than the plain VM**).
171177

172178
Implications for the plan: (a) Phase 3 (widen native eligibility) is re-weighted
173179
**up** — native is already near-Rust where it runs; (b) add a "predict-and-skip
174-
bail" guard so the tiers stop *regressing* ineligible code (#2); (c) the hash-Set
175-
anomaly (#3) and the quadratic `task_group` runtime (#4) are separate, likely
176-
cheap, high-impact bug fixes that do not depend on the tier work; (d) Rc/clone
180+
bail" guard so the tiers stop *regressing* ineligible code (#2); (c) the two
181+
runtime bugs the suite surfaced — the hash-`Set` (#3) and the quadratic
182+
`task_group` (#4) — **are now fixed**, independent of the tier work; (d) Rc/clone
177183
traffic (#7) confirms Phase 4 can stay deferred.
178184

179185
## Adding a kernel

benchmarks/vm-jit/baseline/baseline-20260620.json

Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,42 @@
22
"iterations": 5,
33
"warmup": 1,
44
"cases": [
5-
{"category":"native-scalar","case":"native_scalar_loop.rss","size":"2000000","reg_vm_ms":153.450249,"jit_ms":153.371398,"native_ms":3.201323,"rust_ms":2.412036},
6-
{"category":"native-read-heap","case":"native_read_heap.rss","size":"2000000","reg_vm_ms":115.515553,"jit_ms":118.760618,"native_ms":6.772098,"rust_ms":0.612261},
7-
{"category":"native-call","case":"native_call_chain.rss","size":"1500000","reg_vm_ms":196.302776,"jit_ms":197.774866,"native_ms":5.327675,"rust_ms":5.555101},
8-
{"category":"int-divmod","case":"int_divmod_loop.rss","size":"2000000","reg_vm_ms":179.280179,"jit_ms":166.543577,"native_ms":4.197145,"rust_ms":1.958134},
9-
{"category":"bool-logic","case":"bool_logic_loop.rss","size":"2000000","reg_vm_ms":347.169502,"jit_ms":338.2524,"native_ms":6.694568,"rust_ms":3.212792},
10-
{"category":"int-arith","case":"pure_loop_sum.rss","size":"2000000","reg_vm_ms":240.724204,"jit_ms":241.646979,"native_ms":245.257864,"rust_ms":3.860776},
11-
{"category":"struct-field","case":"struct_field_rw.rss","size":"200000","reg_vm_ms":43.875971,"jit_ms":43.066937,"native_ms":43.761612,"rust_ms":0.165308},
12-
{"category":"struct-nested","case":"nested_struct_field.rss","size":"300000","reg_vm_ms":92.842627,"jit_ms":95.59277,"native_ms":97.32352,"rust_ms":0.24795},
13-
{"category":"call-static","case":"function_call_hot_loop.rss","size":"200000","reg_vm_ms":37.870419,"jit_ms":36.868427,"native_ms":38.519386,"rust_ms":0.545433},
14-
{"category":"recursion-tree","case":"recursion_fib.rss","size":"30","reg_vm_ms":224.940183,"jit_ms":213.232579,"native_ms":211.518604,"rust_ms":2.820809},
15-
{"category":"recursion-linear","case":"linear_recursion.rss","size":"50000","reg_vm_ms":4156.306189,"jit_ms":4099.619487,"native_ms":4147.873087,"rust_ms":14.13297},
16-
{"category":"float","case":"float_loop_sum.rss","size":"1000000","reg_vm_ms":72.741296,"jit_ms":73.878812,"native_ms":89.474229,"rust_ms":1.498632},
17-
{"category":"string","case":"string_build_scan.rss","size":"200000","reg_vm_ms":37.227373,"jit_ms":36.949423,"native_ms":36.891548,"rust_ms":14.551392},
18-
{"category":"string-text","case":"string_text_processing.rss","size":"100000","reg_vm_ms":44.948627,"jit_ms":80.683534,"native_ms":47.285951,"rust_ms":28.514427},
19-
{"category":"string-map","case":"map_string_keys.rss","size":"16000","reg_vm_ms":7.651754,"jit_ms":7.253979,"native_ms":6.903029,"rust_ms":3.07649},
20-
{"category":"variant-user","case":"variant_match_loop.rss","size":"300000","reg_vm_ms":125.618428,"jit_ms":108.532653,"native_ms":110.234594,"rust_ms":0.367141},
21-
{"category":"variant-option","case":"match_option_loop.rss","size":"200000","reg_vm_ms":30.798301,"jit_ms":29.998618,"native_ms":31.626625,"rust_ms":0.085174},
22-
{"category":"variant-combinator","case":"option_result_chain.rss","size":"60000","reg_vm_ms":48.3353,"jit_ms":45.721135,"native_ms":51.190782,"rust_ms":0.073008},
23-
{"category":"list-scan","case":"list_index_scan.rss","size":"19000","reg_vm_ms":1.62429,"jit_ms":1.609049,"native_ms":1.583207,"rust_ms":0.017175},
24-
{"category":"list-closure","case":"list_closure_pipeline.rss","size":"12000","reg_vm_ms":3.47659,"jit_ms":3.600923,"native_ms":3.777506,"rust_ms":0.038183},
25-
{"category":"closure-alloc","case":"closure_alloc_loop.rss","size":"300000","reg_vm_ms":34.232657,"jit_ms":34.585007,"native_ms":39.661796,"rust_ms":0.124166},
26-
{"category":"list-pipeline","case":"pipeline_chain.rss","size":"16000","reg_vm_ms":3.468223,"jit_ms":3.383273,"native_ms":3.550698,"rust_ms":0.054074},
27-
{"category":"list-sort","case":"list_sort.rss","size":"4000","reg_vm_ms":18.384282,"jit_ms":17.547816,"native_ms":17.495865,"rust_ms":0.482924},
28-
{"category":"map-int","case":"map_insert_lookup.rss","size":"17000","reg_vm_ms":2.705407,"jit_ms":2.761598,"native_ms":2.62059,"rust_ms":0.573258},
29-
{"category":"sorted-map-insert","case":"sorted_map_insert.rss","size":"8000","reg_vm_ms":2.427648,"jit_ms":2.422907,"native_ms":2.405448,"rust_ms":0.567841},
30-
{"category":"sorted-map-scan","case":"sorted_map_scan.rss","size":"12000","reg_vm_ms":3.707281,"jit_ms":3.768256,"native_ms":3.700639,"rust_ms":0.926999},
31-
{"category":"set","case":"set_insert_contains.rss","size":"30000","reg_vm_ms":1796.190578,"jit_ms":1745.084837,"native_ms":1732.635993,"rust_ms":1.067999},
32-
{"category":"sorted-set","case":"sorted_set_ops.rss","size":"8000","reg_vm_ms":1.297166,"jit_ms":1.243474,"native_ms":1.288466,"rust_ms":0.582866},
33-
{"category":"deque","case":"deque_queue.rss","size":"50000","reg_vm_ms":4.658297,"jit_ms":4.760022,"native_ms":4.720656,"rust_ms":0.109524},
34-
{"category":"bytes","case":"bytes_scan.rss","size":"200000","reg_vm_ms":20.877156,"jit_ms":21.167498,"native_ms":20.374739,"rust_ms":2.347498},
35-
{"category":"deep-copy","case":"deep_copy_list.rss","size":"100000","reg_vm_ms":68.505373,"jit_ms":69.119698,"native_ms":69.659689,"rust_ms":3.151415},
36-
{"category":"closure-dynamic","case":"dynamic_closure_call.rss","size":"200000","reg_vm_ms":37.551014,"jit_ms":37.879705,"native_ms":39.199713,"rust_ms":0.360008},
37-
{"category":"json","case":"json_parse_access.rss","size":"50000","reg_vm_ms":50.076174,"jit_ms":50.481908,"native_ms":49.539274,"rust_ms":31.489825},
38-
{"category":"async","case":"async_call_loop.rss","size":"200000","reg_vm_ms":36.381123,"jit_ms":36.349031,"native_ms":37.444839,"rust_ms":null},
39-
{"category":"async-taskgroup","case":"task_group_spawn.rss","size":"2000","reg_vm_ms":31.691751,"jit_ms":31.264826,"native_ms":30.398209,"rust_ms":0.0941},
40-
{"category":"selfhost-manifest","case":"selfhost_manifest_inspector.rss","size":"benchmarks/micro/fixtures/package-medium/rsspkg.toml","reg_vm_ms":0.046441,"jit_ms":0.043958,"native_ms":0.049116,"rust_ms":0.045941},
41-
{"category":"selfhost-mailbox","case":"selfhost_mailbox_bench.rss","size":"60000","reg_vm_ms":79.176401,"jit_ms":96.132959,"native_ms":85.825131,"rust_ms":0.433083}
5+
{"category":"native-scalar","case":"native_scalar_loop.rss","size":"2000000","reg_vm_ms":153.647255,"jit_ms":156.223949,"native_ms":3.115721,"rust_ms":2.290868},
6+
{"category":"native-read-heap","case":"native_read_heap.rss","size":"2000000","reg_vm_ms":110.498523,"jit_ms":285.696353,"native_ms":7.575415,"rust_ms":0.571644},
7+
{"category":"native-call","case":"native_call_chain.rss","size":"1500000","reg_vm_ms":217.77955,"jit_ms":210.09721,"native_ms":5.314955,"rust_ms":5.65309},
8+
{"category":"int-divmod","case":"int_divmod_loop.rss","size":"2000000","reg_vm_ms":167.95479,"jit_ms":173.565073,"native_ms":4.131184,"rust_ms":1.952041},
9+
{"category":"bool-logic","case":"bool_logic_loop.rss","size":"2000000","reg_vm_ms":321.685371,"jit_ms":342.012965,"native_ms":6.260959,"rust_ms":2.975446},
10+
{"category":"int-arith","case":"pure_loop_sum.rss","size":"2000000","reg_vm_ms":235.275324,"jit_ms":242.020936,"native_ms":239.234357,"rust_ms":3.889116},
11+
{"category":"struct-field","case":"struct_field_rw.rss","size":"200000","reg_vm_ms":43.337749,"jit_ms":45.171907,"native_ms":44.506696,"rust_ms":0.164717},
12+
{"category":"struct-nested","case":"nested_struct_field.rss","size":"300000","reg_vm_ms":96.550306,"jit_ms":131.217077,"native_ms":97.237301,"rust_ms":0.264226},
13+
{"category":"call-static","case":"function_call_hot_loop.rss","size":"200000","reg_vm_ms":36.063651,"jit_ms":36.764204,"native_ms":38.821205,"rust_ms":0.550019},
14+
{"category":"recursion-tree","case":"recursion_fib.rss","size":"30","reg_vm_ms":203.837741,"jit_ms":207.697649,"native_ms":207.362706,"rust_ms":2.89837},
15+
{"category":"recursion-linear","case":"linear_recursion.rss","size":"50000","reg_vm_ms":4031.030371,"jit_ms":4059.884092,"native_ms":4065.832831,"rust_ms":14.476702},
16+
{"category":"float","case":"float_loop_sum.rss","size":"1000000","reg_vm_ms":72.363723,"jit_ms":72.768863,"native_ms":72.451132,"rust_ms":1.474174},
17+
{"category":"string","case":"string_build_scan.rss","size":"200000","reg_vm_ms":36.763329,"jit_ms":37.725211,"native_ms":36.950849,"rust_ms":12.976202},
18+
{"category":"string-text","case":"string_text_processing.rss","size":"100000","reg_vm_ms":45.409186,"jit_ms":46.863409,"native_ms":47.10843,"rust_ms":27.656139},
19+
{"category":"string-map","case":"map_string_keys.rss","size":"16000","reg_vm_ms":15.637553,"jit_ms":7.216367,"native_ms":7.495779,"rust_ms":3.182102},
20+
{"category":"variant-user","case":"variant_match_loop.rss","size":"300000","reg_vm_ms":107.028001,"jit_ms":107.136518,"native_ms":108.122055,"rust_ms":0.359034},
21+
{"category":"variant-option","case":"match_option_loop.rss","size":"200000","reg_vm_ms":29.750383,"jit_ms":30.411344,"native_ms":30.911355,"rust_ms":0.085858},
22+
{"category":"variant-combinator","case":"option_result_chain.rss","size":"60000","reg_vm_ms":47.151165,"jit_ms":45.93246,"native_ms":56.399812,"rust_ms":0.072375},
23+
{"category":"list-scan","case":"list_index_scan.rss","size":"19000","reg_vm_ms":1.517956,"jit_ms":1.55904,"native_ms":1.562415,"rust_ms":0.017887},
24+
{"category":"list-closure","case":"list_closure_pipeline.rss","size":"12000","reg_vm_ms":3.496087,"jit_ms":3.586032,"native_ms":3.75336,"rust_ms":0.037333},
25+
{"category":"closure-alloc","case":"closure_alloc_loop.rss","size":"300000","reg_vm_ms":33.994413,"jit_ms":35.194108,"native_ms":40.719938,"rust_ms":0.134012},
26+
{"category":"list-pipeline","case":"pipeline_chain.rss","size":"16000","reg_vm_ms":3.282257,"jit_ms":3.212459,"native_ms":3.937583,"rust_ms":0.056395},
27+
{"category":"list-sort","case":"list_sort.rss","size":"4000","reg_vm_ms":17.700293,"jit_ms":17.376164,"native_ms":17.421232,"rust_ms":0.442854},
28+
{"category":"map-int","case":"map_insert_lookup.rss","size":"17000","reg_vm_ms":2.467535,"jit_ms":2.532477,"native_ms":2.549794,"rust_ms":0.587294},
29+
{"category":"sorted-map-insert","case":"sorted_map_insert.rss","size":"8000","reg_vm_ms":2.357726,"jit_ms":2.323851,"native_ms":2.371343,"rust_ms":0.577836},
30+
{"category":"sorted-map-scan","case":"sorted_map_scan.rss","size":"12000","reg_vm_ms":3.545981,"jit_ms":3.571631,"native_ms":3.577356,"rust_ms":0.913053},
31+
{"category":"set","case":"set_insert_contains.rss","size":"30000","reg_vm_ms":4.543385,"jit_ms":4.410392,"native_ms":4.44668,"rust_ms":1.011796},
32+
{"category":"sorted-set","case":"sorted_set_ops.rss","size":"8000","reg_vm_ms":1.481284,"jit_ms":1.454098,"native_ms":1.266472,"rust_ms":0.657622},
33+
{"category":"deque","case":"deque_queue.rss","size":"50000","reg_vm_ms":5.056221,"jit_ms":4.987753,"native_ms":5.11202,"rust_ms":0.114657},
34+
{"category":"bytes","case":"bytes_scan.rss","size":"200000","reg_vm_ms":20.801564,"jit_ms":20.861939,"native_ms":29.618047,"rust_ms":2.282469},
35+
{"category":"deep-copy","case":"deep_copy_list.rss","size":"100000","reg_vm_ms":71.355636,"jit_ms":72.261042,"native_ms":69.557991,"rust_ms":3.273645},
36+
{"category":"closure-dynamic","case":"dynamic_closure_call.rss","size":"200000","reg_vm_ms":38.82545,"jit_ms":38.875215,"native_ms":39.226846,"rust_ms":0.354489},
37+
{"category":"json","case":"json_parse_access.rss","size":"50000","reg_vm_ms":49.726418,"jit_ms":50.255195,"native_ms":49.777718,"rust_ms":31.893109},
38+
{"category":"async","case":"async_call_loop.rss","size":"200000","reg_vm_ms":36.082602,"jit_ms":35.685541,"native_ms":36.493897,"rust_ms":null},
39+
{"category":"async-taskgroup","case":"task_group_spawn.rss","size":"20000","reg_vm_ms":31.727662,"jit_ms":31.491087,"native_ms":50.063375,"rust_ms":0.996844},
40+
{"category":"selfhost-manifest","case":"selfhost_manifest_inspector.rss","size":"benchmarks/micro/fixtures/package-medium/rsspkg.toml","reg_vm_ms":0.093523,"jit_ms":0.044778,"native_ms":0.058328,"rust_ms":0.056646},
41+
{"category":"selfhost-mailbox","case":"selfhost_mailbox_bench.rss","size":"60000","reg_vm_ms":80.878149,"jit_ms":80.8973,"native_ms":83.463481,"rust_ms":0.45694}
4242
]
4343
}

benchmarks/vm-jit/cases.tsv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,6 @@ deep-copy benchmarks/vm-jit/kernels/deep_copy_list.rss
4949
closure-dynamic benchmarks/vm-jit/kernels/dynamic_closure_call.rss 200000 stored owned Fn dynamic call
5050
json benchmarks/micro/json_parse_access.rss 50000 Json parse/field access
5151
async benchmarks/micro/async_call_loop.rss 200000 async call/await park/resume
52-
async-taskgroup benchmarks/vm-jit/kernels/task_group_spawn.rss 2000 structured concurrency spawn+join
52+
async-taskgroup benchmarks/vm-jit/kernels/task_group_spawn.rss 20000 structured concurrency spawn+join
5353
selfhost-manifest benchmarks/micro/selfhost_manifest_inspector.rss benchmarks/micro/fixtures/package-medium/rsspkg.toml realistic mixed (parse+collections)
5454
selfhost-mailbox benchmarks/micro/selfhost_mailbox_bench.rss 60000 realistic async actor loop

benchmarks/vm-jit/kernels/task_group_spawn.rss

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ features: async
55
// distinct from async_call_loop's single sequential await. (RSScript has no
66
// `select`; a task_group implicitly joins all its tasks at block end.)
77
//
8-
// NOTE: this kernel scales ~QUADRATICALLY in round count under every interpreted
9-
// mode (eval == vm == jit: ~0.34/9.9/725 ms at 100/1k/10k rounds), so it is run
10-
// at a deliberately small size (2000). The blowup is a runtime bug (completed
11-
// task_group frames appear unreclaimed), not a JIT issue — see README finding #4.
8+
// HISTORY: this kernel once scaled ~quadratically in round count (completed
9+
// task slots were never reclaimed from the scheduler's task table, so its
10+
// per-step scans grew O(n)). Fixed by reaping a task slot on join — it now
11+
// scales linearly. See README finding #4.
1212

1313
fn bench_size(default: Int) -> Int {
1414
let raw = Args.get_or_default(index: 0, default: read String.from_int(value: default))
@@ -41,7 +41,7 @@ fn run_round(seed: Int) -> Result<Int, String> {
4141
}
4242

4343
fn main() -> Result<Unit, String> {
44-
let limit = bench_size(default: 2000)
44+
let limit = bench_size(default: 20000)
4545
let mut index = 0
4646
let mut total = 0
4747

0 commit comments

Comments
 (0)