Skip to content

Commit 1f41a5f

Browse files
committed
ZJIT: Annotate Array#size, Array#first, and Array#last
Array#size shares rb_ary_length with Array#length, so it was already covered by the existing annotation; add an explicit entry for it. Array#first and Array#last with no arguments used to compile to opaque invokebuiltin calls of per-line cexpr! functions, which ZJIT could not annotate by a stable function pointer. Turn those cexpr! calls into named rb_jit_ary_first/rb_jit_ary_last primitives and annotate them so ZJIT inlines them as ArrayLength + bounds guard + ArrayAref, side-exiting on empty arrays.
1 parent 9e58d04 commit 1f41a5f

4 files changed

Lines changed: 128 additions & 6 deletions

File tree

array.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2730,6 +2730,20 @@ rb_builtin_fixnum_inc(rb_execution_context_t *ec, VALUE self, VALUE num)
27302730
return LONG2FIX(FIX2LONG(num) + 1);
27312731
}
27322732

2733+
// Return the first element of the array, or nil if empty.
2734+
VALUE
2735+
rb_builtin_ary_first(rb_execution_context_t *ec, VALUE self)
2736+
{
2737+
return ary_first(self);
2738+
}
2739+
2740+
// Return the last element of the array, or nil if empty.
2741+
VALUE
2742+
rb_builtin_ary_last(rb_execution_context_t *ec, VALUE self)
2743+
{
2744+
return ary_last(self);
2745+
}
2746+
27332747
// Push a value onto an array and return the value.
27342748
static VALUE
27352749
rb_jit_ary_push(rb_execution_context_t *ec, VALUE self, VALUE ary, VALUE val)

array.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,10 @@ def sample(n = (ary = false), random: Random)
129129
def first n = unspecified = true
130130
if Primitive.mandatory_only?
131131
Primitive.attr! :leaf
132-
Primitive.cexpr! %q{ ary_first(self) }
132+
Primitive.rb_builtin_ary_first
133133
else
134134
if unspecified
135-
Primitive.cexpr! %q{ ary_first(self) }
135+
Primitive.rb_builtin_ary_first
136136
else
137137
Primitive.cexpr! %q{ ary_take_first_or_last_n(self, NUM2LONG(n), ARY_TAKE_FIRST) }
138138
end
@@ -166,10 +166,10 @@ def first n = unspecified = true
166166
def last n = unspecified = true
167167
if Primitive.mandatory_only?
168168
Primitive.attr! :leaf
169-
Primitive.cexpr! %q{ ary_last(self) }
169+
Primitive.rb_builtin_ary_last
170170
else
171171
if unspecified
172-
Primitive.cexpr! %q{ ary_last(self) }
172+
Primitive.rb_builtin_ary_last
173173
else
174174
Primitive.cexpr! %q{ ary_take_first_or_last_n(self, NUM2LONG(n), ARY_TAKE_LAST) }
175175
end

zjit/src/cruby_methods.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ unsafe extern "C" {
1919
fn rb_builtin_ary_at_end(ec: EcPtr, self_: VALUE, index: VALUE) -> VALUE;
2020
fn rb_builtin_ary_at(ec: EcPtr, self_: VALUE, index: VALUE) -> VALUE;
2121
fn rb_builtin_fixnum_inc(ec: EcPtr, self_: VALUE, num: VALUE) -> VALUE;
22+
fn rb_builtin_ary_first(ec: EcPtr, self_: VALUE) -> VALUE;
23+
fn rb_builtin_ary_last(ec: EcPtr, self_: VALUE) -> VALUE;
2224
fn rb_str_equal(str1: VALUE, str2: VALUE) -> VALUE;
2325
}
2426

@@ -229,6 +231,8 @@ pub fn init() -> Annotations {
229231
annotate!(rb_cModule, "name", types::StringExact.union(types::NilClass), no_gc, leaf, elidable);
230232
annotate!(rb_cModule, "===", inline_module_eqq, types::BoolExact, no_gc, leaf);
231233
annotate!(rb_cArray, "length", inline_array_length, types::Fixnum, no_gc, leaf, elidable);
234+
// Array#size shares rb_ary_length with Array#length, so this maps to the same function pointer.
235+
annotate!(rb_cArray, "size", inline_array_length, types::Fixnum, no_gc, leaf, elidable);
232236
annotate!(rb_cArray, "empty?", inline_array_empty_p, types::BoolExact, no_gc, leaf, elidable);
233237
annotate!(rb_cArray, "reverse", types::ArrayExact, leaf, elidable);
234238
annotate!(rb_cArray, "join", types::StringExact);
@@ -301,6 +305,10 @@ pub fn init() -> Annotations {
301305
builtin_funcs.insert(rb_builtin_ary_at as *mut c_void, FnProperties { inline: inline_ary_at, ..Default::default() });
302306
builtin_funcs.insert(rb_builtin_ary_at_end as *mut c_void, FnProperties { inline: inline_ary_at_end, return_type: types::BoolExact, ..Default::default() });
303307

308+
// Builtins used by Array#first and Array#last when called with no arguments
309+
builtin_funcs.insert(rb_builtin_ary_first as *mut c_void, FnProperties { inline: inline_ary_first, no_gc: true, leaf: true, elidable: true, ..Default::default() });
310+
builtin_funcs.insert(rb_builtin_ary_last as *mut c_void, FnProperties { inline: inline_ary_last, no_gc: true, leaf: true, elidable: true, ..Default::default() });
311+
304312
Annotations {
305313
cfuncs: std::mem::take(cfuncs),
306314
builtin_funcs: std::mem::take(builtin_funcs),
@@ -1057,3 +1065,34 @@ fn inline_ary_at_end(fun: &mut hir::Function, block: hir::BlockId, _recv: hir::I
10571065
let result = fun.push_insn(block, hir::Insn::FixnumGe { left: index, right: length });
10581066
Some(result)
10591067
}
1068+
1069+
/// Inline `ary_first(ec, self)` from Array#first with no arguments.
1070+
/// Guards that the array is non-empty and loads the element at index 0;
1071+
/// side-exits on empty arrays and lets the interpreter return nil.
1072+
fn inline_ary_first(fun: &mut hir::Function, block: hir::BlockId, _recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> {
1073+
use crate::hir::SideExitReason;
1074+
let &[recv] = args else { return None; };
1075+
let recv = fun.push_insn(block, hir::Insn::RefineType { val: recv, new_type: types::Array });
1076+
let length = fun.push_insn(block, hir::Insn::ArrayLength { array: recv });
1077+
let zero = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(0) });
1078+
let index = fun.push_insn(block, hir::Insn::GuardLess { left: zero, right: length, reason: Box::new(SideExitReason::GuardLess), state });
1079+
let result = fun.push_insn(block, hir::Insn::ArrayAref { array: recv, index });
1080+
Some(result)
1081+
}
1082+
1083+
/// Inline `ary_last(ec, self)` from Array#last with no arguments.
1084+
/// Guards that the array is non-empty and loads the element at index length-1;
1085+
/// side-exits on empty arrays and lets the interpreter return nil.
1086+
fn inline_ary_last(fun: &mut hir::Function, block: hir::BlockId, _recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> {
1087+
use crate::hir::SideExitReason;
1088+
let &[recv] = args else { return None; };
1089+
let recv = fun.push_insn(block, hir::Insn::RefineType { val: recv, new_type: types::Array });
1090+
let length = fun.push_insn(block, hir::Insn::ArrayLength { array: recv });
1091+
let minus_one = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(-1) });
1092+
// AdjustBounds computes length - 1 since the index is negative
1093+
let index = fun.push_insn(block, hir::Insn::AdjustBounds { index: minus_one, length });
1094+
let zero = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(0) });
1095+
let index = fun.push_insn(block, hir::Insn::GuardGreaterEq { left: index, right: zero, reason: Box::new(SideExitReason::GuardGreaterEq), state });
1096+
let result = fun.push_insn(block, hir::Insn::ArrayAref { array: recv, index });
1097+
Some(result)
1098+
}

zjit/src/hir/opt_tests.rs

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3688,9 +3688,12 @@ mod hir_opt_tests {
36883688
v18:ArrayExact = ArrayDup v17
36893689
PatchPoint NoSingletonClass(Array@0x1010)
36903690
PatchPoint MethodRedefined(Array@0x1010, first@0x1018, cme:0x1020)
3691-
v31:BasicObject = InvokeBuiltin leaf <inline_expr>, v18
3691+
v32:CInt64 = ArrayLength v18
3692+
v33:CInt64[0] = Const CInt64(0)
3693+
v34:CInt64[0] = GuardLess v33, v32
3694+
v35:BasicObject = ArrayAref v18, v34
36923695
CheckInterrupts
3693-
Return v31
3696+
Return v35
36943697
");
36953698
}
36963699

@@ -11782,6 +11785,72 @@ mod hir_opt_tests {
1178211785
");
1178311786
}
1178411787

11788+
#[test]
11789+
fn test_optimize_array_first() {
11790+
eval("
11791+
def test(arr) = arr.first
11792+
test([1])
11793+
");
11794+
assert_snapshot!(hir_string("test"), @"
11795+
fn test@<compiled>:2:
11796+
bb1():
11797+
EntryPoint interpreter
11798+
v1:BasicObject = LoadSelf
11799+
v2:CPtr = LoadSP
11800+
v3:BasicObject = LoadField v2, :arr@0x1000
11801+
Jump bb3(v1, v3)
11802+
bb2():
11803+
EntryPoint JIT(0)
11804+
v6:BasicObject = LoadArg :self@0
11805+
v7:BasicObject = LoadArg :arr@1
11806+
Jump bb3(v6, v7)
11807+
bb3(v9:BasicObject, v10:BasicObject):
11808+
PatchPoint NoSingletonClass(Array@0x1008)
11809+
PatchPoint MethodRedefined(Array@0x1008, first@0x1010, cme:0x1018)
11810+
v23:ArrayExact = GuardType v10, ArrayExact recompile
11811+
v25:CInt64 = ArrayLength v23
11812+
v26:CInt64[0] = Const CInt64(0)
11813+
v27:CInt64[0] = GuardLess v26, v25
11814+
v28:BasicObject = ArrayAref v23, v27
11815+
CheckInterrupts
11816+
Return v28
11817+
");
11818+
}
11819+
11820+
#[test]
11821+
fn test_optimize_array_last() {
11822+
eval("
11823+
def test(arr) = arr.last
11824+
test([1])
11825+
");
11826+
assert_snapshot!(hir_string("test"), @"
11827+
fn test@<compiled>:2:
11828+
bb1():
11829+
EntryPoint interpreter
11830+
v1:BasicObject = LoadSelf
11831+
v2:CPtr = LoadSP
11832+
v3:BasicObject = LoadField v2, :arr@0x1000
11833+
Jump bb3(v1, v3)
11834+
bb2():
11835+
EntryPoint JIT(0)
11836+
v6:BasicObject = LoadArg :self@0
11837+
v7:BasicObject = LoadArg :arr@1
11838+
Jump bb3(v6, v7)
11839+
bb3(v9:BasicObject, v10:BasicObject):
11840+
PatchPoint NoSingletonClass(Array@0x1008)
11841+
PatchPoint MethodRedefined(Array@0x1008, last@0x1010, cme:0x1018)
11842+
v23:ArrayExact = GuardType v10, ArrayExact recompile
11843+
v25:CInt64 = ArrayLength v23
11844+
v26:CInt64[-1] = Const CInt64(-1)
11845+
v27:CInt64 = AdjustBounds v26, v25
11846+
v28:CInt64[0] = Const CInt64(0)
11847+
v29:CInt64 = GuardGreaterEq v27, v28
11848+
v30:BasicObject = ArrayAref v23, v29
11849+
CheckInterrupts
11850+
Return v30
11851+
");
11852+
}
11853+
1178511854
#[test]
1178611855
fn test_optimize_regexpmatch2() {
1178711856
eval(r#"

0 commit comments

Comments
 (0)