Skip to content

Commit baff5e8

Browse files
committed
Improve load store readability and documentation
1 parent 4dfaa2c commit baff5e8

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
@@ -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

Comments
 (0)