Skip to content

Commit 06269c3

Browse files
committed
perf(analyzer): memoize SSA function calls sparsely
1 parent 6338298 commit 06269c3

3 files changed

Lines changed: 276 additions & 14 deletions

File tree

crates/analyzer/src/comb_loop_detect.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ mod procedure;
1212
mod region;
1313
mod ssa;
1414

15+
#[cfg(test)]
16+
pub(crate) use procedure::{
17+
function_barrier_evaluation_count, function_evaluation_count,
18+
function_result_region_probe_count, function_result_version_count,
19+
reset_function_evaluation_count,
20+
};
21+
1522
use region::{ArraySpan, BitPartition, IdxKey, NodeKey, PackedSpan, dst_writes, var_reads};
1623

1724
use crate::AnalyzerError;

crates/analyzer/src/comb_loop_detect/procedure.rs

Lines changed: 182 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
33
use super::region::{BitPartition, NodeKey, PackedSpan, dst_writes, var_reads};
44
use super::ssa::{BranchState, SsaStore, VersionId};
5-
use crate::HashSet;
65
use crate::conv::Context;
76
use crate::ir::VarId;
87
use crate::ir::{
@@ -11,6 +10,57 @@ use crate::ir::{
1110
VarSelect,
1211
};
1312
use crate::value::Value;
13+
use crate::{HashMap, HashSet};
14+
15+
#[cfg(test)]
16+
use std::cell::Cell;
17+
18+
#[derive(Clone)]
19+
struct CallResult {
20+
region_groups: Vec<Vec<(PackedSpan, VersionId)>>,
21+
opaque_sources: Vec<VersionId>,
22+
}
23+
24+
// Region-split writes query one RHS several times, but a function call in that
25+
// RHS is one procedural evaluation. `None` is an invocation barrier: temporary
26+
// call nodes in a cloned callee body must never enter the caller's cache.
27+
type CallCache = Option<HashMap<*const FunctionCall, CallResult>>;
28+
29+
#[cfg(test)]
30+
thread_local! {
31+
static FUNCTION_EVALUATIONS: Cell<usize> = const { Cell::new(0) };
32+
static FUNCTION_RESULT_VERSIONS: Cell<usize> = const { Cell::new(0) };
33+
static FUNCTION_RESULT_REGION_PROBES: Cell<usize> = const { Cell::new(0) };
34+
static FUNCTION_BARRIER_EVALUATIONS: Cell<usize> = const { Cell::new(0) };
35+
}
36+
37+
#[cfg(test)]
38+
pub(crate) fn reset_function_evaluation_count() {
39+
FUNCTION_EVALUATIONS.set(0);
40+
FUNCTION_RESULT_VERSIONS.set(0);
41+
FUNCTION_RESULT_REGION_PROBES.set(0);
42+
FUNCTION_BARRIER_EVALUATIONS.set(0);
43+
}
44+
45+
#[cfg(test)]
46+
pub(crate) fn function_evaluation_count() -> usize {
47+
FUNCTION_EVALUATIONS.get()
48+
}
49+
50+
#[cfg(test)]
51+
pub(crate) fn function_result_version_count() -> usize {
52+
FUNCTION_RESULT_VERSIONS.get()
53+
}
54+
55+
#[cfg(test)]
56+
pub(crate) fn function_result_region_probe_count() -> usize {
57+
FUNCTION_RESULT_REGION_PROBES.get()
58+
}
59+
60+
#[cfg(test)]
61+
pub(crate) fn function_barrier_evaluation_count() -> usize {
62+
FUNCTION_BARRIER_EVALUATIONS.get()
63+
}
1464

1565
pub(super) fn analyze(
1666
module: &Module,
@@ -25,6 +75,7 @@ struct ProcedureAnalysis<'a> {
2575
ctx: Context,
2676
ssa: SsaStore<NodeKey>,
2777
written: HashSet<NodeKey>,
78+
call_caches: Vec<CallCache>,
2879
}
2980

3081
impl<'a> ProcedureAnalysis<'a> {
@@ -41,19 +92,39 @@ impl<'a> ProcedureAnalysis<'a> {
4192
ctx,
4293
ssa: SsaStore::default(),
4394
written: HashSet::default(),
95+
call_caches: Vec::new(),
4496
};
4597
this.eval_block(statements, &[]);
4698

4799
let mut dependencies = Vec::new();
48-
let destinations: Vec<_> = this.written.iter().copied().collect();
100+
let destinations: Vec<_> = this
101+
.written
102+
.iter()
103+
.copied()
104+
.filter(|key| this.is_module_scope_key(*key))
105+
.collect();
49106
for destination in destinations {
50107
let version = this.ssa.read(destination);
51108
let sources = this.ssa.root_sources(version);
52-
dependencies.extend(sources.into_iter().map(|source| (source, destination)));
109+
dependencies.extend(
110+
sources
111+
.into_iter()
112+
.filter(|source| this.is_module_scope_key(*source))
113+
.map(|source| (source, destination)),
114+
);
53115
}
54116
dependencies
55117
}
56118

119+
fn is_module_scope_key(&self, key: NodeKey) -> bool {
120+
self.ctx.variables.get(&key.0).is_none_or(|variable| {
121+
matches!(
122+
variable.affiliation,
123+
crate::symbol::Affiliation::Module | crate::symbol::Affiliation::Interface
124+
)
125+
})
126+
}
127+
57128
fn read_keys(&mut self, id: VarId, index: &VarIndex, select: &VarSelect) -> Vec<NodeKey> {
58129
let mut keys = Vec::new();
59130
for (idx, span) in var_reads(id, index, select, &mut self.ctx) {
@@ -182,6 +253,7 @@ impl<'a> ProcedureAnalysis<'a> {
182253
fn eval_statement(&mut self, statement: &Statement, controls: &[VersionId]) -> bool {
183254
match statement {
184255
Statement::Assign(assign) => {
256+
self.call_caches.push(Some(HashMap::default()));
185257
let widths: Vec<_> = assign
186258
.dst
187259
.iter()
@@ -207,6 +279,7 @@ impl<'a> ProcedureAnalysis<'a> {
207279
self.write_destination(destination, &sources, controls);
208280
}
209281
}
282+
self.call_caches.pop();
210283
false
211284
}
212285
Statement::If(statement) => {
@@ -390,7 +463,7 @@ impl<'a> ProcedureAnalysis<'a> {
390463
}
391464
_ => self.eval_system_call(call, &[], true),
392465
},
393-
Factor::FunctionCall(call) => self.eval_call(call, &[]),
466+
Factor::FunctionCall(call) => self.eval_call_requested(call, &[], Some(requested)),
394467
Factor::HierVariable(_)
395468
| Factor::Value(_)
396469
| Factor::Anonymous(_)
@@ -558,6 +631,75 @@ impl<'a> ProcedureAnalysis<'a> {
558631
}
559632

560633
fn eval_call(&mut self, call: &FunctionCall, controls: &[VersionId]) -> Vec<VersionId> {
634+
self.eval_call_requested(call, controls, None)
635+
}
636+
637+
fn eval_call_requested(
638+
&mut self,
639+
call: &FunctionCall,
640+
controls: &[VersionId],
641+
requested: Option<PackedSpan>,
642+
) -> Vec<VersionId> {
643+
#[cfg(test)]
644+
if matches!(self.call_caches.last(), Some(None)) {
645+
FUNCTION_BARRIER_EVALUATIONS.set(FUNCTION_BARRIER_EVALUATIONS.get() + 1);
646+
}
647+
let cache_key = std::ptr::from_ref(call);
648+
if let Some(cached) = self
649+
.call_caches
650+
.last()
651+
.and_then(Option::as_ref)
652+
.and_then(|cache| cache.get(&cache_key))
653+
{
654+
let result = self.select_call_result(cached, requested);
655+
#[cfg(test)]
656+
FUNCTION_RESULT_VERSIONS.set(FUNCTION_RESULT_VERSIONS.get() + result.len());
657+
return result;
658+
}
659+
660+
let evaluated = self.eval_call_uncached(call, controls);
661+
let result = self.select_call_result(&evaluated, requested);
662+
if let Some(Some(cache)) = self.call_caches.last_mut() {
663+
cache.insert(cache_key, evaluated);
664+
}
665+
#[cfg(test)]
666+
FUNCTION_RESULT_VERSIONS.set(FUNCTION_RESULT_VERSIONS.get() + result.len());
667+
result
668+
}
669+
670+
fn select_call_result(
671+
&self,
672+
evaluated: &CallResult,
673+
requested: Option<PackedSpan>,
674+
) -> Vec<VersionId> {
675+
let mut result = Vec::new();
676+
for regions in &evaluated.region_groups {
677+
let first = requested.map_or(0, |requested| {
678+
regions.partition_point(|(span, _)| {
679+
#[cfg(test)]
680+
FUNCTION_RESULT_REGION_PROBES.set(FUNCTION_RESULT_REGION_PROBES.get() + 1);
681+
span.end() <= requested.start
682+
})
683+
});
684+
for (span, version) in &regions[first..] {
685+
#[cfg(test)]
686+
FUNCTION_RESULT_REGION_PROBES.set(FUNCTION_RESULT_REGION_PROBES.get() + 1);
687+
if requested.is_some_and(|requested| span.start >= requested.end()) {
688+
break;
689+
}
690+
result.push(*version);
691+
}
692+
}
693+
result.extend_from_slice(&evaluated.opaque_sources);
694+
result.sort_unstable();
695+
result.dedup();
696+
result
697+
}
698+
699+
fn eval_call_uncached(&mut self, call: &FunctionCall, controls: &[VersionId]) -> CallResult {
700+
#[cfg(test)]
701+
FUNCTION_EVALUATIONS.set(FUNCTION_EVALUATIONS.get() + 1);
702+
561703
let body = self.ctx.functions.get(&call.id).and_then(|function| {
562704
if let Some(index) = &call.index {
563705
function.get_function(index)
@@ -575,7 +717,10 @@ impl<'a> ProcedureAnalysis<'a> {
575717
self.write_destination(destination, &sources, controls);
576718
}
577719
}
578-
return sources;
720+
return CallResult {
721+
region_groups: Vec::new(),
722+
opaque_sources: sources,
723+
};
579724
};
580725
let mut actual_sources = Vec::new();
581726

@@ -591,7 +736,9 @@ impl<'a> ProcedureAnalysis<'a> {
591736
}
592737
}
593738

739+
self.call_caches.push(None);
594740
self.eval_block(&body.statements, controls);
741+
self.call_caches.pop();
595742

596743
for (path, destinations) in &call.outputs {
597744
let Some(&formal) = body.arg_map.get(path) else {
@@ -617,16 +764,19 @@ impl<'a> ProcedureAnalysis<'a> {
617764
}
618765
}
619766

620-
let mut result = body
767+
let region_groups = body
621768
.ret
622-
.map(|ret| self.current_versions_for_id(ret))
769+
.map(|ret| self.current_region_groups_for_id(ret))
623770
.unwrap_or_default();
624-
if statements_have_unknown(&body.statements) {
625-
result.extend(actual_sources);
771+
let opaque_sources = if statements_have_unknown(&body.statements) {
772+
actual_sources
773+
} else {
774+
Vec::new()
775+
};
776+
CallResult {
777+
region_groups,
778+
opaque_sources,
626779
}
627-
result.sort_unstable();
628-
result.dedup();
629-
result
630780
}
631781

632782
fn keys_for_id(&self, id: VarId) -> Vec<NodeKey> {
@@ -650,6 +800,26 @@ impl<'a> ProcedureAnalysis<'a> {
650800
.collect()
651801
}
652802

803+
fn current_region_groups_for_id(&mut self, id: VarId) -> Vec<Vec<(PackedSpan, VersionId)>> {
804+
let mut groups = Vec::<Vec<(PackedSpan, VersionId)>>::new();
805+
let mut previous_array_span = None;
806+
for key in self.keys_for_id(id) {
807+
let Some(span) = self.key_span(key) else {
808+
continue;
809+
};
810+
if previous_array_span != Some(key.1) {
811+
groups.push(Vec::new());
812+
previous_array_span = Some(key.1);
813+
}
814+
let group = groups.last_mut().expect("pushed above");
815+
debug_assert!(group.last().is_none_or(|(previous, _)| {
816+
previous.start <= span.start && previous.end() <= span.start
817+
}));
818+
group.push((span, self.ssa.read(key)));
819+
}
820+
groups
821+
}
822+
653823
fn eval_actual_for_formal_key(
654824
&mut self,
655825
actual: &Expression,

0 commit comments

Comments
 (0)