@@ -4984,28 +4984,58 @@ impl Function {
49844984 }
49854985 }
49864986
4987+ /// Load Store Forwarding traverses individual blocks to simplify redundant LoadField and StoreField instructions.
4988+ /// This is done through abstract interpretation on an abstract domain of an `InsnId` and an `i32` offset.
4989+ /// For further reading, see the following Rails at Scale blog post.
4990+ /// <https://railsatscale.com/2026-03-18-how-zjit-removes-redundant-object-loads-and-stores/>
49874991 fn optimize_load_store ( & mut self ) {
4992+ /// HeapKey represents the abstract domain that load store optimization is built upon
4993+ #[ derive( PartialEq , Eq , Hash , Clone , Copy ) ]
4994+ struct HeapKey {
4995+ object : InsnId ,
4996+ offset : i32 ,
4997+ }
4998+
4999+ // This helper function is used to clear the cache for matching offsets
5000+ // while we don't have type based alias analysis.
5001+ // TBAA will primarily modify any part of this optimization that
5002+ // currently uses this helper function.
5003+ fn invalidate_cached_aliases ( map : & mut HashMap < HeapKey , InsnId > , offset : i32 ) {
5004+ map. retain ( |HeapKey { object : _, offset : off } , _| * off != offset) ;
5005+ }
5006+
5007+ // Algorithm sketch
5008+ // 1. For each block, construct a heap.
5009+ // 2. Iterate through the block, adding the following to the heap when encountered:
5010+ // - HeapKey (LoadField or StoreField instruction combined with an offset)
5011+ // - Value (Another instruction because HIR is in SSA form)
5012+ // 3. If a known key but new value is encountered, update the value
5013+ // 4. If a known key and known value is entered:
5014+ // - Elide LoadField instructions and fix up the SSA
5015+ // - Replace StoreField instructions with the value and fix up the SSA
5016+ // 5. If we run into any aliasing issues or effectful instructions, clear the offending portions of our heap (invalidate our optimization assumptions)
5017+ // 6. Handle tricky WriteBarrier cases or LoadField issues from type checking bugs
5018+
49885019 for block in self . reverse_post_order ( ) {
4989- let mut compile_time_heap: HashMap < ( InsnId , i32 ) , InsnId > = HashMap :: new ( ) ;
5020+ // Potential TODO: Replace hashmap with a data structure with lower overhead, even if we lose the constant-time-lookup benefit.
5021+ // This is not high priority, but is likely the worst offender in the optimize_load_store pass
5022+ let mut compile_time_heap: HashMap < HeapKey , InsnId > = HashMap :: new ( ) ;
49905023 let old_insns = std:: mem:: take ( & mut self . blocks [ block. 0 ] . insns ) ;
49915024 let mut new_insns = vec ! [ ] ;
49925025 for insn_id in old_insns {
4993- let replacement_insn : InsnId = match self . find ( insn_id) {
5026+ match self . find ( insn_id) {
49945027 Insn :: StoreField { recv, offset, val, .. } => {
4995- let key = ( self . chase_insn ( recv) , offset) ;
5028+ let key = HeapKey { object : self . chase_insn ( recv) , offset } ;
49965029 let heap_entry = compile_time_heap. get ( & key) . copied ( ) ;
4997- // TODO(Jacob): Switch from actual to partial equality
49985030 if Some ( val) == heap_entry {
49995031 // If the value is already stored, short circuit and don't add an instruction to the block
50005032 continue
50015033 }
5002- // TODO(Jacob): Add TBAA to avoid removing so many entries
5003- compile_time_heap. retain ( |( _, off) , _| * off != offset) ;
5034+ invalidate_cached_aliases ( & mut compile_time_heap, offset) ;
50045035 compile_time_heap. insert ( key, val) ;
5005- insn_id
50065036 } ,
50075037 Insn :: LoadField { recv, offset, return_type, .. } => {
5008- let key = ( self . chase_insn ( recv) , offset) ;
5038+ let key = HeapKey { object : self . chase_insn ( recv) , offset } ;
50095039 match compile_time_heap. entry ( key) {
50105040 std:: collections:: hash_map:: Entry :: Occupied ( entry) => {
50115041 let cached_insn = * entry. get ( ) ;
@@ -5032,27 +5062,23 @@ impl Function {
50325062 compile_time_heap. insert ( key, insn_id) ;
50335063 }
50345064 }
5035- insn_id
50365065 }
50375066 Insn :: WriteBarrier { .. } => {
50385067 // Currently, WriteBarrier write effects are Allocator and Memory when we'd really like them to be flags.
50395068 // We don't use LoadField for mark bits so we can ignore them for now.
50405069 // But flags does not exist in our effects abstract heap modeling and we don't want to add special casing to effects.
50415070 // This special casing in this pass here should be removed once we refine our effects system to provide greater granularity for WriteBarrier.
5042- // TODO: use TBAA
50435071 let offset = RUBY_OFFSET_RBASIC_FLAGS ;
5044- compile_time_heap. retain ( |( _, off) , _| * off != offset) ;
5045- insn_id
5072+ invalidate_cached_aliases ( & mut compile_time_heap, offset) ;
50465073 } ,
50475074 insn => {
5048- // If an instruction affects memory and we haven't modeled it, the compile_time_heap is invalidated
5075+ // If an instruction affects memory and we haven't modeled it, the entire compile_time_heap is invalidated
50495076 if insn. effects_of ( ) . includes ( Effect :: write ( abstract_heaps:: Memory ) ) {
50505077 compile_time_heap. clear ( ) ;
50515078 }
5052- insn_id
50535079 }
50545080 } ;
5055- new_insns. push ( replacement_insn ) ;
5081+ new_insns. push ( insn_id ) ;
50565082 }
50575083 self . blocks [ block. 0 ] . insns = new_insns;
50585084 }
0 commit comments