Skip to content

Commit 4a779b6

Browse files
Haofeiclaude
andcommitted
perf(selfhost): ring-buffer Mailbox (Step 4) + O(n^2) bug fix (SH-014)
Rewrote the Mailbox as a ring buffer: head/count index a circular live window over values/sources, grown lazily up to capacity. Common send/take are O(1) (was O(n) scans); source-filtered take scans + compacts. Mutable scalar state (head, count) lives in plain Int fields via the Step-3 mut-param assignment — no more List<Int>-as-cell (SH-007 smell gone). Found and fixed a real O(n^2) along the way (SH-014): the benchmark driver computed i%3 with a hand-rolled `while v>=m {v-=m}` loop called on the *loop counter*, so it looped i/3 times per cycle. Switched to the native `%` operator everywhere. The ring buffer is now linear and ~3x faster than the scanning version (vm 112ms vs 330ms at 60k cycles). Also SH-015: a generic `read T` pushed into a List<T> mis-infers Vec<&T> in AOT (same family as SH-010); worked around with lazy growth so the element only enters via the proven send path. Final (60k cycles): vm 112.9 / jit 114.6 / jit-native 112.8 (≈vm, SH-012) / AOT 0.784 ms. Feature differential 22/22; vm 112; corpus 7; clippy + default build clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4bcb6c0 commit 4a779b6

3 files changed

Lines changed: 192 additions & 230 deletions

File tree

benchmark/selfhost_mailbox.rss

Lines changed: 70 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,103 @@
11
features: local
22

3-
// A fixed-capacity mailbox (message queue) implemented in RSS itself, on top of
4-
// parallel lists. Each message carries a source id and a monotonic sequence
5-
// number; `take` returns the OLDEST pending message (lowest seq), optionally
6-
// filtered by source.
7-
//
8-
// NOTE (language finding): RSS does not allow reassigning a *scalar* struct field
9-
// through a `mut` parameter (`m.count = ...` is rejected). `List` fields are
10-
// reference types, so `List.set(list: mut m.field, ...)` mutates in place and
11-
// propagates. So mutable scalar state (`next_seq`) is held in a 1-element list,
12-
// and `count` is computed by scanning. See docs/rss-selfhost-ledger.md SH-007.
3+
// A fixed-capacity ring-buffer mailbox (message queue) implemented in RSS itself.
4+
// `head`/`count` index a circular live window over `values`/`sources` (grown lazily
5+
// up to `capacity`). Common send/take are O(1); only the source-filtered take scans
6+
// (and compacts). Mutable scalar state lives in plain `Int` fields, mutated
7+
// through `mut` parameters (see ledger SH-013) — no `List<Int>`-as-cell.
138
struct Mailbox<T> {
149
capacity: Int
15-
next_seq: List<Int>
16-
msg: List<T>
17-
valid: List<Bool>
18-
src: List<Int>
19-
seq: List<Int>
10+
head: Int
11+
count: Int
12+
values: List<T>
13+
sources: List<Int>
2014
}
2115

2216
fn mailbox_new<T>(capacity: Int) -> fresh Mailbox<T> {
23-
let mut next_seq = List<Int>.new()
24-
List.push<Int>(list: mut next_seq, value: read 0)
2517
return Mailbox(
2618
capacity: capacity,
27-
next_seq: next_seq,
28-
msg: List<T>.new(),
29-
valid: List<Bool>.new(),
30-
src: List<Int>.new(),
31-
seq: List<Int>.new(),
19+
head: 0,
20+
count: 0,
21+
values: List<T>.new(),
22+
sources: List<Int>.new(),
3223
)
3324
}
3425

3526
fn mailbox_count<T>(m: read Mailbox<T>) -> Int {
36-
let mut count = 0
37-
let mut i = 0
38-
let len = List.len<Bool>(list: read m.valid)
39-
while i < len {
40-
if List.get<Bool>(list: read m.valid, index: i) {
41-
count = count + 1
42-
}
43-
i = i + 1
44-
}
45-
return count
27+
return m.count
4628
}
4729

48-
// Store a message; returns false if the mailbox is full.
30+
// Store a message at the tail. O(1). Returns false if full.
4931
fn mailbox_send<T>(m: mut Mailbox<T>, value: read T, source: Int) -> Bool {
50-
if mailbox_count<T>(m: read m) >= m.capacity {
32+
if m.count >= m.capacity {
5133
return false
5234
}
53-
let seq = List.get<Int>(list: read m.next_seq, index: 0)
54-
let next = seq + 1
55-
List.set<Int>(list: mut m.next_seq, index: 0, value: read next)
56-
57-
let mut i = 0
58-
let len = List.len<Bool>(list: read m.valid)
59-
while i < len {
60-
if List.get<Bool>(list: read m.valid, index: i) == false {
61-
List.set<T>(list: mut m.msg, index: i, value: read value)
62-
List.set<Bool>(list: mut m.valid, index: i, value: read true)
63-
List.set<Int>(list: mut m.src, index: i, value: read source)
64-
List.set<Int>(list: mut m.seq, index: i, value: read seq)
65-
return true
66-
}
67-
i = i + 1
35+
let raw = m.head + m.count
36+
let tail = raw % m.capacity
37+
// Slots fill 0..capacity-1 contiguously, so `tail` is either an existing slot
38+
// (reuse) or exactly the next one to allocate (grow). Avoids needing a generic
39+
// placeholder value to pre-size the lists.
40+
if tail < List.len<T>(list: read m.values) {
41+
List.set<T>(list: mut m.values, index: tail, value: read value)
42+
List.set<Int>(list: mut m.sources, index: tail, value: read source)
43+
} else {
44+
List.push<T>(list: mut m.values, value: read value)
45+
List.push<Int>(list: mut m.sources, value: read source)
6846
}
69-
70-
List.push<T>(list: mut m.msg, value: read value)
71-
List.push<Bool>(list: mut m.valid, value: read true)
72-
List.push<Int>(list: mut m.src, value: read source)
73-
List.push<Int>(list: mut m.seq, value: read seq)
47+
m.count = m.count + 1
7448
return true
7549
}
7650

77-
// Slot index of the oldest valid message (optionally only from `source`), or -1.
78-
fn oldest_slot<T>(m: read Mailbox<T>, filtered: Bool, source: Int) -> Int {
79-
let mut best = -1
80-
let mut best_seq = 0
81-
let mut i = 0
82-
let len = List.len<Bool>(list: read m.valid)
83-
while i < len {
84-
if List.get<Bool>(list: read m.valid, index: i) {
85-
let mut matches = true
86-
if filtered {
87-
if List.get<Int>(list: read m.src, index: i) != source {
88-
matches = false
89-
}
90-
}
91-
if matches {
92-
let s = List.get<Int>(list: read m.seq, index: i)
93-
if best < 0 {
94-
best = i
95-
best_seq = s
96-
} else {
97-
if s < best_seq {
98-
best = i
99-
best_seq = s
100-
}
101-
}
102-
}
103-
}
104-
i = i + 1
105-
}
106-
return best
107-
}
108-
109-
fn take_at<T>(m: mut Mailbox<T>, slot: Int) -> T {
110-
let value = List.get<T>(list: read m.msg, index: slot)
111-
List.set<Bool>(list: mut m.valid, index: slot, value: read false)
112-
return value
113-
}
114-
51+
// Take the oldest message (ring head). O(1).
11552
fn mailbox_take<T>(m: mut Mailbox<T>) -> Option<T> {
116-
let slot = oldest_slot<T>(m: read m, filtered: false, source: 0)
117-
if slot < 0 {
53+
if m.count <= 0 {
11854
return None
11955
}
120-
return Some(take_at<T>(m: mut m, slot: slot))
56+
let value = List.get<T>(list: read m.values, index: m.head)
57+
let next = m.head + 1
58+
m.head = next % m.capacity
59+
m.count = m.count - 1
60+
return Some(value)
12161
}
12262

63+
// Take the oldest message from `source`. Scans the live window; on a middle
64+
// removal, shifts the older elements forward by one to close the gap (preserving
65+
// FIFO order of the rest).
12366
fn mailbox_take_from<T>(m: mut Mailbox<T>, source: Int) -> Option<T> {
124-
let slot = oldest_slot<T>(m: read m, filtered: true, source: source)
125-
if slot < 0 {
67+
let mut offset = -1
68+
let mut k = 0
69+
while k < m.count {
70+
let raw = m.head + k
71+
let idx = raw % m.capacity
72+
if List.get<Int>(list: read m.sources, index: idx) == source {
73+
offset = k
74+
k = m.count
75+
} else {
76+
k = k + 1
77+
}
78+
}
79+
if offset < 0 {
12680
return None
12781
}
128-
return Some(take_at<T>(m: mut m, slot: slot))
82+
let take_raw = m.head + offset
83+
let take_idx = take_raw % m.capacity
84+
let value = List.get<T>(list: read m.values, index: take_idx)
85+
let mut j = offset
86+
while j > 0 {
87+
let dst_raw = m.head + j
88+
let dst = dst_raw % m.capacity
89+
let src_raw = m.head + j - 1
90+
let src = src_raw % m.capacity
91+
let moved_value = List.get<T>(list: read m.values, index: src)
92+
let moved_source = List.get<Int>(list: read m.sources, index: src)
93+
List.set<T>(list: mut m.values, index: dst, value: read moved_value)
94+
List.set<Int>(list: mut m.sources, index: dst, value: read moved_source)
95+
j = j - 1
96+
}
97+
let next = m.head + 1
98+
m.head = next % m.capacity
99+
m.count = m.count - 1
100+
return Some(value)
129101
}
130102

131103
fn show(label: read String, value: Int) -> Unit {
@@ -142,7 +114,6 @@ fn main() -> Unit {
142114
let _b = mailbox_send<Int>(m: mut box, value: read 20, source: 2)
143115
let _c = mailbox_send<Int>(m: mut box, value: read 30, source: 1)
144116

145-
// Match the owned `Option` returned by each take directly (keeps `v` an `Int`).
146117
match mailbox_take<Int>(m: mut box) {
147118
Some(v) => { show(label: read "oldest=", value: v) }
148119
None => { show_none(label: read "oldest=") }

0 commit comments

Comments
 (0)