Skip to content

Commit bbaa063

Browse files
committed
Record rc-elision result, refinement roadmap, and the guiding model
Mark the elision built (conservative first cut, the measured incref/decref reduction), and record the refinements with headroom: proper dominance rather than the straight-line rule, full dataflow liveness, more elision categories (return moves, orphan-temp direct frees that attack the untouched allocations, retain/release cancellation, struct-field moves). Record the guiding model: take Rust's ownership and borrow analysis as an internal oracle, never a gate, with RC as the always-correct fallback, so nothing is rejected and the surface stays invisible. The larger unrealized lever is borrow elision (Rust's shared reference): a parameter the callee only reads needs no caller retain at all, which needs a per-function escape summary and is interprocedural.
1 parent ccf20da commit bbaa063

1 file changed

Lines changed: 46 additions & 18 deletions

File tree

plans/PROFILING.md

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -106,24 +106,52 @@ First result on the http hello server: call-arg retains dominate (72 of the 97 m
106106
retains) and 55 are movable, so the calling-convention retain (the caller retains, the callee drops)
107107
is the large lever and most arguments are moves.
108108

109-
### Follow-ups (non-trivial, revisit before building the elision)
110-
111-
- **The liveness is a first cut.** The loop rule is sound but coarse. A full version wants proper
112-
dataflow liveness: nested loops, `break`/`continue`, conditionals and merge points, multiple
113-
reaching definitions, and cross-block moves. The current rule is intentionally conservative, so it
114-
under-counts rather than ever over-counting, but the real elision transform needs the stronger
115-
analysis to catch the cases it currently rejects.
116-
- **Find more elision categories.** Beyond the three movable categories there are more: a value
117-
returned moves ownership out of the function (its drop is elidable), a uniquely-owned orphan
118-
temporary's drop can become a direct free rather than a decrement-and-check, a retain and release
119-
that cancel within a block regardless of category, and a struct construction that moves its fields
120-
in rather than retaining them. Each is its own rule to measure and prove.
121-
- **The counts are static, not runtime-weighted.** They count retain sites in the code, not their
122-
execution frequency, so they show the shape of the opportunity rather than the exact runtime total.
123-
Weighting by the per-request hot path would bridge to the runtime `vm-counters` number.
124-
- **Then build the transform.** The mechanism is already present: the dormant `moved` flag, which the
125-
drop emitter already respects. The elision sets it for proven moves and skips the paired retain,
126-
keyed on the same decision the measurement makes.
109+
### The elision transform (built, conservative first cut)
110+
111+
`elidable_moves` in the lowering decides when a reference-typed variable's last use moves it into a
112+
consumer (a call argument, a spawn argument, or an aliasing copy), and the lowering skips both the
113+
retain at that site and the variable's drop. It is deliberately conservative, so it can only
114+
under-elide, never over-elide: the site must be the last real use (loop-aware), the variable must
115+
appear exactly once at the site, and it must have a single drop the move dominates (approximated by no
116+
control flow between the move and the drop). Measured on the http hello path: incref 102 to 81 and
117+
decref 172 to 151 per request, memory-safe, all suites green.
118+
119+
### Refinements (there is a lot of headroom)
120+
121+
The static analysis found 80 movable retains but the runtime elided about 21 per request, so the gap
122+
is the conservatism plus what actually runs on the hot path. Each refinement widens what is elided, so
123+
each needs its own review since that is where an unsound over-elision would hide.
124+
125+
- **Proper dominance instead of "no control flow between".** The current straight-line rule rejects a
126+
move that happens before a branch, or in a `match` arm, even when the move still dominates the drop.
127+
A dominance analysis catches those and enables per-path and multi-drop elision.
128+
- **Full dataflow liveness.** Nested loops, `break`/`continue`, merge points, and multiple reaching
129+
definitions, replacing the loop-and-linear-last-use approximation.
130+
- **More elision categories.** A value returned moves ownership out of the function, so its drop is
131+
elidable. A uniquely-owned orphan temporary's drop can become a direct free rather than a
132+
decrement-and-check, which attacks the roughly 94 allocations per request that RC-op elision alone
133+
does not touch. A retain and release that cancel within a block regardless of category. A struct or
134+
enum construction that moves its fields in rather than retaining them.
135+
- **The report counts are static, not runtime-weighted.** Weighting by the per-request hot path would
136+
bridge `--rc-report` to the runtime `vm-counters` number.
137+
138+
### Guiding model: Rust's analysis as an oracle, not a gate
139+
140+
The north star is to take Rust's ownership and borrow analysis as an internal optimization oracle
141+
while keeping none of its surface. Where the analysis proves a value's lifetime is clean the reference
142+
counting is dropped, and where it cannot the reference counting stays, which is always correct. So the
143+
programmer never sees a move, a borrow, or an ownership error, and no program is ever rejected. This
144+
is the difference from Rust: Rust rejects what it cannot prove, this keeps the refcount.
145+
146+
Under this model the current move elision is only the caller-side, last-use case. The larger lever is
147+
the borrow, Rust's `&T`: a parameter the callee only reads is borrowed, so the caller needs no retain
148+
at all, even when it uses the value again after the call. Realizing it needs a per-function escape
149+
summary (does a parameter get stored into a field, returned, or spawned, or is it only read), which
150+
makes it interprocedural and wants a fixpoint over recursion. It stays fallback-safe: an unknown or
151+
conservatively-escaping callee just keeps the retain, so a wrong summary costs performance, never
152+
correctness. Most parameters are borrows, so this is a much larger category than move elision, and
153+
together they approach a common case with no reference counting, leaving it only for genuine shared
154+
references, escapes into aggregates, and cycles, where the future cycle collector takes over.
127155

128156
## Where this sits
129157

0 commit comments

Comments
 (0)