Skip to content

Commit ee89b3d

Browse files
committed
Add --rc-report: static reference-count elision measurement
A read-only analysis over the typed IR classifies each emitted retain by category (aliasing copy, call argument, spawn argument, borrowed element load, store into an aggregate) and, for the categories that become a move when the source's last real use is the retain site, counts how many are movable. A movable retain pairs with the moved-from variable's drop, so eliding it elides both. Changes no codegen. Driven by a --rc-report CLI flag handled at the top like --explain, over the IR already exposed on the compile result, so no environment reads live in the library and nothing is threaded into lower_program. First measurement on the http hello server: call-arg retains dominate (72 of the 97 movable-category retains) and 56 of them are movable, so the calling-convention retain is the big lever and about 82 percent of movable-category retains are actually moves.
1 parent 5fed051 commit ee89b3d

3 files changed

Lines changed: 205 additions & 0 deletions

File tree

solid-snake-compiler/src/bytecode_gen.rs

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,181 @@ fn for_each_stmt_var(stmt: &TypedIRStmt, f: &mut impl FnMut(usize)) {
465465
}
466466
}
467467

468+
/// Read-only measurement of reference-count elision opportunity over one function body (no codegen
469+
/// change). It counts each emitted retain by category, and for the categories that are a move when the
470+
/// source's last real use is the retain site (an aliasing copy, a call argument, or a spawn argument),
471+
/// how many are movable. A movable retain pairs with the moved-from variable's drop, so eliding it
472+
/// elides both. The kept categories (a borrowed element load, a store into an aggregate) are genuinely
473+
/// shared references. See plans/PROFILING.md.
474+
#[derive(Default, Clone, Copy)]
475+
pub struct RcElisionReport {
476+
pub alias_total: u64,
477+
pub alias_movable: u64,
478+
pub call_arg_total: u64,
479+
pub call_arg_movable: u64,
480+
pub spawn_arg_total: u64,
481+
pub spawn_arg_movable: u64,
482+
pub indexed_load_kept: u64,
483+
pub aggregate_store_kept: u64,
484+
}
485+
486+
impl RcElisionReport {
487+
pub fn add(&mut self, other: &RcElisionReport) {
488+
self.alias_total += other.alias_total;
489+
self.alias_movable += other.alias_movable;
490+
self.call_arg_total += other.call_arg_total;
491+
self.call_arg_movable += other.call_arg_movable;
492+
self.spawn_arg_total += other.spawn_arg_total;
493+
self.spawn_arg_movable += other.spawn_arg_movable;
494+
self.indexed_load_kept += other.indexed_load_kept;
495+
self.aggregate_store_kept += other.aggregate_store_kept;
496+
}
497+
498+
pub fn movable(&self) -> u64 {
499+
self.alias_movable + self.call_arg_movable + self.spawn_arg_movable
500+
}
501+
502+
pub fn line(&self) -> String {
503+
format!(
504+
"rc-elision (static, per function set): alias {}/{} movable, call-arg {}/{} movable, spawn-arg {}/{} movable, indexed-load {} kept, aggregate-store {} kept; movable retains total {} (each pairs with a drop)",
505+
self.alias_movable, self.alias_total,
506+
self.call_arg_movable, self.call_arg_total,
507+
self.spawn_arg_movable, self.spawn_arg_total,
508+
self.indexed_load_kept, self.aggregate_store_kept,
509+
self.movable(),
510+
)
511+
}
512+
}
513+
514+
/// Last index at which each variable is read for a real use (excluding its `Drop`, which is a release
515+
/// rather than a keeping use). The move signal: when a reference's last real use is a retain site, the
516+
/// retain transfers the only live reference and can become a move.
517+
fn last_real_use(ir: &[TypedIRInstruction]) -> HashMap<usize, usize> {
518+
let mut last: HashMap<usize, usize> = HashMap::new();
519+
for (i, instr) in ir.iter().enumerate() {
520+
if matches!(instr.kind, TypedIRStmt::Drop { .. }) {
521+
continue;
522+
}
523+
for_each_stmt_read(&instr.kind, &mut |v| {
524+
last.insert(v, i);
525+
});
526+
}
527+
last
528+
}
529+
530+
/// Visit only the variables a statement reads (its uses), not the variable it defines. The read side
531+
/// of an `Assign` comes from the value expression, so a self-referential `x = x + 1` still counts the
532+
/// read of `x`.
533+
fn for_each_stmt_read(stmt: &TypedIRStmt, f: &mut impl FnMut(usize)) {
534+
match stmt {
535+
TypedIRStmt::Assign { value, .. } => {
536+
for v in value.read_vars() {
537+
f(v);
538+
}
539+
}
540+
TypedIRStmt::Drop { target, .. } => f(target.id()),
541+
TypedIRStmt::JumpIf { condition, .. } | TypedIRStmt::JumpIfNot { condition, .. } => {
542+
f(condition.id())
543+
}
544+
TypedIRStmt::Jump { .. } | TypedIRStmt::Label(_) => {}
545+
TypedIRStmt::Call { args, .. } | TypedIRStmt::CallBuiltin { args, .. } => {
546+
for a in args {
547+
f(a.id());
548+
}
549+
}
550+
TypedIRStmt::StructNew { args, .. } => {
551+
for a in args {
552+
f(a.id());
553+
}
554+
}
555+
TypedIRStmt::ArrayNew { elements, .. } => {
556+
for e in elements {
557+
f(e.id());
558+
}
559+
}
560+
TypedIRStmt::FieldGet { base, .. } => f(base.id()),
561+
TypedIRStmt::FieldSet { base, value, .. } => {
562+
f(base.id());
563+
f(value.id());
564+
}
565+
TypedIRStmt::Return { value } => {
566+
if let Some(v) = value {
567+
f(v.id());
568+
}
569+
}
570+
}
571+
}
572+
573+
/// Classify one function body's retains into elision categories. Read-only, mirrors the retain rules
574+
/// in `lower_body`/`lower_expr_to_bytecode_stage_one`.
575+
pub fn analyze_rc_elision(ir: &[TypedIRInstruction]) -> RcElisionReport {
576+
let last = last_real_use(ir);
577+
let mut r = RcElisionReport::default();
578+
let is_movable = |v: usize, i: usize| last.get(&v) == Some(&i);
579+
for (i, instr) in ir.iter().enumerate() {
580+
match &instr.kind {
581+
// Aliasing `q = p` retains the copied reference.
582+
TypedIRStmt::Assign {
583+
target,
584+
value: TypedIRExpr::Var(src),
585+
} if is_rc(target.typ()) => {
586+
r.alias_total += 1;
587+
if is_movable(src.id(), i) {
588+
r.alias_movable += 1;
589+
}
590+
}
591+
// Every reference argument to a user call is retained (the callee drops it).
592+
TypedIRStmt::Call { args, .. } => {
593+
for a in args {
594+
if is_rc(a.typ()) {
595+
r.call_arg_total += 1;
596+
if is_movable(a.id(), i) {
597+
r.call_arg_movable += 1;
598+
}
599+
}
600+
}
601+
}
602+
TypedIRStmt::CallBuiltin { builtin, args, dest } => match builtin {
603+
// Spawn retains its reference arguments (argument zero is the function id).
604+
Builtin::CtxNew => {
605+
for (idx, a) in args.iter().enumerate() {
606+
if idx > 0 && is_rc(a.typ()) {
607+
r.spawn_arg_total += 1;
608+
if is_movable(a.id(), i) {
609+
r.spawn_arg_movable += 1;
610+
}
611+
}
612+
}
613+
}
614+
// A loaded element or map value is a borrowed reference into a live collection, so it
615+
// is a genuine new reference that stays counted.
616+
Builtin::ArrayGet | Builtin::ListGet | Builtin::MapGet => {
617+
if let Some(d) = dest {
618+
if is_rc(d.typ()) {
619+
r.indexed_load_kept += 1;
620+
}
621+
}
622+
}
623+
_ => {}
624+
},
625+
// Storing a reference into an object field or array element makes the aggregate share it,
626+
// an escape that stays counted.
627+
TypedIRStmt::FieldSet { value, .. } if is_rc(value.typ()) => {
628+
r.aggregate_store_kept += 1;
629+
}
630+
TypedIRStmt::ArrayNew {
631+
elem_is_ref: true,
632+
elements,
633+
..
634+
} => {
635+
r.aggregate_store_kept += elements.len() as u64;
636+
}
637+
_ => {}
638+
}
639+
}
640+
r
641+
}
642+
468643
/// The variable a statement defines (writes a fresh value into a variable), if any. Used to emit a
469644
/// store-back when that variable is memory-resident. Statements that only read (drops, jumps,
470645
/// returns) or write into an object field rather than a variable define nothing.

solid-snake-compiler/src/main.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ struct Cli {
5555
#[arg(long)]
5656
explain: bool,
5757

58+
/// Print the static reference-count elision report (how much emitted retain traffic is a movable
59+
/// last-use) and exit without running. Changes no codegen. See plans/PROFILING.md.
60+
#[arg(long = "rc-report")]
61+
rc_report: bool,
62+
5863
/// Disable optimizations (constant folding), so runtime paths run instead of being folded away
5964
#[arg(long = "no-opt")]
6065
no_opt: bool,
@@ -96,6 +101,19 @@ fn main() {
96101
return;
97102
}
98103

104+
// `--rc-report` prints the static reference-count elision report and exits without running.
105+
if cli.rc_report {
106+
match runner::compile_named(&source, &label, opts) {
107+
Ok(compiled) => println!("{}", runner::rc_report(&compiled.ir_functions)),
108+
Err(failure) => {
109+
for err in failure.errors.err_iter() {
110+
report_error_attributed(err, &failure.sources);
111+
}
112+
}
113+
}
114+
return;
115+
}
116+
99117
// Run the injected program first, then optionally stay in an interactive session, so the
100118
// session continues from a process that has already executed the given code.
101119
compile_and_run(&source, &label, cli.verbose, opts);

solid-snake-compiler/src/runner.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,18 @@ pub fn run_streaming(
251251
/// buffer and appended, which is both the efficient shape and a dogfood of the growable-string
252252
/// pattern the language now supports. Used by the CLI `--explain` flag, and handy for debugging
253253
/// lowering (which source construct produced which IR).
254+
/// A read-only reference-count elision report over the whole program's IR, for `--rc-report`. It sums
255+
/// the per-function retain-category counts, so the movable share of the emitted retain traffic is
256+
/// visible before any elision is applied. Changes no codegen.
257+
pub fn rc_report(ir_functions: &[TypedIrFunction]) -> String {
258+
use crate::bytecode_gen::{analyze_rc_elision, RcElisionReport};
259+
let mut report = RcElisionReport::default();
260+
for func in ir_functions {
261+
report.add(&analyze_rc_elision(&func.body));
262+
}
263+
report.line()
264+
}
265+
254266
pub fn explain(source: &str, ir_functions: &[TypedIrFunction]) -> String {
255267
use std::fmt::Write as _;
256268

0 commit comments

Comments
 (0)