Skip to content

Commit 86b1d2d

Browse files
allow jits to understand narrowed arrays
1 parent 509e3c7 commit 86b1d2d

10 files changed

Lines changed: 96 additions & 103 deletions

File tree

‎array.c‎

Lines changed: 27 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,7 @@ rb_ary_embeddable_p(VALUE ary)
236236
* flag.
237237
* - Shared: we don't want to re-embed an array that points to a shared
238238
* root (to save memory).
239-
* - Narrowed: the buffer is not VALUEs, so it cannot be copied into the
240-
* slot as-is, and GC compaction (the caller) cannot widen because it
241-
* must not allocate.
239+
* - Narrowed: the buffer is not VALUEs, and compaction cannot widen.
242240
*/
243241
if (ARY_SHARED_ROOT_P(ary) || OBJ_FROZEN(ary) || ARY_SHARED_P(ary) ||
244242
rb_array_stride(ary) != RARRAY_STRIDE_VALUE) return false;
@@ -300,8 +298,6 @@ ary_verify_(VALUE ary, const char *file, int line)
300298
RUBY_ASSERT(ARY_CAPA(ary) >= RARRAY_LEN(ary));
301299
}
302300
if (RARRAY_LEN(ary) > 0) {
303-
/* RARRAY_AREF is narrow-aware and does not widen, so an assertion
304-
* cannot change the array's representation. */
305301
volatile VALUE v = RARRAY_AREF(ary, 0); /* access check: HEAD only */
306302
v = v;
307303
}
@@ -345,10 +341,8 @@ rb_ary_widen(VALUE ary)
345341
{
346342
if (rb_array_stride(ary) == RARRAY_STRIDE_VALUE) return;
347343

348-
/* A frozen narrow array may be Ractor-shareable, so two Ractors can reach
349-
* this point in parallel through RARRAY_CONST_PTR. Widening swaps and
350-
* frees the buffer, which must happen exactly once. Take the VM lock and
351-
* re-check: whoever loses the race finds the array already widened. */
344+
/* A frozen narrow array may be shareable, so two Ractors can widen it
345+
* concurrently. Lock and re-check so the buffer swap happens once. */
352346
RB_VM_LOCKING() {
353347
if (rb_array_stride(ary) != RARRAY_STRIDE_VALUE) {
354348
ary_widen_locked(ary);
@@ -425,10 +419,8 @@ rb_ary_narrow(VALUE ary)
425419

426420
/* ---- narrow fast paths ------------------------------------------------- */
427421

428-
/* Run BODY once with `const T *p` bound to a narrowed array's packed buffer,
429-
* where T is the element's C type. This hoists the width/sign dispatch out
430-
* of the caller's loop: inside BODY, p[i] is a plain typed load the compiler
431-
* can vectorize, instead of a per-element RARRAY_AREF decode. */
422+
/* Run BODY with `const T *p` bound to the packed buffer, so the width/sign
423+
* dispatch happens once instead of per element. */
432424
#define ARY_NARROW_DISPATCH(ary, T, p, BODY) do { \
433425
const void *narrow_raw_ = rarray_raw_ptr(ary); \
434426
if (rb_array_stride_unsigned_p(ary)) { \
@@ -447,10 +439,8 @@ rb_ary_narrow(VALUE ary)
447439
} \
448440
} while (0)
449441

450-
/* Index of the first (or last) element equal to needle, or -1. Equality on
451-
* unboxed integers is a plain compare, so this is only valid when Integer#==
452-
* is not redefined; callers check. A needle the element type cannot
453-
* represent cannot be present, which the round trip through T detects. */
442+
/* Index of the first (or last) element equal to needle, or -1. Valid only
443+
* when Integer#== is not redefined; callers check. */
454444
static long
455445
ary_narrow_find(VALUE ary, long needle, bool from_end)
456446
{
@@ -484,8 +474,7 @@ ary_narrow_count(VALUE ary, long needle)
484474
return count;
485475
}
486476

487-
/* Sum of a narrowed array as a long. Elements are at most 32 bits, so the
488-
* accumulator cannot overflow for fewer than 2**31 elements; callers check. */
477+
/* Sum as a long. Cannot overflow below 2**31 elements; callers check. */
489478
static long
490479
ary_narrow_sum(VALUE ary)
491480
{
@@ -497,8 +486,8 @@ ary_narrow_sum(VALUE ary)
497486
return acc;
498487
}
499488

500-
/* Minimum (or maximum) of a non-empty narrowed array. Only valid when
501-
* Integer#<=> is not redefined; callers check. */
489+
/* Min (or max) of a non-empty narrowed array. Valid only when Integer#<=>
490+
* is not redefined; callers check. */
502491
static long
503492
ary_narrow_minmax(VALUE ary, bool want_max)
504493
{
@@ -514,10 +503,8 @@ ary_narrow_minmax(VALUE ary, bool want_max)
514503
return best;
515504
}
516505

517-
/* Decode a narrowed array's elements into dst[0..len) as VALUEs, in order or
518-
* reversed. One dispatch, then a tight typed loop: ~8x faster than
519-
* RARRAY_AREF per element. Elements are immediates, so dst needs no write
520-
* barrier. */
506+
/* Decode into dst[0..len) as VALUEs, in order or reversed. Elements are
507+
* immediates, so no write barrier is needed. */
521508
static void
522509
ary_narrow_decode(VALUE src, VALUE *dst, long len, bool reversed)
523510
{
@@ -1198,11 +1185,8 @@ ary_make_shared(VALUE ary)
11981185
{
11991186
ary_verify(ary);
12001187

1201-
/* A shared root's buffer is read as VALUEs by every array that borrows
1202-
* it, and borrowers copy the raw pointer. A narrow array must therefore
1203-
* never become a root; widen before handing the buffer out. This is the
1204-
* frozen case in practice: narrowing happens on freeze, and a frozen
1205-
* array is returned below as its own root. */
1188+
/* Borrowers read a shared root's buffer as VALUEs, so a narrow array
1189+
* must be widened before it can become one. */
12061190
if (RB_UNLIKELY(rb_array_stride(ary) != RARRAY_STRIDE_VALUE)) {
12071191
rb_ary_widen(ary);
12081192
}
@@ -5724,9 +5708,8 @@ rb_ary_rassoc(VALUE ary, VALUE value)
57245708
return Qnil;
57255709
}
57265710

5727-
/* Two wide heap arrays that share one buffer are trivially equal. A narrow
5728-
* array has no VALUE buffer to compare, and asking for one would widen it;
5729-
* narrow arrays never share a buffer, so the shortcut simply does not apply. */
5711+
/* Two wide heap arrays sharing one buffer are trivially equal. Narrow arrays
5712+
* never share a buffer, so the shortcut does not apply to them. */
57305713
static inline bool
57315714
ary_same_buffer_p(VALUE ary1, VALUE ary2)
57325715
{
@@ -5735,9 +5718,8 @@ ary_same_buffer_p(VALUE ary1, VALUE ary2)
57355718
RARRAY_CONST_PTR(ary1) == RARRAY_CONST_PTR(ary2);
57365719
}
57375720

5738-
/* Element-wise == from index i, re-reading both arrays every iteration.
5739-
* rb_equal() may run Ruby code that mutates, widens or shrinks either array,
5740-
* so nothing is cached across it. Lengths were equal on entry. */
5721+
/* Element-wise == from index i. rb_equal() may mutate, widen or shrink
5722+
* either array, so nothing is cached across it. */
57415723
static VALUE
57425724
ary_equal_slow_from(VALUE ary1, VALUE ary2, long i, bool eql)
57435725
{
@@ -5754,11 +5736,9 @@ ary_equal_slow_from(VALUE ary1, VALUE ary2, long i, bool eql)
57545736
return Qtrue;
57555737
}
57565738

5757-
/* == where at least one side is narrow. Two equal-width narrow arrays are a
5758-
* memcmp. Otherwise the dispatch is hoisted onto the narrow side and the
5759-
* other side is read through RARRAY_AREF; a mismatch against a Fixnum is a
5760-
* definite inequality (Integer#== not redefined), anything else drops to the
5761-
* slow path, which is safe against rb_equal() mutating the arrays. */
5739+
/* == where at least one side is narrow. Equal-width narrow arrays are a
5740+
* memcmp; otherwise compare element-wise, falling back to the slow path for
5741+
* anything that is not a Fixnum mismatch. */
57625742
static VALUE
57635743
ary_narrow_equal(VALUE ary1, VALUE ary2, bool eql)
57645744
{
@@ -5772,8 +5752,7 @@ ary_narrow_equal(VALUE ary1, VALUE ary2, bool eql)
57725752
return RBOOL(memcmp(rarray_raw_ptr(ary1), rarray_raw_ptr(ary2), (size_t)len * ary_stride_bytes(ary1)) == 0);
57735753
}
57745754

5775-
/* Two Fixnums that are not identical are never == (unless Integer#== is
5776-
* redefined) and never eql?. Anything else goes to the slow path. */
5755+
/* Distinct Fixnums are never == (Integer#== not redefined) nor eql?. */
57775756
const bool fix_mismatch_decides = eql || BASIC_OP_UNREDEFINED_P(BOP_EQ, INTEGER_REDEFINED_OP_FLAG);
57785757
long i = 0;
57795758
if (n1 && n2) {
@@ -5825,8 +5804,7 @@ recursive_equal(VALUE ary1, VALUE ary2, int recur)
58255804
return ary_narrow_equal(ary1, ary2, false);
58265805
}
58275806

5828-
/* Both wide: the original cached-pointer loop. rb_equal() can evacuate
5829-
* ptrs, hence the re-fetch after each call that returns true. */
5807+
/* Both wide: rb_equal() can evacuate ptrs, hence the re-fetch. */
58305808
p1 = RARRAY_CONST_PTR(ary1);
58315809
p2 = RARRAY_CONST_PTR(ary2);
58325810
len1 = RARRAY_LEN(ary1);
@@ -5984,10 +5962,8 @@ rb_ary_hash(VALUE ary)
59845962
{
59855963
RBIMPL_ASSERT_OR_ASSUME(ary);
59865964
if (RB_UNLIKELY(rb_array_stride(ary) != RARRAY_STRIDE_VALUE)) {
5987-
/* Same combine sequence as ary_hash_values, over decoded elements, so
5988-
* a narrowed array hashes identically to its wide equivalent (they
5989-
* are ==, so they must). Elements are immediates: rb_hash cannot
5990-
* re-enter and change the array, so no length re-check is needed. */
5965+
/* Same combine sequence as ary_hash_values, so a narrowed array
5966+
* hashes identically to its wide equivalent. */
59915967
const long len = RARRAY_LEN(ary);
59925968
st_index_t h = rb_hash_start(len);
59935969
h = rb_hash_uint(h, (st_index_t)rb_ary_hash_values);
@@ -6025,8 +6001,6 @@ rb_ary_includes(VALUE ary, VALUE item)
60256001
return RBOOL(ary_narrow_find(ary, FIX2LONG(item), false) >= 0);
60266002
}
60276003

6028-
/* RARRAY_AREF is narrow-aware inside core, so this loop reads a narrowed
6029-
* array in place without widening it. */
60306004
for (i=0; i<RARRAY_LEN(ary); i++) {
60316005
e = RARRAY_AREF(ary, i);
60326006
if (rb_equal(e, item)) {
@@ -6036,8 +6010,7 @@ rb_ary_includes(VALUE ary, VALUE item)
60366010
return Qfalse;
60376011
}
60386012

6039-
/* Test hooks. Not public API: they exist so the stride machinery can be
6040-
* driven directly from the test suite before any automatic policy exists. */
6013+
/* Test hooks for the stride machinery. Not public API. */
60416014
static VALUE
60426015
rb_ary_narrow_bang(VALUE ary)
60436016
{
@@ -8786,9 +8759,7 @@ rb_ary_sum(int argc, VALUE *argv, VALUE ary)
87868759
n = 0;
87878760
r = Qundef;
87888761

8789-
/* Narrow fast path: unboxed elements are summed as plain longs. Mirrors
8790-
* the Fixnum loop below (which also adds natively); elements are <= 32
8791-
* bits, so the accumulator is safe below 2**31 elements. */
8762+
/* Narrow fast path: sum unboxed elements as longs (see ary_narrow_sum). */
87928763
if (!block_given && FIXNUM_P(v) &&
87938764
RB_UNLIKELY(rb_array_stride(ary) != RARRAY_STRIDE_VALUE) &&
87948765
RARRAY_LEN(ary) < (1L << 30)) {

‎include/ruby/internal/core/rarray.h‎

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -114,22 +114,19 @@ enum ruby_rarray_flags {
114114
RUBY_FL_USER5 | RUBY_FL_USER4 | RUBY_FL_USER3,
115115

116116
/**
117-
* Width of one element in the backend storage. Zero means the storage is
118-
* an array of ::VALUE, which is the only representation 3rd parties can
119-
* see. A non-zero value means the elements are stored unboxed, narrower
120-
* than a ::VALUE, and the array must be widened before its buffer is
121-
* handed out. See ::rb_ary_widen.
117+
* Width of one element in the backend storage. Zero means an array of
118+
* ::VALUE. Non-zero means elements are stored unboxed and narrower than
119+
* a ::VALUE; the array is widened before its buffer is handed out. See
120+
* ::rb_ary_widen.
122121
*
123122
* @internal
124123
*
125-
* 3rd parties must not be aware that there even is more than one way to
126-
* store array elements. It was a bad idea to expose this to them.
124+
* 3rd parties must not be aware of this.
127125
*/
128126
RARRAY_STRIDE_MASK = RUBY_FL_USER11 | RUBY_FL_USER10,
129127

130128
/**
131-
* Set when a narrowed array's unboxed elements are to be read as unsigned.
132-
* Doubles the non-negative range a given width can hold.
129+
* Set when a narrowed array's unboxed elements are read as unsigned.
133130
*
134131
* @internal
135132
*
@@ -361,8 +358,7 @@ rb_array_stride(VALUE a)
361358
* This is an implementation detail of RARRAY_PTR(). People do not use it
362359
* directly.
363360
*
364-
* This is deliberately not pure: a narrowed array is widened in place before
365-
* its buffer is returned, because every caller expects ::VALUE elements.
361+
* Not pure: a narrowed array is widened in place before its buffer is returned.
366362
*
367363
* @param[in] a An object of ::RArray.
368364
* @return Its backend storage.

‎internal/array.h‎

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@
2323
#define RARRAY_PTR_IN_USE_FLAG FL_USER14
2424
#define RARRAY_FAKEARY FL_USER19
2525

26-
/* Address of a narrowed array's packed buffer. Only the narrow decode path
27-
* may use this; everything else goes through RARRAY_CONST_PTR, which widens.
28-
* Narrow arrays always own a heap buffer: embedded arrays are never narrowed. */
26+
/* Packed buffer of a narrowed array. Only the narrow decode path may use
27+
* this; everything else goes through RARRAY_CONST_PTR, which widens. */
2928
static inline const void *
3029
rarray_raw_ptr(VALUE ary)
3130
{
@@ -34,10 +33,8 @@ rarray_raw_ptr(VALUE ary)
3433
return (const void *)RARRAY(ary)->as.heap.ptr;
3534
}
3635

37-
/* Load element i of a narrowed array as a C integer. This is the single
38-
* place that knows the width/sign encoding. It serves any strategy whose
39-
* unboxed payload is an integer; a strategy with a different payload type
40-
* would add a sibling loader. */
36+
/* Load element i of a narrowed array as a C integer. The single place that
37+
* knows the width/sign encoding. */
4138
static inline long
4239
rarray_narrow_load_int(VALUE ary, long i)
4340
{
@@ -56,23 +53,16 @@ rarray_narrow_load_int(VALUE ary, long i)
5653
}
5754
}
5855

59-
/* Decode element i of a narrowed array into a ::VALUE. This is where the
60-
* per-strategy knowledge lives: how to rebuild a VALUE from the payload. */
56+
/* Decode element i of a narrowed array into a ::VALUE. */
6157
static inline VALUE
6258
rarray_narrow_aref(VALUE ary, long i)
6359
{
6460
return LONG2FIX(rarray_narrow_load_int(ary, i));
6561
}
6662

67-
/* Core's RARRAY_AREF replaces the public macro (which must stay an lvalue-
68-
* capable macro for extensions). It adds a bounds assertion, and it is
69-
* narrow-aware: an element of a narrowed array is decoded in place rather
70-
* than widening the whole array, so read-only iteration (each, map, sum,
71-
* index, ==, ...) leaves a narrowed array narrow. Never allocates, so it is
72-
* also safe where RARRAY_CONST_PTR is not (opt_aref, GC callbacks).
73-
*
74-
* Not PURE: it is still a pure function of (ary, i) in the narrow case, but
75-
* the wide case reads through RARRAY_CONST_PTR, which is not pure either. */
63+
/* Core's RARRAY_AREF replaces the public macro. It adds a bounds assertion
64+
* and decodes narrowed elements in place instead of widening the array, so
65+
* read-only paths leave a narrowed array narrow. Never allocates. */
7666
#undef RARRAY_AREF
7767
RBIMPL_ATTR_ARTIFICIAL()
7868
static inline VALUE
@@ -107,10 +97,8 @@ size_t rb_ary_memsize(VALUE);
10797
VALUE rb_to_array_type(VALUE obj);
10898
VALUE rb_to_array(VALUE obj);
10999
void rb_ary_cancel_sharing(VALUE ary);
110-
/* Pack a heap array whose elements can all be stored unboxed to the narrowest
111-
* element width that holds every value. Returns the resulting stride;
112-
* RARRAY_STRIDE_VALUE means the array was left as-is. Used by the freeze
113-
* trigger and by the compiler for frozen array literals. */
100+
/* Pack an all-Fixnum heap array to the narrowest width that holds every
101+
* value. Returns the resulting stride; RARRAY_STRIDE_VALUE means unchanged. */
114102
int rb_ary_narrow(VALUE ary);
115103
size_t rb_ary_size_as_embedded(VALUE ary);
116104
void rb_ary_make_embedded(VALUE ary);

‎test/ruby/test_array_stride.rb‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,11 @@ def test_duparray_literal_table_narrows_and_frees_template
158158
assert_equal W16, stride(t)
159159
assert_operator ObjectSpace.memsize_of(t), :<=, n * 2 + 128
160160
assert_equal src, t.map { |x| x }
161-
refute template_alive.call, "template must be freed once the borrower owns its own buffer"
161+
# A JIT may embed the literal's template in compiled code as a constant,
162+
# which legitimately keeps it alive; only the interpreter can prove it dead.
163+
unless defined?(RubyVM::ZJIT.enabled?) && RubyVM::ZJIT.enabled?
164+
refute template_alive.call, "template must be freed once the borrower owns its own buffer"
165+
end
162166
end
163167

164168
# Census: homogeneous arrays >= 1024 elements are 93.6% (Core) / 99.3%

‎vm_args.c‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,8 @@ arg_rest_dup(struct args_info *args)
4545
}
4646
}
4747

48-
/* The setup code below reads the splat array through RARRAY_CONST_PTR, which
49-
* would widen a narrowed array in place. Take a private (wide) copy instead,
50-
* so f(*TABLE) leaves TABLE packed. rb_ary_dup decodes element-wise. */
48+
/* Argument setup reads the splat array through RARRAY_CONST_PTR, which would
49+
* widen it. Take a wide copy instead so f(*TABLE) leaves TABLE packed. */
5150
static inline void
5251
arg_rest_unnarrow(struct args_info *args)
5352
{

0 commit comments

Comments
 (0)