Skip to content

Commit 20a9fe5

Browse files
committed
ZJIT: Inline Array#first and Array#last
1 parent 9e58d04 commit 20a9fe5

4 files changed

Lines changed: 127 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: 38 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

@@ -301,6 +303,10 @@ pub fn init() -> Annotations {
301303
builtin_funcs.insert(rb_builtin_ary_at as *mut c_void, FnProperties { inline: inline_ary_at, ..Default::default() });
302304
builtin_funcs.insert(rb_builtin_ary_at_end as *mut c_void, FnProperties { inline: inline_ary_at_end, return_type: types::BoolExact, ..Default::default() });
303305

306+
// Builtins used by Array#first and Array#last when called with no arguments
307+
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() });
308+
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() });
309+
304310
Annotations {
305311
cfuncs: std::mem::take(cfuncs),
306312
builtin_funcs: std::mem::take(builtin_funcs),
@@ -1057,3 +1063,35 @@ fn inline_ary_at_end(fun: &mut hir::Function, block: hir::BlockId, _recv: hir::I
10571063
let result = fun.push_insn(block, hir::Insn::FixnumGe { left: index, right: length });
10581064
Some(result)
10591065
}
1066+
1067+
/// Inline `ary_first(ec, self)` from Array#first with no arguments.
1068+
/// Guards that the array is non-empty and loads the element at index 0;
1069+
/// side-exits on empty arrays and lets the interpreter return nil.
1070+
fn inline_ary_first(fun: &mut hir::Function, block: hir::BlockId, _recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> {
1071+
use crate::hir::SideExitReason;
1072+
let &[recv] = args else { return None; };
1073+
let recv = fun.push_insn(block, hir::Insn::RefineType { val: recv, new_type: types::Array });
1074+
let length = fun.push_insn(block, hir::Insn::ArrayLength { array: recv });
1075+
let zero = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(0) });
1076+
let index = fun.push_insn(block, hir::Insn::GuardLess { left: zero, right: length, reason: Box::new(SideExitReason::GuardLess), state });
1077+
let result = fun.push_insn(block, hir::Insn::ArrayAref { array: recv, index });
1078+
Some(result)
1079+
}
1080+
1081+
/// Inline `ary_last(ec, self)` from Array#last with no arguments.
1082+
/// Guards that the array is non-empty and loads the element at index length-1;
1083+
/// side-exits on empty arrays and lets the interpreter return nil.
1084+
fn inline_ary_last(fun: &mut hir::Function, block: hir::BlockId, _recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> {
1085+
use crate::hir::SideExitReason;
1086+
let &[recv] = args else { return None; };
1087+
let recv = fun.push_insn(block, hir::Insn::RefineType { val: recv, new_type: types::Array });
1088+
let length = fun.push_insn(block, hir::Insn::ArrayLength { array: recv });
1089+
let minus_one = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(-1) });
1090+
// AdjustBounds computes length - 1 since the index is negative
1091+
// TODO: Consider doing something like IntSub instead
1092+
let index = fun.push_insn(block, hir::Insn::AdjustBounds { index: minus_one, length });
1093+
let zero = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(0) });
1094+
let index = fun.push_insn(block, hir::Insn::GuardGreaterEq { left: index, right: zero, reason: Box::new(SideExitReason::GuardGreaterEq), state });
1095+
let result = fun.push_insn(block, hir::Insn::ArrayAref { array: recv, index });
1096+
Some(result)
1097+
}

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)