Skip to content

Commit 2124801

Browse files
committed
perf(analyzer): track sparse SSA branch deltas
1 parent 890e0b0 commit 2124801

2 files changed

Lines changed: 135 additions & 63 deletions

File tree

crates/analyzer/src/comb_loop_detect/procedure.rs

Lines changed: 17 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Analyzer-IR procedure evaluation for combinational dependency extraction.
22
33
use super::region::{BitPartition, NodeKey, PackedSpan, dst_writes, var_reads};
4-
use super::ssa::{SsaStore, VersionId};
4+
use super::ssa::{BranchState, SsaStore, VersionId};
55
use crate::HashSet;
66
use crate::conv::Context;
77
use crate::ir::VarId;
@@ -100,8 +100,9 @@ impl<'a> ProcedureAnalysis<'a> {
100100
if let Some((_, expression)) = &destination.select.1 {
101101
dependencies.extend(self.eval_expr(expression));
102102
}
103-
for key in self.write_keys(destination) {
104-
let version = self.ssa.definition(dependencies.clone());
103+
let keys = self.write_keys(destination);
104+
let version = self.ssa.definition(dependencies);
105+
for key in keys {
105106
self.ssa.bind(key, version);
106107
self.written.insert(key);
107108
}
@@ -240,22 +241,15 @@ impl<'a> ProcedureAnalysis<'a> {
240241
let condition = self.eval_expr(&statement.cond);
241242
let mut nested_controls = controls.to_vec();
242243
nested_controls.extend_from_slice(&condition);
243-
let saved_state = self.ssa.snapshot();
244-
let saved_written = self.written.clone();
245-
244+
let checkpoint = self.ssa.checkpoint();
246245
self.eval_block(&statement.true_side, &nested_controls);
247-
let true_state = self.ssa.snapshot();
248-
let true_written = self.written.clone();
246+
let true_state = self.ssa.capture_and_rollback(checkpoint);
249247

250-
self.ssa.restore(&saved_state);
251-
self.written = saved_written;
248+
let checkpoint = self.ssa.checkpoint();
252249
self.eval_block(&statement.false_side, &nested_controls);
253-
let false_state = self.ssa.snapshot();
254-
let false_written = self.written.clone();
250+
let false_state = self.ssa.capture_and_rollback(checkpoint);
255251

256-
self.ssa.merge(&saved_state, &[true_state, false_state]);
257-
self.written = true_written;
258-
self.written.extend(false_written);
252+
self.ssa.merge(&[true_state, false_state]);
259253
}
260254

261255
fn eval_case(&mut self, statement: &CaseStatement, controls: &[VersionId]) {
@@ -275,24 +269,16 @@ impl<'a> ProcedureAnalysis<'a> {
275269
}
276270
let mut nested_controls = controls.to_vec();
277271
nested_controls.extend(condition);
278-
let saved_state = self.ssa.snapshot();
279-
let saved_written = self.written.clone();
280272
let mut states = Vec::with_capacity(statement.arms.len() + 1);
281-
let mut written = saved_written.clone();
282273
for arm in &statement.arms {
283-
self.ssa.restore(&saved_state);
284-
self.written = saved_written.clone();
274+
let checkpoint = self.ssa.checkpoint();
285275
self.eval_block(&arm.body, &nested_controls);
286-
states.push(self.ssa.snapshot());
287-
written.extend(self.written.iter().copied());
276+
states.push(self.ssa.capture_and_rollback(checkpoint));
288277
}
289-
self.ssa.restore(&saved_state);
290-
self.written = saved_written;
278+
let checkpoint = self.ssa.checkpoint();
291279
self.eval_block(&statement.default, &nested_controls);
292-
states.push(self.ssa.snapshot());
293-
written.extend(self.written.iter().copied());
294-
self.ssa.merge(&saved_state, &states);
295-
self.written = written;
280+
states.push(self.ssa.capture_and_rollback(checkpoint));
281+
self.ssa.merge(&states);
296282
}
297283

298284
fn eval_for(&mut self, statement: &ForStatement, controls: &[VersionId]) {
@@ -329,15 +315,10 @@ impl<'a> ProcedureAnalysis<'a> {
329315
// Runtime loops have a zero-trip path. One symbolic body traversal is
330316
// enough to expose all explicit reads; the exit phi keeps LiveOnEntry
331317
// separate so retained state does not become a loop edge.
332-
let saved_state = self.ssa.snapshot();
333-
let saved_written = self.written.clone();
318+
let checkpoint = self.ssa.checkpoint();
334319
self.eval_block(&statement.body, &range_controls);
335-
let body_state = self.ssa.snapshot();
336-
let body_written = self.written.clone();
337-
self.ssa
338-
.merge(&saved_state, &[saved_state.clone(), body_state]);
339-
self.written = saved_written;
340-
self.written.extend(body_written);
320+
let body_state = self.ssa.capture_and_rollback(checkpoint);
321+
self.ssa.merge(&[BranchState::unchanged(), body_state]);
341322
}
342323

343324
fn eval_expr_requested(

crates/analyzer/src/comb_loop_detect/ssa.rs

Lines changed: 118 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,34 @@ enum Version<K> {
1212
Phi(Vec<VersionId>),
1313
}
1414

15-
#[derive(Clone)]
16-
pub(super) struct Snapshot<K> {
17-
current: HashMap<K, VersionId>,
15+
pub(super) struct Checkpoint {
16+
undo_start: usize,
17+
depth: usize,
18+
}
19+
20+
pub(super) struct BranchState<K> {
21+
bindings: HashMap<K, VersionId>,
22+
}
23+
24+
impl<K> BranchState<K> {
25+
pub(super) fn unchanged() -> Self {
26+
Self {
27+
bindings: HashMap::default(),
28+
}
29+
}
30+
}
31+
32+
struct Undo<K> {
33+
key: K,
34+
previous: Option<VersionId>,
1835
}
1936

2037
pub(super) struct SsaStore<K> {
2138
versions: Vec<Version<K>>,
2239
entries: HashMap<K, VersionId>,
2340
current: HashMap<K, VersionId>,
41+
undo: Vec<Undo<K>>,
42+
checkpoints: Vec<usize>,
2443
}
2544

2645
impl<K> Default for SsaStore<K> {
@@ -29,6 +48,8 @@ impl<K> Default for SsaStore<K> {
2948
versions: Vec::new(),
3049
entries: HashMap::default(),
3150
current: HashMap::default(),
51+
undo: Vec::new(),
52+
checkpoints: Vec::new(),
3253
}
3354
}
3455
}
@@ -51,9 +72,7 @@ where
5172
if let Some(version) = self.current.get(&key) {
5273
*version
5374
} else {
54-
let version = self.entry(key);
55-
self.current.insert(key, version);
56-
version
75+
self.entry(key)
5776
}
5877
}
5978

@@ -66,38 +85,65 @@ where
6685
}
6786

6887
pub(super) fn bind(&mut self, key: K, version: VersionId) {
69-
self.current.insert(key, version);
88+
let previous = self.current.insert(key, version);
89+
if !self.checkpoints.is_empty() {
90+
self.undo.push(Undo { key, previous });
91+
}
7092
}
7193

72-
pub(super) fn snapshot(&self) -> Snapshot<K> {
73-
Snapshot {
74-
current: self.current.clone(),
75-
}
94+
pub(super) fn checkpoint(&mut self) -> Checkpoint {
95+
let checkpoint = Checkpoint {
96+
undo_start: self.undo.len(),
97+
depth: self.checkpoints.len(),
98+
};
99+
self.checkpoints.push(checkpoint.undo_start);
100+
checkpoint
76101
}
77102

78-
pub(super) fn restore(&mut self, snapshot: &Snapshot<K>) {
79-
self.current.clone_from(&snapshot.current);
103+
pub(super) fn capture_and_rollback(&mut self, checkpoint: Checkpoint) -> BranchState<K> {
104+
assert_eq!(checkpoint.depth + 1, self.checkpoints.len());
105+
assert_eq!(self.checkpoints.pop(), Some(checkpoint.undo_start));
106+
107+
let mut bindings = HashMap::default();
108+
for undo in &self.undo[checkpoint.undo_start..] {
109+
let version = self
110+
.current
111+
.get(&undo.key)
112+
.copied()
113+
.expect("a branch binding must exist until rollback");
114+
bindings.insert(undo.key, version);
115+
}
116+
117+
while self.undo.len() > checkpoint.undo_start {
118+
let undo = self.undo.pop().expect("undo length checked above");
119+
if let Some(previous) = undo.previous {
120+
self.current.insert(undo.key, previous);
121+
} else {
122+
self.current.remove(&undo.key);
123+
}
124+
}
125+
bindings.retain(|key, version| self.current.get(key).copied() != Some(*version));
126+
BranchState { bindings }
80127
}
81128

82-
pub(super) fn merge(&mut self, base: &Snapshot<K>, states: &[Snapshot<K>]) {
83-
let mut keys: HashSet<K> = base.current.keys().copied().collect();
129+
pub(super) fn merge(&mut self, states: &[BranchState<K>]) {
130+
let mut keys = HashSet::default();
84131
for state in states {
85-
keys.extend(state.current.keys().copied());
132+
keys.extend(state.bindings.keys().copied());
86133
}
87-
let mut merged = HashMap::default();
88134
for key in keys {
89-
let fallback = base
135+
let fallback = self
90136
.current
91137
.get(&key)
92138
.copied()
93139
.unwrap_or_else(|| self.entry(key));
94140
let inputs = states
95141
.iter()
96-
.map(|state| state.current.get(&key).copied().unwrap_or(fallback))
142+
.map(|state| state.bindings.get(&key).copied().unwrap_or(fallback))
97143
.collect();
98-
merged.insert(key, self.phi(inputs));
144+
let version = self.phi(inputs);
145+
self.bind(key, version);
99146
}
100-
self.current = merged;
101147
}
102148

103149
pub(super) fn root_sources(&self, version: VersionId) -> HashSet<K> {
@@ -179,32 +225,77 @@ mod tests {
179225
#[test]
180226
fn retained_live_on_entry_is_not_a_combinational_read() {
181227
let mut ssa = SsaStore::default();
182-
let base = ssa.snapshot();
183228
let retained = ssa.read("destination");
229+
let checkpoint = ssa.checkpoint();
184230
let assigned = ssa.definition(Vec::new());
185231
ssa.bind("destination", assigned);
186-
let branch = ssa.snapshot();
232+
let branch = ssa.capture_and_rollback(checkpoint);
187233

188-
ssa.merge(&base, &[base.clone(), branch]);
234+
ssa.merge(&[BranchState::unchanged(), branch]);
189235

190236
let merged = ssa.read("destination");
191237
assert_ne!(merged, retained);
192238
assert!(ssa.root_sources(merged).is_empty());
193239
}
194240

195241
#[test]
196-
fn restore_discards_current_bindings_without_discarding_versions() {
242+
fn rollback_discards_current_bindings_without_discarding_versions() {
197243
let mut ssa = SsaStore::default();
198-
let base = ssa.snapshot();
244+
let checkpoint = ssa.checkpoint();
199245
let source = ssa.read("source");
200246
let definition = ssa.definition(vec![source]);
201247
ssa.bind("destination", definition);
202248

203-
ssa.restore(&base);
249+
let _ = ssa.capture_and_rollback(checkpoint);
204250
let restored = ssa.read("destination");
205251

206252
let expected = ["source"].into_iter().collect::<HashSet<_>>();
207253
assert_eq!(ssa.root_sources(definition), expected);
208254
assert!(ssa.root_sources(restored).is_empty());
209255
}
256+
257+
#[test]
258+
fn branch_state_contains_only_keys_changed_since_checkpoint() {
259+
let mut ssa = SsaStore::default();
260+
for key in 0..1_000 {
261+
let version = ssa.definition(Vec::new());
262+
ssa.bind(key, version);
263+
}
264+
265+
let checkpoint = ssa.checkpoint();
266+
let version = ssa.definition(Vec::new());
267+
ssa.bind(500, version);
268+
let branch = ssa.capture_and_rollback(checkpoint);
269+
270+
assert_eq!(branch.bindings.len(), 1);
271+
assert_eq!(branch.bindings[&500], version);
272+
}
273+
274+
#[test]
275+
fn nested_rollback_preserves_the_outer_transaction() {
276+
let mut ssa = SsaStore::default();
277+
let base = ssa.definition(Vec::new());
278+
ssa.bind("outer", base);
279+
280+
let outer_checkpoint = ssa.checkpoint();
281+
let outer_definition = ssa.definition(Vec::new());
282+
ssa.bind("outer", outer_definition);
283+
284+
let inner_checkpoint = ssa.checkpoint();
285+
let inner_definition = ssa.definition(Vec::new());
286+
ssa.bind("inner", inner_definition);
287+
let inner_state = ssa.capture_and_rollback(inner_checkpoint);
288+
289+
assert_eq!(ssa.read("outer"), outer_definition);
290+
assert_ne!(ssa.read("inner"), inner_definition);
291+
292+
ssa.merge(&[BranchState::unchanged(), inner_state]);
293+
let merged_inner = ssa.read("inner");
294+
let outer_state = ssa.capture_and_rollback(outer_checkpoint);
295+
296+
assert_eq!(ssa.read("outer"), base);
297+
assert_ne!(ssa.read("inner"), merged_inner);
298+
assert_eq!(outer_state.bindings["outer"], outer_definition);
299+
assert_eq!(outer_state.bindings["inner"], merged_inner);
300+
}
210301
}

0 commit comments

Comments
 (0)