Skip to content

Commit ccf20da

Browse files
committed
Build reference-count elision: skip the retain and drop for a proved move
When a reference-typed variable's last use hands it to a call, spawn, or aliasing copy that consumes it, the retain there and the variable's own drop are both elided: the consumer takes over the single reference. The decision is per site (a variable used at several places is still retained at the earlier ones) and per variable for the drop. Soundness conditions, conservative so it can only under-elide, never over-elide: the use must be the variable's last real use (loop-aware, since a loop-carried variable is live across the back-edge), the variable must appear exactly once at the move site (passing it twice hands the callee several references), and it must have a single drop that the move dominates (approximated by no control flow between the move and the drop). When any condition is unmet the reference counting stays. Two unsound attempts were caught by the leak and memory-safety tests during development: skipping the retain per variable rather than per site (freed a value still used at a later call), and treating a value passed twice in one call as a single move (under-referenced it). Both are fixed and pinned by the existing suite. Measured effect on the http hello path: incref 102 to 81 and decref 172 to 151 per request, memory-safe, all suites and goldens green in both fold modes.
1 parent a1eaf7e commit ccf20da

1 file changed

Lines changed: 127 additions & 8 deletions

File tree

solid-snake-compiler/src/bytecode_gen.rs

Lines changed: 127 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,109 @@ fn for_each_stmt_read(stmt: &TypedIRStmt, f: &mut impl FnMut(usize)) {
577577
}
578578
}
579579

580+
/// Each variable whose reference is moved into a call, spawn, or aliasing copy that consumes it,
581+
/// mapped to the instruction index of that move. The retain is elided only at that move site (a
582+
/// variable used at several sites is still retained at the earlier ones), and the variable's own drop
583+
/// is elided (it was consumed). Conservative and sound: the move must dominate the drop, approximated
584+
/// by a single drop with no control flow between the move and it (a straight-line consumed value), and
585+
/// it is loop-aware (a variable carried into a loop is live across the back-edge, never a move). When
586+
/// it cannot prove a move it keeps the reference counting. See plans/PROFILING.md.
587+
fn elidable_moves(ir: &[TypedIRInstruction]) -> HashMap<usize, usize> {
588+
let last = last_real_use(ir);
589+
let loops = loop_ranges(ir);
590+
let mut defs: HashMap<usize, Vec<usize>> = HashMap::new();
591+
let mut drops: HashMap<usize, Vec<usize>> = HashMap::new();
592+
for (i, instr) in ir.iter().enumerate() {
593+
if let Some(v) = stmt_def_var(&instr.kind) {
594+
defs.entry(v).or_default().push(i);
595+
}
596+
if let TypedIRStmt::Drop { target, .. } = &instr.kind {
597+
drops.entry(target.id()).or_default().push(i);
598+
}
599+
}
600+
let is_movable = |v: usize, i: usize| -> bool {
601+
if last.get(&v) != Some(&i) {
602+
return false;
603+
}
604+
for &(start, end) in &loops {
605+
if start <= i && i <= end {
606+
let defined_in_loop = defs
607+
.get(&v)
608+
.is_some_and(|ds| ds.iter().any(|&d| start <= d && d <= i));
609+
if !defined_in_loop {
610+
return false;
611+
}
612+
}
613+
}
614+
true
615+
};
616+
// A branch or jump between the move and the drop means the move may not dominate the drop (the
617+
// drop could be reached on a path that skips the move), so the value stays counted.
618+
let control_flow_between = |m: usize, d: usize| -> bool {
619+
ir.get(m + 1..=d).is_some_and(|slice| {
620+
slice.iter().any(|ins| {
621+
matches!(
622+
ins.kind,
623+
TypedIRStmt::Jump { .. }
624+
| TypedIRStmt::JumpIf { .. }
625+
| TypedIRStmt::JumpIfNot { .. }
626+
| TypedIRStmt::Label(_)
627+
)
628+
})
629+
})
630+
};
631+
632+
// Candidate (moved-from variable, is_rc, retain-site index), collected before deciding so the
633+
// decision reads the fully-built def/drop maps.
634+
let mut candidates: Vec<(usize, bool, usize)> = Vec::new();
635+
for (i, instr) in ir.iter().enumerate() {
636+
match &instr.kind {
637+
TypedIRStmt::Assign {
638+
value: TypedIRExpr::Var(src),
639+
..
640+
} => candidates.push((src.id(), is_rc(src.typ()), i)),
641+
TypedIRStmt::Call { args, .. } => {
642+
for a in args {
643+
candidates.push((a.id(), is_rc(a.typ()), i));
644+
}
645+
}
646+
TypedIRStmt::CallBuiltin {
647+
builtin: Builtin::CtxNew,
648+
args,
649+
..
650+
} => {
651+
for (idx, a) in args.iter().enumerate() {
652+
if idx > 0 {
653+
candidates.push((a.id(), is_rc(a.typ()), i));
654+
}
655+
}
656+
}
657+
_ => {}
658+
}
659+
}
660+
661+
// How many times each variable is a retain candidate at a given instruction. A variable passed
662+
// more than once in one call (`f(a, a)`) hands the callee several references and is not a single
663+
// move, so it must stay counted.
664+
let mut occurrences: HashMap<(usize, usize), usize> = HashMap::new();
665+
for &(v, _, i) in &candidates {
666+
*occurrences.entry((v, i)).or_insert(0) += 1;
667+
}
668+
669+
let mut elidable: HashMap<usize, usize> = HashMap::new();
670+
for (v, is_rc_v, i) in candidates {
671+
if !is_rc_v || occurrences.get(&(v, i)) != Some(&1) || !is_movable(v, i) {
672+
continue;
673+
}
674+
if let Some(ds) = drops.get(&v) {
675+
if ds.len() == 1 && ds[0] > i && !control_flow_between(i, ds[0]) {
676+
elidable.insert(v, i);
677+
}
678+
}
679+
}
680+
elidable
681+
}
682+
580683
/// Classify one function body's retains into elision categories. Read-only, mirrors the retain rules
581684
/// in `lower_body`/`lower_expr_to_bytecode_stage_one`.
582685
pub fn analyze_rc_elision(ir: &[TypedIRInstruction]) -> RcElisionReport {
@@ -983,21 +1086,30 @@ fn lower_body(
9831086
}
9841087
}
9851088

1089+
// Variables whose reference is moved into a consumer, so their retain and drop are both elided.
1090+
let elidable = elidable_moves(ir);
1091+
9861092
for (idx, ir_instr) in ir.iter().enumerate() {
9871093
match &ir_instr.kind {
9881094
TypedIRStmt::Assign { target, value } => {
9891095
let (reg, instrs) = get_var_register(target.id(), reg_alloc)?;
9901096
instructions.extend_from_slice(&instrs);
9911097
let instrs = lower_expr_to_bytecode_stage_one(value, reg, reg_alloc, constants)?;
9921098
instructions.extend_from_slice(&instrs);
993-
// Aliasing (`q = p`) creates another reference to the same value.
994-
if matches!(value, TypedIRExpr::Var(_)) {
995-
emit_retain(target.typ(), reg, &mut instructions);
1099+
// Aliasing (`q = p`) creates another reference to the same value, unless the source is
1100+
// moved here (its last use), in which case `q` takes the reference and no retain is
1101+
// needed.
1102+
if let TypedIRExpr::Var(src) = value {
1103+
if elidable.get(&src.id()) != Some(&idx) {
1104+
emit_retain(target.typ(), reg, &mut instructions);
1105+
}
9961106
}
9971107
}
9981108
TypedIRStmt::Drop { target, .. } => {
999-
// Releasing a reference at end of life, freeing the object at zero.
1000-
if is_rc(target.typ()) {
1109+
// Releasing a reference at end of life, freeing the object at zero. A moved variable
1110+
// was consumed at its last use, so its reference is already gone and its drop is elided
1111+
// (the register is still freed below).
1112+
if is_rc(target.typ()) && !elidable.contains_key(&target.id()) {
10011113
let (reg, instrs) = get_var_register(target.id(), reg_alloc)?;
10021114
instructions.extend_from_slice(&instrs);
10031115
emit_release(target.typ(), reg, &mut instructions);
@@ -1051,8 +1163,12 @@ fn lower_body(
10511163
instructions
10521164
.push(UnprocessedInstruction::MoveU64((arg_reg.into(), reg.into())));
10531165
// The callee owns its parameters and drops them, so an argument gets an extra
1054-
// reference here to balance that drop.
1055-
emit_retain(arg.typ(), reg, &mut instructions);
1166+
// reference here to balance that drop, unless the argument is moved into the call
1167+
// (its last use), in which case the caller hands over its reference and skips both
1168+
// this retain and the argument's drop.
1169+
if elidable.get(&arg.id()) != Some(&idx) {
1170+
emit_retain(arg.typ(), reg, &mut instructions);
1171+
}
10561172
}
10571173
instructions.push(UnprocessedInstruction::CallFn((func_id,)));
10581174
// The return value comes back in R0.
@@ -1121,7 +1237,10 @@ fn lower_body(
11211237
// reference arguments (its own return-path drops release them), so they
11221238
// get the same extra reference an ordinary call's arguments do. Argument
11231239
// zero is the function id, never a reference.
1124-
if *builtin == Builtin::CtxNew && i > 0 {
1240+
if *builtin == Builtin::CtxNew
1241+
&& i > 0
1242+
&& elidable.get(&arg.id()) != Some(&idx)
1243+
{
11251244
emit_retain(arg.typ(), reg, &mut instructions);
11261245
}
11271246
}

0 commit comments

Comments
 (0)