Skip to content

Commit 1c07a28

Browse files
allow jits to understand narrowed arrays
1 parent 07dcbf2 commit 1c07a28

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
@@ -237,9 +237,7 @@ rb_ary_embeddable_p(VALUE ary)
237237
* flag.
238238
* - Shared: we don't want to re-embed an array that points to a shared
239239
* root (to save memory).
240-
* - Narrowed: the buffer is not VALUEs, so it cannot be copied into the
241-
* slot as-is, and GC compaction (the caller) cannot widen because it
242-
* must not allocate.
240+
* - Narrowed: the buffer is not VALUEs, and compaction cannot widen.
243241
*/
244242
if (ARY_SHARED_ROOT_P(ary) || OBJ_FROZEN(ary) || ARY_SHARED_P(ary) ||
245243
rb_array_stride(ary) != RARRAY_STRIDE_VALUE) return false;
@@ -302,8 +300,6 @@ ary_verify_(VALUE ary, const char *file, int line)
302300
RUBY_ASSERT(ARY_CAPA(ary) >= RARRAY_LEN(ary));
303301
}
304302
if (RARRAY_LEN(ary) > 0) {
305-
/* RARRAY_AREF is narrow-aware and does not widen, so an assertion
306-
* cannot change the array's representation. */
307303
volatile VALUE v = RARRAY_AREF(ary, 0); /* access check: HEAD only */
308304
v = v;
309305
}
@@ -347,10 +343,8 @@ rb_ary_widen(VALUE ary)
347343
{
348344
if (rb_array_stride(ary) == RARRAY_STRIDE_VALUE) return;
349345

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

428422
/* ---- narrow fast paths ------------------------------------------------- */
429423

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

452-
/* Index of the first (or last) element equal to needle, or -1. Equality on
453-
* unboxed integers is a plain compare, so this is only valid when Integer#==
454-
* is not redefined; callers check. A needle the element type cannot
455-
* represent cannot be present, which the round trip through T detects. */
444+
/* Index of the first (or last) element equal to needle, or -1. Valid only
445+
* when Integer#== is not redefined; callers check. */
456446
static long
457447
ary_narrow_find(VALUE ary, long needle, bool from_end)
458448
{
@@ -486,8 +476,7 @@ ary_narrow_count(VALUE ary, long needle)
486476
return count;
487477
}
488478

489-
/* Sum of a narrowed array as a long. Elements are at most 32 bits, so the
490-
* accumulator cannot overflow for fewer than 2**31 elements; callers check. */
479+
/* Sum as a long. Cannot overflow below 2**31 elements; callers check. */
491480
static long
492481
ary_narrow_sum(VALUE ary)
493482
{
@@ -499,8 +488,8 @@ ary_narrow_sum(VALUE ary)
499488
return acc;
500489
}
501490

502-
/* Minimum (or maximum) of a non-empty narrowed array. Only valid when
503-
* Integer#<=> is not redefined; callers check. */
491+
/* Min (or max) of a non-empty narrowed array. Valid only when Integer#<=>
492+
* is not redefined; callers check. */
504493
static long
505494
ary_narrow_minmax(VALUE ary, bool want_max)
506495
{
@@ -516,10 +505,8 @@ ary_narrow_minmax(VALUE ary, bool want_max)
516505
return best;
517506
}
518507

519-
/* Decode a narrowed array's elements into dst[0..len) as VALUEs, in order or
520-
* reversed. One dispatch, then a tight typed loop: ~8x faster than
521-
* RARRAY_AREF per element. Elements are immediates, so dst needs no write
522-
* barrier. */
508+
/* Decode into dst[0..len) as VALUEs, in order or reversed. Elements are
509+
* immediates, so no write barrier is needed. */
523510
static void
524511
ary_narrow_decode(VALUE src, VALUE *dst, long len, bool reversed)
525512
{
@@ -1200,11 +1187,8 @@ ary_make_shared(VALUE ary)
12001187
{
12011188
ary_verify(ary);
12021189

1203-
/* A shared root's buffer is read as VALUEs by every array that borrows
1204-
* it, and borrowers copy the raw pointer. A narrow array must therefore
1205-
* never become a root; widen before handing the buffer out. This is the
1206-
* frozen case in practice: narrowing happens on freeze, and a frozen
1207-
* array is returned below as its own root. */
1190+
/* Borrowers read a shared root's buffer as VALUEs, so a narrow array
1191+
* must be widened before it can become one. */
12081192
if (RB_UNLIKELY(rb_array_stride(ary) != RARRAY_STRIDE_VALUE)) {
12091193
rb_ary_widen(ary);
12101194
}
@@ -5732,9 +5716,8 @@ rb_ary_rassoc(VALUE ary, VALUE value)
57325716
return Qnil;
57335717
}
57345718

5735-
/* Two wide heap arrays that share one buffer are trivially equal. A narrow
5736-
* array has no VALUE buffer to compare, and asking for one would widen it;
5737-
* narrow arrays never share a buffer, so the shortcut simply does not apply. */
5719+
/* Two wide heap arrays sharing one buffer are trivially equal. Narrow arrays
5720+
* never share a buffer, so the shortcut does not apply to them. */
57385721
static inline bool
57395722
ary_same_buffer_p(VALUE ary1, VALUE ary2)
57405723
{
@@ -5743,9 +5726,8 @@ ary_same_buffer_p(VALUE ary1, VALUE ary2)
57435726
RARRAY_CONST_PTR(ary1) == RARRAY_CONST_PTR(ary2);
57445727
}
57455728

5746-
/* Element-wise == from index i, re-reading both arrays every iteration.
5747-
* rb_equal() may run Ruby code that mutates, widens or shrinks either array,
5748-
* so nothing is cached across it. Lengths were equal on entry. */
5729+
/* Element-wise == from index i. rb_equal() may mutate, widen or shrink
5730+
* either array, so nothing is cached across it. */
57495731
static VALUE
57505732
ary_equal_slow_from(VALUE ary1, VALUE ary2, long i, bool eql)
57515733
{
@@ -5762,11 +5744,9 @@ ary_equal_slow_from(VALUE ary1, VALUE ary2, long i, bool eql)
57625744
return Qtrue;
57635745
}
57645746

5765-
/* == where at least one side is narrow. Two equal-width narrow arrays are a
5766-
* memcmp. Otherwise the dispatch is hoisted onto the narrow side and the
5767-
* other side is read through RARRAY_AREF; a mismatch against a Fixnum is a
5768-
* definite inequality (Integer#== not redefined), anything else drops to the
5769-
* slow path, which is safe against rb_equal() mutating the arrays. */
5747+
/* == where at least one side is narrow. Equal-width narrow arrays are a
5748+
* memcmp; otherwise compare element-wise, falling back to the slow path for
5749+
* anything that is not a Fixnum mismatch. */
57705750
static VALUE
57715751
ary_narrow_equal(VALUE ary1, VALUE ary2, bool eql)
57725752
{
@@ -5780,8 +5760,7 @@ ary_narrow_equal(VALUE ary1, VALUE ary2, bool eql)
57805760
return RBOOL(memcmp(rarray_raw_ptr(ary1), rarray_raw_ptr(ary2), (size_t)len * ary_stride_bytes(ary1)) == 0);
57815761
}
57825762

5783-
/* Two Fixnums that are not identical are never == (unless Integer#== is
5784-
* redefined) and never eql?. Anything else goes to the slow path. */
5763+
/* Distinct Fixnums are never == (Integer#== not redefined) nor eql?. */
57855764
const bool fix_mismatch_decides = eql || BASIC_OP_UNREDEFINED_P(BOP_EQ, INTEGER_REDEFINED_OP_FLAG);
57865765
long i = 0;
57875766
if (n1 && n2) {
@@ -5833,8 +5812,7 @@ recursive_equal(VALUE ary1, VALUE ary2, int recur)
58335812
return ary_narrow_equal(ary1, ary2, false);
58345813
}
58355814

5836-
/* Both wide: the original cached-pointer loop. rb_equal() can evacuate
5837-
* ptrs, hence the re-fetch after each call that returns true. */
5815+
/* Both wide: rb_equal() can evacuate ptrs, hence the re-fetch. */
58385816
p1 = RARRAY_CONST_PTR(ary1);
58395817
p2 = RARRAY_CONST_PTR(ary2);
58405818
len1 = RARRAY_LEN(ary1);
@@ -5993,10 +5971,8 @@ rb_ary_hash(VALUE ary)
59935971
{
59945972
RBIMPL_ASSERT_OR_ASSUME(ary);
59955973
if (RB_UNLIKELY(rb_array_stride(ary) != RARRAY_STRIDE_VALUE)) {
5996-
/* Same combine sequence as ary_hash_values, over decoded elements, so
5997-
* a narrowed array hashes identically to its wide equivalent (they
5998-
* are ==, so they must). Elements are immediates: rb_hash cannot
5999-
* re-enter and change the array, so no length re-check is needed. */
5974+
/* Same combine sequence as ary_hash_values, so a narrowed array
5975+
* hashes identically to its wide equivalent. */
60005976
const long len = RARRAY_LEN(ary);
60015977
st_index_t h = rb_hash_start(len);
60025978
h = rb_hash_uint(h, (st_index_t)rb_ary_hash_values);
@@ -6034,8 +6010,6 @@ rb_ary_includes(VALUE ary, VALUE item)
60346010
return RBOOL(ary_narrow_find(ary, FIX2LONG(item), false) >= 0);
60356011
}
60366012

6037-
/* RARRAY_AREF is narrow-aware inside core, so this loop reads a narrowed
6038-
* array in place without widening it. */
60396013
for (i=0; i<RARRAY_LEN(ary); i++) {
60406014
e = RARRAY_AREF(ary, i);
60416015
if (rb_equal(e, item)) {
@@ -6045,8 +6019,7 @@ rb_ary_includes(VALUE ary, VALUE item)
60456019
return Qfalse;
60466020
}
60476021

6048-
/* Test hooks. Not public API: they exist so the stride machinery can be
6049-
* driven directly from the test suite before any automatic policy exists. */
6022+
/* Test hooks for the stride machinery. Not public API. */
60506023
static VALUE
60516024
rb_ary_narrow_bang(VALUE ary)
60526025
{
@@ -8819,9 +8792,7 @@ rb_ary_sum(int argc, VALUE *argv, VALUE ary)
88198792
n = 0;
88208793
r = Qundef;
88218794

8822-
/* Narrow fast path: unboxed elements are summed as plain longs. Mirrors
8823-
* the Fixnum loop below (which also adds natively); elements are <= 32
8824-
* bits, so the accumulator is safe below 2**31 elements. */
8795+
/* Narrow fast path: sum unboxed elements as longs (see ary_narrow_sum). */
88258796
if (!block_given && FIXNUM_P(v) &&
88268797
RB_UNLIKELY(rb_array_stride(ary) != RARRAY_STRIDE_VALUE) &&
88278798
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)