Skip to main content

semantic_compaction_benchmark/
semantic_compaction_benchmark.rs

1use 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, &registry)
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(&registry).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(&registry)).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}