semantic_compaction_benchmark/
semantic_compaction_benchmark.rs1use std::{
2 alloc::{GlobalAlloc, Layout, System},
3 hint::black_box,
4 process::Command,
5 sync::atomic::{AtomicIsize, Ordering},
6 time::Instant,
7};
8
9use zirium::{
10 dialect::DialectRegistry,
11 parser::ParsedFile,
12 semantic::{
13 AttributeSpec, DocumentStatistics, LoweringMode, TypeSpec, TypeValue,
14 lower_with_dialect_registry,
15 },
16};
17
18struct CountingAllocator;
19static LIVE_BYTES: AtomicIsize = AtomicIsize::new(0);
20static PEAK_BYTES: AtomicIsize = AtomicIsize::new(0);
21
22unsafe impl GlobalAlloc for CountingAllocator {
23 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
24 let pointer = unsafe { System.alloc(layout) };
25 if !pointer.is_null() {
26 record(layout.size() as isize);
27 }
28 pointer
29 }
30 unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
31 LIVE_BYTES.fetch_sub(layout.size() as isize, Ordering::Relaxed);
32 unsafe { System.dealloc(pointer, layout) }
33 }
34 unsafe fn realloc(&self, pointer: *mut u8, old: Layout, size: usize) -> *mut u8 {
35 let result = unsafe { System.realloc(pointer, old, size) };
36 if !result.is_null() {
37 record(size as isize - old.size() as isize);
38 }
39 result
40 }
41}
42
43#[global_allocator]
44static ALLOCATOR: CountingAllocator = CountingAllocator;
45
46fn record(delta: isize) {
47 let live = LIVE_BYTES.fetch_add(delta, Ordering::Relaxed) + delta;
48 PEAK_BYTES.fetch_max(live, Ordering::Relaxed);
49}
50
51#[derive(Clone, Copy)]
52struct Measurement {
53 edit_ns: u128,
54 verify_ns: u128,
55 peak_allocated_bytes: usize,
56 baseline_owned_bytes: usize,
57 stats: DocumentStatistics,
58 reclaimed_list_entries: usize,
59}
60
61fn main() {
62 let (iterations, warmups, runs) = arguments();
63 println!(
64 "environment rustc={} target={} os={} profile={}",
65 command_output("rustc", &["--version"]),
66 rustc_host(),
67 command_output("uname", &["-sr"]),
68 if cfg!(debug_assertions) {
69 "debug"
70 } else {
71 "release"
72 },
73 );
74 for _ in 0..warmups {
75 black_box(measure(iterations));
76 }
77 let measurements = (0..runs).map(|_| measure(iterations)).collect::<Vec<_>>();
78 let representative = measurements[runs / 2];
79 println!(
80 "workload iterations={iterations} warmups={warmups} runs={runs} live_strings={} retained_strings={} live_types={} retained_types={} live_attributes={} retained_attributes={} baseline_owned_bytes={} retained_owned_bytes={} pooled_entries={} reclaimed_list_entries={}",
81 representative.stats.live_strings,
82 representative.stats.retained_strings,
83 representative.stats.live_types,
84 representative.stats.retained_types,
85 representative.stats.live_attributes,
86 representative.stats.retained_attributes,
87 representative.baseline_owned_bytes,
88 representative.stats.direct_owned_bytes,
89 representative.stats.pooled_list_entries,
90 representative.reclaimed_list_entries,
91 );
92 print_summary("edit_ns", measurements.iter().map(|run| run.edit_ns));
93 print_summary("verify_ns", measurements.iter().map(|run| run.verify_ns));
94 print_summary(
95 "peak_allocated_bytes",
96 measurements
97 .iter()
98 .map(|run| run.peak_allocated_bytes as u128),
99 );
100}
101
102fn arguments() -> (u32, usize, usize) {
103 let (mut iterations, mut warmups, mut runs) = (10_000, 1, 3);
104 let mut args = std::env::args().skip(1);
105 while let Some(argument) = args.next() {
106 let value = args
107 .next()
108 .unwrap_or_else(|| panic!("{argument} requires a value"));
109 match argument.as_str() {
110 "--iterations" => iterations = value.parse().expect("iterations must be an integer"),
111 "--warmups" => warmups = value.parse().expect("warmups must be an integer"),
112 "--runs" => runs = value.parse().expect("runs must be an integer"),
113 _ => panic!("unknown argument: {argument}"),
114 }
115 }
116 assert!(iterations > 0, "iterations must be positive");
117 assert!(runs > 0, "runs must be positive");
118 (iterations, warmups, runs)
119}
120
121fn measure(iterations: u32) -> Measurement {
122 let registry = DialectRegistry::EMPTY;
123 let parsed = ParsedFile::parse(br#"%0 = "anchor"() : () -> i32"#.as_slice()).unwrap();
124 let mut document = lower_with_dialect_registry(&parsed, LoweringMode::Strict, ®istry)
125 .document
126 .unwrap();
127 let operation = document.root_operations()[0];
128 let baseline = document.statistics();
129 let allocator_baseline = LIVE_BYTES.load(Ordering::Relaxed);
130 PEAK_BYTES.store(allocator_baseline, Ordering::Relaxed);
131 let edit_start = Instant::now();
132 let mut editor = document.edit(®istry).unwrap();
133 for index in 0..iterations {
134 let width = 33 + index;
135 editor
136 .replace_result_types(
137 operation,
138 &[TypeSpec {
139 spelling: format!("i{width}"),
140 value: TypeValue::Integer {
141 width,
142 signedness: None,
143 },
144 }],
145 )
146 .unwrap();
147 let name = format!("temporary_{index}");
148 editor
149 .set_attribute(
150 operation,
151 AttributeSpec::string(name.clone(), format!("value_{index}")),
152 )
153 .unwrap();
154 editor.remove_attribute(operation, &name).unwrap();
155 }
156 editor
157 .replace_result_types(
158 operation,
159 &[TypeSpec {
160 spelling: "i32".into(),
161 value: TypeValue::Integer {
162 width: 32,
163 signedness: None,
164 },
165 }],
166 )
167 .unwrap();
168 let pooled_before = editor.document().statistics().pooled_list_entries;
169 let reclaimed_list_entries = editor.compact_pools();
170 editor.commit().unwrap();
171 let edit_ns = edit_start.elapsed().as_nanos();
172 let verify_start = Instant::now();
173 black_box(document.verify_semantics(®istry)).unwrap();
174 let verify_ns = verify_start.elapsed().as_nanos();
175 let stats = document.statistics();
176 assert_eq!(stats.live_attributes, 0);
177 assert!(stats.retained_types > stats.live_types);
178 assert!(stats.retained_attributes > stats.live_attributes);
179 assert!(reclaimed_list_entries > 0 && stats.pooled_list_entries < pooled_before);
180 Measurement {
181 edit_ns,
182 verify_ns,
183 peak_allocated_bytes: PEAK_BYTES
184 .load(Ordering::Relaxed)
185 .saturating_sub(allocator_baseline) as usize,
186 baseline_owned_bytes: baseline.direct_owned_bytes,
187 stats,
188 reclaimed_list_entries,
189 }
190}
191
192fn print_summary(name: &str, values: impl Iterator<Item = u128>) {
193 let mut values = values.collect::<Vec<_>>();
194 values.sort_unstable();
195 println!(
196 "summary metric={name} min={} median={} max={} spread={}",
197 values[0],
198 values[values.len() / 2],
199 values[values.len() - 1],
200 values[values.len() - 1] - values[0]
201 );
202}
203
204fn command_output(command: &str, args: &[&str]) -> String {
205 Command::new(command)
206 .args(args)
207 .output()
208 .ok()
209 .filter(|output| output.status.success())
210 .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_owned())
211 .unwrap_or_else(|| "unavailable".into())
212}
213
214fn rustc_host() -> String {
215 command_output("rustc", &["-vV"])
216 .lines()
217 .find_map(|line| line.strip_prefix("host: "))
218 .unwrap_or("unavailable")
219 .to_owned()
220}