Skip to content

Commit 3eac82b

Browse files
committed
ZJIT: Return nil instead of side-exiting on OOB Array#[] / String#getbyte
Bounds failures on these inlines used to side-exit with no recompile feedback, so OOB-heavy callers (e.g. ActionDispatch::Journey's route tokenizer) ran interpreted forever. Both return nil out of bounds, so emit ArrayArefOrNil / StringGetbyteOrNil which handle it in JIT code. 3.5% faster on railsbench.
1 parent 435f43b commit 3eac82b

4 files changed

Lines changed: 230 additions & 132 deletions

File tree

zjit/src/codegen.rs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
628628
Insn::ArrayDup { val, state } => gen_array_dup(jit, asm, function, *val, opnd!(val), &function.frame_state(*state)),
629629
Insn::AdjustBounds { index, length } => gen_adjust_bounds(asm, opnd!(index), opnd!(length)),
630630
Insn::ArrayAref { array, index, .. } => gen_array_aref(asm, opnd!(array), opnd!(index)),
631+
Insn::ArrayArefOrNil { array, index } => gen_array_aref_or_nil(jit, asm, opnd!(array), opnd!(index)),
631632
Insn::ArrayAset { array, index, val } => {
632633
no_output!(gen_array_aset(asm, opnd!(array), opnd!(index), opnd!(val)))
633634
}
@@ -638,6 +639,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
638639
Insn::StringCopy { val, chilled, state } => gen_string_copy(jit, asm, function, *val, opnd!(val), *chilled, &function.frame_state(*state)),
639640
Insn::StringConcat { strings, state } => gen_string_concat(jit, asm, function, opnds!(strings), &function.frame_state(*state)),
640641
&Insn::StringGetbyte { string, index } => gen_string_getbyte(asm, opnd!(string), opnd!(index)),
642+
&Insn::StringGetbyteOrNil { string, index } => gen_string_getbyte_or_nil(jit, asm, opnd!(string), opnd!(index)),
641643
Insn::StringSetbyteFixnum { string, index, value } => gen_string_setbyte_fixnum(asm, opnd!(string), opnd!(index), opnd!(value)),
642644
Insn::StringAppend { recv, other, state } => gen_string_append(jit, asm, function, opnd!(recv), opnd!(other), &function.frame_state(*state)),
643645
Insn::StringAppendCodepoint { recv, other, state } => gen_string_append_codepoint(jit, asm, function, opnd!(recv), opnd!(other), &function.frame_state(*state)),
@@ -2217,6 +2219,69 @@ fn gen_array_aref(
22172219
asm.load(Opnd::mem(VALUE_BITS, elem_ptr, 0))
22182220
}
22192221

2222+
/// Compile array access (`array[index]`) where `index` is a raw (unboxed)
2223+
/// C long that may be negative or out of bounds. Out-of-bounds reads yield
2224+
/// nil instead of side-exiting, matching rb_ary_entry semantics.
2225+
fn gen_array_aref_or_nil(
2226+
jit: &mut JITState,
2227+
asm: &mut Assembler,
2228+
array: Opnd,
2229+
index: Opnd,
2230+
) -> lir::Opnd {
2231+
asm_comment!(asm, "ArrayArefOrNil");
2232+
let hir_block_id = asm.current_block().hir_block_id;
2233+
let rpo_idx = asm.current_block().rpo_index;
2234+
2235+
let load_block = asm.new_block(hir_block_id, false, rpo_idx);
2236+
let result_block = asm.new_block(hir_block_id, false, rpo_idx);
2237+
let result_edge = |v| Target::Block(Box::new(lir::BranchEdge {
2238+
target: result_block,
2239+
args: vec![v],
2240+
}));
2241+
2242+
let idx = asm.load_mem(index);
2243+
let array = asm.load_mem(array);
2244+
let length = gen_array_length(asm, array);
2245+
let array_ptr = gen_array_ptr(asm, array);
2246+
2247+
// Adjust a negative index by the length, as in gen_adjust_bounds
2248+
let adjusted = asm.add(idx, length);
2249+
asm.test(idx, idx);
2250+
let adjusted = asm.csel_l(adjusted, idx);
2251+
2252+
// In bounds iff (u64)adjusted < (u64)length; this single unsigned
2253+
// comparison covers both negative-out-of-range and past-the-end indices.
2254+
asm.cmp(adjusted, length);
2255+
asm.jb(Target::Block(Box::new(lir::BranchEdge {
2256+
target: load_block,
2257+
args: vec![array_ptr, adjusted],
2258+
})));
2259+
2260+
// Out of bounds: nil
2261+
asm.jmp(result_edge(Qnil.into()));
2262+
2263+
// In bounds: load the element
2264+
asm.set_current_block(load_block);
2265+
let label = jit.get_label(asm, load_block, hir_block_id);
2266+
asm.write_label(label);
2267+
let array_ptr = asm.new_block_param(VALUE_BITS);
2268+
asm.current_block().add_parameter(array_ptr);
2269+
let adjusted = asm.new_block_param(VALUE_BITS);
2270+
asm.current_block().add_parameter(adjusted);
2271+
let elem_offset = asm.lshift(adjusted, Opnd::UImm(SIZEOF_VALUE.trailing_zeros() as u64));
2272+
let elem_ptr = asm.add(array_ptr, elem_offset);
2273+
let elem = asm.load(Opnd::mem(VALUE_BITS, elem_ptr, 0));
2274+
asm.jmp(result_edge(elem));
2275+
2276+
// Join block
2277+
asm.set_current_block(result_block);
2278+
let label = jit.get_label(asm, result_block, hir_block_id);
2279+
asm.write_label(label);
2280+
let param = asm.new_block_param(VALUE_BITS);
2281+
asm.current_block().add_parameter(param);
2282+
param
2283+
}
2284+
22202285
fn gen_array_aset(
22212286
asm: &mut Assembler,
22222287
array: Opnd,
@@ -4068,6 +4133,77 @@ fn gen_string_getbyte(asm: &mut Assembler, string: Opnd, index: Opnd) -> Opnd {
40684133
// TODO(max): Use SIB indexing here once the backend supports it
40694134
let string_ptr = asm.add(string_ptr, index);
40704135
let byte = asm.load(Opnd::mem(8, string_ptr, 0));
4136+
tag_byte(asm, byte)
4137+
}
4138+
4139+
/// Compile byte access (`string.getbyte(index)`) where `index` is a raw
4140+
/// (unboxed) C long that may be negative or out of bounds. Out-of-bounds
4141+
/// reads yield nil instead of side-exiting, matching rb_str_getbyte semantics.
4142+
fn gen_string_getbyte_or_nil(
4143+
jit: &mut JITState,
4144+
asm: &mut Assembler,
4145+
string: Opnd,
4146+
index: Opnd,
4147+
) -> lir::Opnd {
4148+
asm_comment!(asm, "StringGetbyteOrNil");
4149+
let hir_block_id = asm.current_block().hir_block_id;
4150+
let rpo_idx = asm.current_block().rpo_index;
4151+
4152+
let load_block = asm.new_block(hir_block_id, false, rpo_idx);
4153+
let result_block = asm.new_block(hir_block_id, false, rpo_idx);
4154+
let result_edge = |v| Target::Block(Box::new(lir::BranchEdge {
4155+
target: result_block,
4156+
args: vec![v],
4157+
}));
4158+
4159+
let idx = asm.load_mem(index);
4160+
let string_reg = asm.load_mem(string);
4161+
// struct RString stores `len` at a fixed offset for both embedded and
4162+
// heap strings, so this is a plain load (unlike RArray).
4163+
let length = asm.load(Opnd::mem(VALUE_BITS, string_reg, RUBY_OFFSET_RSTRING_LEN));
4164+
let string_ptr = get_string_ptr(asm, string);
4165+
4166+
// Adjust a negative index by the length, as in gen_adjust_bounds
4167+
let adjusted = asm.add(idx, length);
4168+
asm.test(idx, idx);
4169+
let adjusted = asm.csel_l(adjusted, idx);
4170+
4171+
// In bounds iff (u64)adjusted < (u64)length; this single unsigned
4172+
// comparison covers both negative-out-of-range and past-the-end indices.
4173+
asm.cmp(adjusted, length);
4174+
asm.jb(Target::Block(Box::new(lir::BranchEdge {
4175+
target: load_block,
4176+
args: vec![string_ptr, adjusted],
4177+
})));
4178+
4179+
// Out of bounds: nil
4180+
asm.jmp(result_edge(Qnil.into()));
4181+
4182+
// In bounds: load the byte
4183+
asm.set_current_block(load_block);
4184+
let label = jit.get_label(asm, load_block, hir_block_id);
4185+
asm.write_label(label);
4186+
let string_ptr = asm.new_block_param(VALUE_BITS);
4187+
asm.current_block().add_parameter(string_ptr);
4188+
let adjusted = asm.new_block_param(VALUE_BITS);
4189+
asm.current_block().add_parameter(adjusted);
4190+
// TODO(max): Use SIB indexing here once the backend supports it
4191+
let byte_ptr = asm.add(string_ptr, adjusted);
4192+
let byte = asm.load(Opnd::mem(8, byte_ptr, 0));
4193+
let byte = tag_byte(asm, byte);
4194+
asm.jmp(result_edge(byte));
4195+
4196+
// Join block
4197+
asm.set_current_block(result_block);
4198+
let label = jit.get_label(asm, result_block, hir_block_id);
4199+
asm.write_label(label);
4200+
let param = asm.new_block_param(VALUE_BITS);
4201+
asm.current_block().add_parameter(param);
4202+
param
4203+
}
4204+
4205+
/// Zero-extend a byte loaded from a string and tag it as a Fixnum
4206+
fn tag_byte(asm: &mut Assembler, byte: Opnd) -> Opnd {
40714207
// Zero-extend the byte to 64 bits
40724208
let byte = byte.with_num_bits(64);
40734209
let byte = asm.and(byte, 0xFF.into());

zjit/src/cruby_methods.rs

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -367,13 +367,9 @@ fn inline_array_aref(fun: &mut hir::Function, block: hir::BlockId, recv: hir::In
367367
let recv = fun.coerce_to(block, recv, types::Array, state);
368368
let index = fun.coerce_to(block, index, types::Fixnum, state);
369369
let index = fun.push_insn(block, hir::Insn::UnboxFixnum { val: index });
370-
let length = fun.push_insn(block, hir::Insn::ArrayLength { array: recv });
371-
let index = fun.push_insn(block, hir::Insn::GuardLess { left: index, right: length, reason: Box::new(SideExitReason::GuardLess), state });
372-
let index = fun.push_insn(block, hir::Insn::AdjustBounds { index, length });
373-
let zero = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(0) });
374-
use crate::hir::SideExitReason;
375-
let index = fun.push_insn(block, hir::Insn::GuardGreaterEq { left: index, right: zero, reason: Box::new(SideExitReason::GuardGreaterEq), state });
376-
let result = fun.push_insn(block, hir::Insn::ArrayAref { array: recv, index });
370+
// Out-of-bounds reads are part of Array#[] semantics (they return nil),
371+
// so use a total instruction instead of bounds guards that side-exit.
372+
let result = fun.push_insn(block, hir::Insn::ArrayArefOrNil { array: recv, index });
377373
return Some(result);
378374
}
379375
}
@@ -478,17 +474,9 @@ fn inline_string_getbyte(fun: &mut hir::Function, block: hir::BlockId, recv: hir
478474
// when converting the index to a C integer.
479475
let index = fun.coerce_to(block, index, types::Fixnum, state);
480476
let unboxed_index = fun.push_insn(block, hir::Insn::UnboxFixnum { val: index });
481-
let len = fun.load_string_length(block, recv);
482-
// TODO(max): Find a way to mark these guards as not needed for correctness... as in, once
483-
// the data dependency is gone (say, the StringGetbyte is elided), they can also be elided.
484-
//
485-
// This is unlike most other guards.
486-
let unboxed_index = fun.push_insn(block, hir::Insn::GuardLess { left: unboxed_index, right: len, reason: Box::new(SideExitReason::GuardLess), state });
487-
let unboxed_index = fun.push_insn(block, hir::Insn::AdjustBounds { index: unboxed_index, length: len });
488-
let zero = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(0) });
489-
use crate::hir::SideExitReason;
490-
let _ = fun.push_insn(block, hir::Insn::GuardGreaterEq { left: unboxed_index, right: zero, reason: Box::new(SideExitReason::GuardGreaterEq), state });
491-
let result = fun.push_insn(block, hir::Insn::StringGetbyte { string: recv, index: unboxed_index });
477+
// Out-of-bounds reads are part of String#getbyte semantics (they return nil),
478+
// so use a total instruction instead of bounds guards that side-exit.
479+
let result = fun.push_insn(block, hir::Insn::StringGetbyteOrNil { string: recv, index: unboxed_index });
492480
return Some(result);
493481
}
494482
None

zjit/src/hir.rs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,10 @@ pub enum Insn {
980980
StringConcat { strings: Vec<InsnId>, state: InsnId },
981981
/// Call rb_str_getbyte with known-Fixnum index
982982
StringGetbyte { string: InsnId, index: InsnId },
983+
/// Load the byte at `index` (a raw, unboxed C `long`, possibly negative or
984+
/// out of bounds). Returns the byte as a tagged Fixnum, or `nil` when out
985+
/// of bounds. Unlike [`Insn::StringGetbyte`], never side-exits.
986+
StringGetbyteOrNil { string: InsnId, index: InsnId },
983987
StringSetbyteFixnum { string: InsnId, index: InsnId, value: InsnId },
984988
StringAppend { recv: InsnId, other: InsnId, state: InsnId },
985989
StringAppendCodepoint { recv: InsnId, other: InsnId, state: InsnId },
@@ -1013,6 +1017,10 @@ pub enum Insn {
10131017
/// Push `val` onto `array`, where `array` is already `Array`.
10141018
ArrayPush { array: InsnId, val: InsnId, state: InsnId },
10151019
ArrayAref { array: InsnId, index: InsnId },
1020+
/// Array access where `index` is a raw (unboxed) C `long`, possibly negative
1021+
/// or out of bounds. Returns the element, or `nil` when out of bounds.
1022+
/// Unlike [`Insn::ArrayAref`], never side-exits.
1023+
ArrayArefOrNil { array: InsnId, index: InsnId },
10161024
ArrayAset { array: InsnId, index: InsnId, val: InsnId },
10171025
ArrayPop { array: InsnId, state: InsnId },
10181026
/// Return the length of the array as a C `long` ([`types::CInt64`])
@@ -1393,7 +1401,7 @@ macro_rules! for_each_operand_impl {
13931401
$visit_many!(strings);
13941402
$visit_one!(*state);
13951403
}
1396-
Insn::StringGetbyte { string, index } => {
1404+
Insn::StringGetbyte { string, index } | Insn::StringGetbyteOrNil { string, index } => {
13971405
$visit_one!(*string);
13981406
$visit_one!(*index);
13991407
}
@@ -1506,7 +1514,7 @@ macro_rules! for_each_operand_impl {
15061514
$visit_one!(*val);
15071515
$visit_one!(*state);
15081516
}
1509-
Insn::ArrayAref { array, index } => {
1517+
Insn::ArrayAref { array, index } | Insn::ArrayArefOrNil { array, index } => {
15101518
$visit_one!(*array);
15111519
$visit_one!(*index);
15121520
}
@@ -1723,6 +1731,7 @@ impl Insn {
17231731
Insn::StringIntern { .. } => effects::Any,
17241732
Insn::StringConcat { .. } => effects::Any,
17251733
Insn::StringGetbyte { .. } => Effect::read_write(abstract_heaps::Other, abstract_heaps::Empty),
1734+
Insn::StringGetbyteOrNil { .. } => Effect::read_write(abstract_heaps::Other, abstract_heaps::Empty),
17261735
Insn::StringSetbyteFixnum { .. } => effects::Any,
17271736
Insn::StringAppend { .. } => effects::Any,
17281737
Insn::StringAppendCodepoint { .. } => effects::Any,
@@ -1754,6 +1763,7 @@ impl Insn {
17541763
Insn::ArrayExtend { .. } => effects::Any,
17551764
Insn::ArrayPush { .. } => effects::Any,
17561765
Insn::ArrayAref { .. } => effects::Any,
1766+
Insn::ArrayArefOrNil { .. } => effects::Any,
17571767
Insn::ArrayAset { .. } => effects::Any,
17581768
Insn::ArrayPop { .. } => effects::Any,
17591769
Insn::ArrayLength { .. } => Effect::write(abstract_heaps::Empty),
@@ -2037,6 +2047,9 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> {
20372047
Insn::ArrayAref { array, index, .. } => {
20382048
write!(f, "ArrayAref {array}, {index}")
20392049
}
2050+
Insn::ArrayArefOrNil { array, index, .. } => {
2051+
write!(f, "ArrayArefOrNil {array}, {index}")
2052+
}
20402053
Insn::ArrayAset { array, index, val, ..} => {
20412054
write!(f, "ArrayAset {array}, {index}, {val}")
20422055
}
@@ -2118,6 +2131,9 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> {
21182131
Insn::StringGetbyte { string, index, .. } => {
21192132
write!(f, "StringGetbyte {string}, {index}")
21202133
}
2134+
Insn::StringGetbyteOrNil { string, index, .. } => {
2135+
write!(f, "StringGetbyteOrNil {string}, {index}")
2136+
}
21212137
Insn::StringSetbyteFixnum { string, index, value, .. } => {
21222138
write!(f, "StringSetbyteFixnum {string}, {index}, {value}")
21232139
}
@@ -3386,6 +3402,7 @@ impl Function {
33863402
Insn::StringIntern { .. } => types::Symbol,
33873403
Insn::StringConcat { .. } => types::StringExact,
33883404
Insn::StringGetbyte { .. } => types::Fixnum,
3405+
Insn::StringGetbyteOrNil { .. } => types::Fixnum.union(types::NilClass),
33893406
Insn::StringSetbyteFixnum { .. } => types::Fixnum,
33903407
Insn::StringAppend { .. } => types::StringExact,
33913408
Insn::StringAppendCodepoint { .. } => types::StringExact,
@@ -3394,6 +3411,7 @@ impl Function {
33943411
Insn::NewArray { .. } => types::ArrayExact,
33953412
Insn::ArrayDup { .. } => types::ArrayExact,
33963413
Insn::ArrayAref { .. } => types::BasicObject,
3414+
Insn::ArrayArefOrNil { .. } => types::BasicObject,
33973415
Insn::ArrayPop { .. } => types::BasicObject,
33983416
Insn::ArrayLength { .. } => types::CInt64,
33993417
Insn::AdjustBounds { .. } => types::CInt64,
@@ -6326,7 +6344,7 @@ impl Function {
63266344
_ => None,
63276345
})
63286346
}
6329-
&Insn::ArrayAref { array, index }
6347+
&Insn::ArrayAref { array, index } | &Insn::ArrayArefOrNil { array, index }
63306348
if self.type_of(array).ruby_object_known()
63316349
&& self.type_of(index).is_subtype(types::CInt64) => {
63326350
let array_obj = self.type_of(array).ruby_object().unwrap();
@@ -7157,7 +7175,7 @@ impl Function {
71577175
| Insn::ArrayLength { array, .. } => {
71587176
self.assert_subtype(insn_id, array, types::Array)
71597177
}
7160-
Insn::ArrayAref { array, index } => {
7178+
Insn::ArrayAref { array, index } | Insn::ArrayArefOrNil { array, index } => {
71617179
self.assert_subtype(insn_id, array, types::Array)?;
71627180
self.assert_subtype(insn_id, index, types::CInt64)
71637181
}
@@ -7300,7 +7318,7 @@ impl Function {
73007318
self.assert_subtype(insn_id, left, types::CInt64)?;
73017319
self.assert_subtype(insn_id, right, types::CInt64)
73027320
},
7303-
Insn::StringGetbyte { string, index } => {
7321+
Insn::StringGetbyte { string, index } | Insn::StringGetbyteOrNil { string, index } => {
73047322
self.assert_subtype(insn_id, string, types::String)?;
73057323
self.assert_subtype(insn_id, index, types::CInt64)
73067324
},

0 commit comments

Comments
 (0)