Skip to content

Commit b539cd2

Browse files
authored
ZJIT: Deduplicate side exits (ruby#15105)
1 parent 557eec7 commit b539cd2

File tree

5 files changed

+154
-97
lines changed

5 files changed

+154
-97
lines changed

zjit/src/asm/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub mod x86_64;
1616
pub mod arm64;
1717

1818
/// Index to a label created by cb.new_label()
19-
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
19+
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
2020
pub struct Label(pub usize);
2121

2222
/// The object that knows how to encode the branch instruction.

zjit/src/backend/arm64/mod.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::mem::take;
22

33
use crate::asm::{CodeBlock, Label};
44
use crate::asm::arm64::*;
5+
use crate::codegen::split_patch_point;
56
use crate::cruby::*;
67
use crate::backend::lir::*;
78
use crate::options::asm_dump;
@@ -826,6 +827,14 @@ impl Assembler {
826827
*opnds = vec![];
827828
asm.push_insn(insn);
828829
}
830+
// For compile_exits, support splitting simple return values here
831+
Insn::CRet(opnd) => {
832+
match opnd {
833+
Opnd::Reg(C_RET_REG) => {},
834+
_ => asm.load_into(C_RET_OPND, *opnd),
835+
}
836+
asm.cret(C_RET_OPND);
837+
}
829838
Insn::Lea { opnd, out } => {
830839
*opnd = split_only_stack_membase(asm, *opnd, SCRATCH0_OPND, &stack_state);
831840
let mem_out = split_memory_write(out, SCRATCH0_OPND);
@@ -894,6 +903,9 @@ impl Assembler {
894903
}
895904
}
896905
}
906+
&mut Insn::PatchPoint { ref target, invariant, payload } => {
907+
split_patch_point(asm, target, invariant, payload);
908+
}
897909
_ => {
898910
asm.push_insn(insn);
899911
}
@@ -1514,7 +1526,7 @@ impl Assembler {
15141526
Insn::Jonz(opnd, target) => {
15151527
emit_cmp_zero_jump(cb, opnd.into(), false, target.clone());
15161528
},
1517-
Insn::PatchPoint(_) |
1529+
Insn::PatchPoint { .. } => unreachable!("PatchPoint should have been lowered to PadPatchPoint in arm64_scratch_split"),
15181530
Insn::PadPatchPoint => {
15191531
// If patch points are too close to each other or the end of the block, fill nop instructions
15201532
if let Some(last_patch_pos) = last_patch_pos {
@@ -1694,7 +1706,7 @@ mod tests {
16941706

16951707
let val64 = asm.add(CFP, Opnd::UImm(64));
16961708
asm.store(Opnd::mem(64, SP, 0x10), val64);
1697-
let side_exit = Target::SideExit { reason: SideExitReason::Interrupt, pc: 0 as _, stack: vec![], locals: vec![], label: None };
1709+
let side_exit = Target::SideExit { reason: SideExitReason::Interrupt, exit: SideExit { pc: Opnd::const_ptr(0 as *const u8), stack: vec![], locals: vec![] } };
16981710
asm.push_insn(Insn::Joz(val64, side_exit));
16991711
asm.parallel_mov(vec![(C_ARG_OPNDS[0], C_RET_OPND.with_num_bits(32)), (C_ARG_OPNDS[1], Opnd::mem(64, SP, -8))]);
17001712

zjit/src/backend/lir.rs

Lines changed: 110 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ use std::rc::Rc;
66
use std::sync::{Arc, Mutex};
77
use crate::codegen::local_size_and_idx_to_ep_offset;
88
use crate::cruby::{Qundef, RUBY_OFFSET_CFP_PC, RUBY_OFFSET_CFP_SP, SIZEOF_VALUE_I32, vm_stack_canary};
9-
use crate::hir::SideExitReason;
9+
use crate::hir::{Invariant, SideExitReason};
1010
use crate::options::{TraceExits, debug, get_option};
1111
use crate::cruby::VALUE;
12+
use crate::payload::IseqPayload;
1213
use crate::stats::{exit_counter_ptr, exit_counter_ptr_for_opcode, side_exit_counter, CompileError};
1314
use crate::virtualmem::CodePtr;
1415
use crate::asm::{CodeBlock, Label};
@@ -25,7 +26,7 @@ pub use crate::backend::current::{
2526
pub static JIT_PRESERVED_REGS: &[Opnd] = &[CFP, SP, EC];
2627

2728
// Memory operand base
28-
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
29+
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
2930
pub enum MemBase
3031
{
3132
/// Register: Every Opnd::Mem should have MemBase::Reg as of emit.
@@ -37,7 +38,7 @@ pub enum MemBase
3738
}
3839

3940
// Memory location
40-
#[derive(Copy, Clone, PartialEq, Eq)]
41+
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
4142
pub struct Mem
4243
{
4344
// Base register number or instruction index
@@ -87,7 +88,7 @@ impl fmt::Debug for Mem {
8788
}
8889

8990
/// Operand to an IR instruction
90-
#[derive(Clone, Copy, PartialEq, Eq)]
91+
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
9192
pub enum Opnd
9293
{
9394
None, // For insns with no output
@@ -298,6 +299,14 @@ impl From<VALUE> for Opnd {
298299
}
299300
}
300301

302+
/// Context for a side exit. If `SideExit` matches, it reuses the same code.
303+
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
304+
pub struct SideExit {
305+
pub pc: Opnd,
306+
pub stack: Vec<Opnd>,
307+
pub locals: Vec<Opnd>,
308+
}
309+
301310
/// Branch target (something that we can jump to)
302311
/// for branch instructions
303312
#[derive(Clone, Debug)]
@@ -309,13 +318,10 @@ pub enum Target
309318
Label(Label),
310319
/// Side exit to the interpreter
311320
SideExit {
312-
pc: *const VALUE,
313-
stack: Vec<Opnd>,
314-
locals: Vec<Opnd>,
315-
/// We use this to enrich asm comments.
321+
/// Context used for compiling the side exit
322+
exit: SideExit,
323+
/// We use this to increment exit counters
316324
reason: SideExitReason,
317-
/// Some if the side exit should write this label. We use it for patch points.
318-
label: Option<Label>,
319325
},
320326
}
321327

@@ -525,7 +531,7 @@ pub enum Insn {
525531
Or { left: Opnd, right: Opnd, out: Opnd },
526532

527533
/// Patch point that will be rewritten to a jump to a side exit on invalidation.
528-
PatchPoint(Target),
534+
PatchPoint { target: Target, invariant: Invariant, payload: *mut IseqPayload },
529535

530536
/// Make sure the last PatchPoint has enough space to insert a jump.
531537
/// We insert this instruction at the end of each block so that the jump
@@ -590,7 +596,7 @@ impl Insn {
590596
Insn::Jonz(_, target) |
591597
Insn::Label(target) |
592598
Insn::LeaJumpTarget { target, .. } |
593-
Insn::PatchPoint(target) => {
599+
Insn::PatchPoint { target, .. } => {
594600
Some(target)
595601
}
596602
_ => None,
@@ -652,7 +658,7 @@ impl Insn {
652658
Insn::Mov { .. } => "Mov",
653659
Insn::Not { .. } => "Not",
654660
Insn::Or { .. } => "Or",
655-
Insn::PatchPoint(_) => "PatchPoint",
661+
Insn::PatchPoint { .. } => "PatchPoint",
656662
Insn::PadPatchPoint => "PadPatchPoint",
657663
Insn::PosMarker(_) => "PosMarker",
658664
Insn::RShift { .. } => "RShift",
@@ -750,7 +756,7 @@ impl Insn {
750756
Insn::Jonz(_, target) |
751757
Insn::Label(target) |
752758
Insn::LeaJumpTarget { target, .. } |
753-
Insn::PatchPoint(target) => Some(target),
759+
Insn::PatchPoint { target, .. } => Some(target),
754760
_ => None
755761
}
756762
}
@@ -797,8 +803,8 @@ impl<'a> Iterator for InsnOpndIterator<'a> {
797803
Insn::Jz(target) |
798804
Insn::Label(target) |
799805
Insn::LeaJumpTarget { target, .. } |
800-
Insn::PatchPoint(target) => {
801-
if let Target::SideExit { stack, locals, .. } = target {
806+
Insn::PatchPoint { target, .. } => {
807+
if let Target::SideExit { exit: SideExit { stack, locals, .. }, .. } = target {
802808
let stack_idx = self.idx;
803809
if stack_idx < stack.len() {
804810
let opnd = &stack[stack_idx];
@@ -823,7 +829,7 @@ impl<'a> Iterator for InsnOpndIterator<'a> {
823829
return Some(opnd);
824830
}
825831

826-
if let Target::SideExit { stack, locals, .. } = target {
832+
if let Target::SideExit { exit: SideExit { stack, locals, .. }, .. } = target {
827833
let stack_idx = self.idx - 1;
828834
if stack_idx < stack.len() {
829835
let opnd = &stack[stack_idx];
@@ -966,8 +972,8 @@ impl<'a> InsnOpndMutIterator<'a> {
966972
Insn::Jz(target) |
967973
Insn::Label(target) |
968974
Insn::LeaJumpTarget { target, .. } |
969-
Insn::PatchPoint(target) => {
970-
if let Target::SideExit { stack, locals, .. } = target {
975+
Insn::PatchPoint { target, .. } => {
976+
if let Target::SideExit { exit: SideExit { stack, locals, .. }, .. } = target {
971977
let stack_idx = self.idx;
972978
if stack_idx < stack.len() {
973979
let opnd = &mut stack[stack_idx];
@@ -992,7 +998,7 @@ impl<'a> InsnOpndMutIterator<'a> {
992998
return Some(opnd);
993999
}
9941000

995-
if let Target::SideExit { stack, locals, .. } = target {
1001+
if let Target::SideExit { exit: SideExit { stack, locals, .. }, .. } = target {
9961002
let stack_idx = self.idx - 1;
9971003
if stack_idx < stack.len() {
9981004
let opnd = &mut stack[stack_idx];
@@ -1779,82 +1785,108 @@ impl Assembler
17791785

17801786
/// Compile Target::SideExit and convert it into Target::CodePtr for all instructions
17811787
pub fn compile_exits(&mut self) {
1788+
/// Compile the main side-exit code. This function takes only SideExit so
1789+
/// that it can be safely deduplicated by using SideExit as a dedup key.
1790+
fn compile_exit(asm: &mut Assembler, exit: &SideExit) {
1791+
let SideExit { pc, stack, locals } = exit;
1792+
1793+
asm_comment!(asm, "save cfp->pc");
1794+
asm.store(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_PC), *pc);
1795+
1796+
asm_comment!(asm, "save cfp->sp");
1797+
asm.lea_into(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_SP), Opnd::mem(64, SP, stack.len() as i32 * SIZEOF_VALUE_I32));
1798+
1799+
if !stack.is_empty() {
1800+
asm_comment!(asm, "write stack slots: {}", join_opnds(&stack, ", "));
1801+
for (idx, &opnd) in stack.iter().enumerate() {
1802+
asm.store(Opnd::mem(64, SP, idx as i32 * SIZEOF_VALUE_I32), opnd);
1803+
}
1804+
}
1805+
1806+
if !locals.is_empty() {
1807+
asm_comment!(asm, "write locals: {}", join_opnds(&locals, ", "));
1808+
for (idx, &opnd) in locals.iter().enumerate() {
1809+
asm.store(Opnd::mem(64, SP, (-local_size_and_idx_to_ep_offset(locals.len(), idx) - 1) * SIZEOF_VALUE_I32), opnd);
1810+
}
1811+
}
1812+
1813+
asm_comment!(asm, "exit to the interpreter");
1814+
asm.frame_teardown(&[]); // matching the setup in gen_entry_point()
1815+
asm.cret(Opnd::UImm(Qundef.as_u64()));
1816+
}
1817+
17821818
fn join_opnds(opnds: &Vec<Opnd>, delimiter: &str) -> String {
17831819
opnds.iter().map(|opnd| format!("{opnd}")).collect::<Vec<_>>().join(delimiter)
17841820
}
17851821

1822+
// Extract targets first so that we can update instructions while referencing part of them.
17861823
let mut targets = HashMap::new();
17871824
for (idx, insn) in self.insns.iter().enumerate() {
17881825
if let Some(target @ Target::SideExit { .. }) = insn.target() {
17891826
targets.insert(idx, target.clone());
17901827
}
17911828
}
17921829

1830+
// Map from SideExit to compiled Label. This table is used to deduplicate side exit code.
1831+
let mut compiled_exits: HashMap<SideExit, Label> = HashMap::new();
1832+
17931833
for (idx, target) in targets {
17941834
// Compile a side exit. Note that this is past the split pass and alloc_regs(),
17951835
// so you can't use an instruction that returns a VReg.
1796-
if let Target::SideExit { pc, stack, locals, reason, label } = target {
1797-
asm_comment!(self, "Exit: {reason}");
1798-
let side_exit_label = if let Some(label) = label {
1799-
Target::Label(label)
1800-
} else {
1801-
self.new_label("side_exit")
1802-
};
1803-
self.write_label(side_exit_label.clone());
1804-
1805-
// Restore the PC and the stack for regular side exits. We don't do this for
1806-
// side exits right after JIT-to-JIT calls, which restore them before the call.
1807-
asm_comment!(self, "write stack slots: {}", join_opnds(&stack, ", "));
1808-
for (idx, &opnd) in stack.iter().enumerate() {
1809-
self.store(Opnd::mem(64, SP, idx as i32 * SIZEOF_VALUE_I32), opnd);
1810-
}
1811-
1812-
asm_comment!(self, "write locals: {}", join_opnds(&locals, ", "));
1813-
for (idx, &opnd) in locals.iter().enumerate() {
1814-
self.store(Opnd::mem(64, SP, (-local_size_and_idx_to_ep_offset(locals.len(), idx) - 1) * SIZEOF_VALUE_I32), opnd);
1815-
}
1816-
1817-
asm_comment!(self, "save cfp->pc");
1818-
self.store(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_PC), Opnd::const_ptr(pc));
1819-
1820-
asm_comment!(self, "save cfp->sp");
1821-
self.lea_into(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_SP), Opnd::mem(64, SP, stack.len() as i32 * SIZEOF_VALUE_I32));
1822-
1823-
// Using C_RET_OPND as an additional scratch register, which is no longer used
1824-
if get_option!(stats) {
1825-
asm_comment!(self, "increment a side exit counter");
1826-
self.incr_counter(Opnd::const_ptr(exit_counter_ptr(reason)), 1.into());
1827-
1828-
if let SideExitReason::UnhandledYARVInsn(opcode) = reason {
1829-
asm_comment!(self, "increment an unhandled YARV insn counter");
1830-
self.incr_counter(Opnd::const_ptr(exit_counter_ptr_for_opcode(opcode)), 1.into());
1836+
if let Target::SideExit { exit: exit @ SideExit { pc, .. }, reason } = target {
1837+
// Only record the exit if `trace_side_exits` is defined and the counter is either the one specified
1838+
let should_record_exit = get_option!(trace_side_exits).map(|trace| match trace {
1839+
TraceExits::All => true,
1840+
TraceExits::Counter(counter) if counter == side_exit_counter(reason) => true,
1841+
_ => false,
1842+
}).unwrap_or(false);
1843+
1844+
// If enabled, instrument exits first, and then jump to a shared exit.
1845+
let counted_exit = if get_option!(stats) || should_record_exit {
1846+
let counted_exit = self.new_label("counted_exit");
1847+
self.write_label(counted_exit.clone());
1848+
asm_comment!(self, "Counted Exit: {reason}");
1849+
1850+
if get_option!(stats) {
1851+
asm_comment!(self, "increment a side exit counter");
1852+
self.incr_counter(Opnd::const_ptr(exit_counter_ptr(reason)), 1.into());
1853+
1854+
if let SideExitReason::UnhandledYARVInsn(opcode) = reason {
1855+
asm_comment!(self, "increment an unhandled YARV insn counter");
1856+
self.incr_counter(Opnd::const_ptr(exit_counter_ptr_for_opcode(opcode)), 1.into());
1857+
}
18311858
}
1832-
}
1833-
1834-
if get_option!(trace_side_exits).is_some() {
1835-
// Get the corresponding `Counter` for the current `SideExitReason`.
1836-
let side_exit_counter = side_exit_counter(reason);
1837-
1838-
// Only record the exit if `trace_side_exits` is defined and the counter is either the one specified
1839-
let should_record_exit = get_option!(trace_side_exits)
1840-
.map(|trace| match trace {
1841-
TraceExits::All => true,
1842-
TraceExits::Counter(counter) if counter == side_exit_counter => true,
1843-
_ => false,
1844-
})
1845-
.unwrap_or(false);
18461859

18471860
if should_record_exit {
1848-
asm_ccall!(self, rb_zjit_record_exit_stack, Opnd::const_ptr(pc as *const u8));
1861+
// Preserve caller-saved registers that may be used in the shared exit.
1862+
self.cpush_all();
1863+
asm_ccall!(self, rb_zjit_record_exit_stack, pc);
1864+
self.cpop_all();
18491865
}
1850-
}
18511866

1852-
asm_comment!(self, "exit to the interpreter");
1853-
self.frame_teardown(&[]); // matching the setup in :bb0-prologue:
1854-
self.mov(C_RET_OPND, Opnd::UImm(Qundef.as_u64()));
1855-
self.cret(C_RET_OPND);
1867+
// If the side exit has already been compiled, jump to it.
1868+
// Otherwise, let it fall through and compile the exit next.
1869+
if let Some(&exit_label) = compiled_exits.get(&exit) {
1870+
self.jmp(Target::Label(exit_label));
1871+
}
1872+
Some(counted_exit)
1873+
} else {
1874+
None
1875+
};
1876+
1877+
// Compile the shared side exit if not compiled yet
1878+
let compiled_exit = if let Some(&compiled_exit) = compiled_exits.get(&exit) {
1879+
Target::Label(compiled_exit)
1880+
} else {
1881+
let new_exit = self.new_label("side_exit");
1882+
self.write_label(new_exit.clone());
1883+
asm_comment!(self, "Exit: {pc}");
1884+
compile_exit(self, &exit);
1885+
compiled_exits.insert(exit, new_exit.unwrap_label());
1886+
new_exit
1887+
};
18561888

1857-
*self.insns[idx].target_mut().unwrap() = side_exit_label;
1889+
*self.insns[idx].target_mut().unwrap() = counted_exit.unwrap_or(compiled_exit);
18581890
}
18591891
}
18601892
}
@@ -2268,8 +2300,8 @@ impl Assembler {
22682300
out
22692301
}
22702302

2271-
pub fn patch_point(&mut self, target: Target) {
2272-
self.push_insn(Insn::PatchPoint(target));
2303+
pub fn patch_point(&mut self, target: Target, invariant: Invariant, payload: *mut IseqPayload) {
2304+
self.push_insn(Insn::PatchPoint { target, invariant, payload });
22732305
}
22742306

22752307
pub fn pad_patch_point(&mut self) {

0 commit comments

Comments
 (0)