Skip to content

Commit e656eb4

Browse files
committed
Improve load store readability and documentation
1 parent 613949a commit e656eb4

1 file changed

Lines changed: 41 additions & 15 deletions

File tree

zjit/src/hir.rs

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)