@@ -4999,28 +4999,58 @@ impl Function {
49994999 }
50005000 }
50015001
5002+ /// Load Store Forwarding traverses individual blocks to simplify redundant LoadField and StoreField instructions.
5003+ /// This is done through abstract interpretation on an abstract domain of an `InsnId` and an `i32` offset.
5004+ /// For further reading, see the following Rails at Scale blog post.
5005+ /// <https://railsatscale.com/2026-03-18-how-zjit-removes-redundant-object-loads-and-stores/>
50025006 fn optimize_load_store ( & mut self ) {
5007+ /// HeapKey represents the abstract domain that load store optimization is built upon
5008+ #[ derive( PartialEq , Eq , Hash , Clone , Copy ) ]
5009+ struct HeapKey {
5010+ object : InsnId ,
5011+ offset : i32 ,
5012+ }
5013+
5014+ // This helper function is used to clear the cache for matching offsets
5015+ // while we don't have type based alias analysis.
5016+ // TBAA will primarily modify any part of this optimization that
5017+ // currently uses this helper function.
5018+ fn invalidate_cached_aliases ( map : & mut HashMap < HeapKey , InsnId > , offset : i32 ) {
5019+ map. retain ( |HeapKey { object : _, offset : off } , _| * off != offset) ;
5020+ }
5021+
5022+ // Algorithm sketch
5023+ // 1. For each block, construct a heap.
5024+ // 2. Iterate through the block, adding the following to the heap when encountered:
5025+ // - HeapKey (LoadField or StoreField instruction combined with an offset)
5026+ // - Value (Another instruction because HIR is in SSA form)
5027+ // 3. If a known key but new value is encountered, update the value
5028+ // 4. If a known key and known value is entered:
5029+ // - Elide LoadField instructions and fix up the SSA
5030+ // - Replace StoreField instructions with the value and fix up the SSA
5031+ // 5. If we run into any aliasing issues or effectful instructions, clear the offending portions of our heap (invalidate our optimization assumptions)
5032+ // 6. Handle tricky WriteBarrier cases or LoadField issues from type checking bugs
5033+
50035034 for block in self . reverse_post_order ( ) {
5004- let mut compile_time_heap: HashMap < ( InsnId , i32 ) , InsnId > = HashMap :: new ( ) ;
5035+ // Potential TODO: Replace hashmap with a data structure with lower overhead, even if we lose the constant-time-lookup benefit.
5036+ // This is not high priority, but is likely the worst offender in the optimize_load_store pass
5037+ let mut compile_time_heap: HashMap < HeapKey , InsnId > = HashMap :: new ( ) ;
50055038 let old_insns = std:: mem:: take ( & mut self . blocks [ block. 0 ] . insns ) ;
50065039 let mut new_insns = vec ! [ ] ;
50075040 for insn_id in old_insns {
5008- let replacement_insn : InsnId = match self . find ( insn_id) {
5041+ match self . find ( insn_id) {
50095042 Insn :: StoreField { recv, offset, val, .. } => {
5010- let key = ( self . chase_insn ( recv) , offset) ;
5043+ let key = HeapKey { object : self . chase_insn ( recv) , offset } ;
50115044 let heap_entry = compile_time_heap. get ( & key) . copied ( ) ;
5012- // TODO(Jacob): Switch from actual to partial equality
50135045 if Some ( val) == heap_entry {
50145046 // If the value is already stored, short circuit and don't add an instruction to the block
50155047 continue
50165048 }
5017- // TODO(Jacob): Add TBAA to avoid removing so many entries
5018- compile_time_heap. retain ( |( _, off) , _| * off != offset) ;
5049+ invalidate_cached_aliases ( & mut compile_time_heap, offset) ;
50195050 compile_time_heap. insert ( key, val) ;
5020- insn_id
50215051 } ,
50225052 Insn :: LoadField { recv, offset, return_type, .. } => {
5023- let key = ( self . chase_insn ( recv) , offset) ;
5053+ let key = HeapKey { object : self . chase_insn ( recv) , offset } ;
50245054 match compile_time_heap. entry ( key) {
50255055 std:: collections:: hash_map:: Entry :: Occupied ( entry) => {
50265056 let cached_insn = * entry. get ( ) ;
@@ -5047,27 +5077,23 @@ impl Function {
50475077 compile_time_heap. insert ( key, insn_id) ;
50485078 }
50495079 }
5050- insn_id
50515080 }
50525081 Insn :: WriteBarrier { .. } => {
50535082 // Currently, WriteBarrier write effects are Allocator and Memory when we'd really like them to be flags.
50545083 // We don't use LoadField for mark bits so we can ignore them for now.
50555084 // But flags does not exist in our effects abstract heap modeling and we don't want to add special casing to effects.
50565085 // This special casing in this pass here should be removed once we refine our effects system to provide greater granularity for WriteBarrier.
5057- // TODO: use TBAA
50585086 let offset = RUBY_OFFSET_RBASIC_FLAGS ;
5059- compile_time_heap. retain ( |( _, off) , _| * off != offset) ;
5060- insn_id
5087+ invalidate_cached_aliases ( & mut compile_time_heap, offset) ;
50615088 } ,
50625089 insn => {
5063- // If an instruction affects memory and we haven't modeled it, the compile_time_heap is invalidated
5090+ // If an instruction affects memory and we haven't modeled it, the entire compile_time_heap is invalidated
50645091 if insn. effects_of ( ) . includes ( Effect :: write ( abstract_heaps:: Memory ) ) {
50655092 compile_time_heap. clear ( ) ;
50665093 }
5067- insn_id
50685094 }
50695095 } ;
5070- new_insns. push ( replacement_insn ) ;
5096+ new_insns. push ( insn_id ) ;
50715097 }
50725098 self . blocks [ block. 0 ] . insns = new_insns;
50735099 }
0 commit comments