@@ -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`.
582685pub 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