Skip to content

Commit 509e3c7

Browse files
add array element stride for frozen heap arrays
1 parent 8fdf434 commit 509e3c7

10 files changed

Lines changed: 1598 additions & 41 deletions

File tree

‎array.c‎

Lines changed: 453 additions & 17 deletions
Large diffs are not rendered by default.

‎compile.c‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3676,6 +3676,7 @@ iseq_peephole_optimize(rb_iseq_t *iseq, LINK_ELEMENT *list, const int do_tailcal
36763676
if (vm_ci_simple(ci) && vm_ci_argc(ci) == 0 && blockiseq == NULL && vm_ci_mid(ci) == idFreeze) {
36773677
VALUE ary = iobj->operands[0];
36783678
rb_obj_reveal(ary, rb_cArray);
3679+
rb_ary_narrow(ary);
36793680

36803681
insn_replace_with_operands(iseq, iobj, BIN(opt_ary_freeze), 2, ary, (VALUE)ci);
36813682
ELEM_REMOVE(next);

‎gc.c‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3677,6 +3677,11 @@ rb_gc_mark_children(void *objspace, VALUE obj)
36773677
gc_mark_and_pin_internal(root);
36783678
}
36793679
}
3680+
else if (rb_array_stride(obj) != RARRAY_STRIDE_VALUE) {
3681+
/* narrow storage holds unboxed values, never object references, so
3682+
* there is nothing to trace; widening here is impossible anyway
3683+
* because it allocates */
3684+
}
36803685
else {
36813686
long len = RARRAY_LEN(obj);
36823687
const VALUE *ptr = RARRAY_CONST_PTR(obj);
@@ -4620,7 +4625,8 @@ gc_ref_update_array(void *objspace, VALUE v)
46204625
else {
46214626
long len = RARRAY_LEN(v);
46224627

4623-
if (len > 0) {
4628+
/* narrow storage holds no object references: nothing to re-point */
4629+
if (len > 0 && rb_array_stride(v) == RARRAY_STRIDE_VALUE) {
46244630
VALUE *ptr = (VALUE *)RARRAY_CONST_PTR(v);
46254631
for (long i = 0; i < len; i++) {
46264632
UPDATE_IF_MOVED(objspace, ptr[i]);

‎hash.c‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,21 @@ rb_objid_hash(st_index_t index)
321321
return (long)st_index_hash(index);
322322
}
323323

324+
/* The value NUM2LONG(rb_hash(fixnum)) would produce, without the type switch
325+
* and the Fixnum round trip. For code that hashes many Fixnums in a row. */
326+
long
327+
rb_fixnum_hash(VALUE fixnum)
328+
{
329+
RUBY_ASSERT(FIXNUM_P(fixnum));
330+
/* same clamping as the tail of any_hash() */
331+
st_index_t hnum = rb_objid_hash((st_index_t)fixnum);
332+
if ((SIGNED_VALUE)hnum > 0)
333+
hnum &= FIXNUM_MAX;
334+
else
335+
hnum |= FIXNUM_MIN;
336+
return (long)hnum;
337+
}
338+
324339
static st_index_t
325340
objid_hash(VALUE obj)
326341
{

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

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,31 @@ enum ruby_rarray_flags {
111111
* store array elements. It was a bad idea to expose this to them.
112112
*/
113113
RARRAY_EMBED_LEN_MASK = RUBY_FL_USER9 | RUBY_FL_USER8 | RUBY_FL_USER7 | RUBY_FL_USER6 |
114-
RUBY_FL_USER5 | RUBY_FL_USER4 | RUBY_FL_USER3
114+
RUBY_FL_USER5 | RUBY_FL_USER4 | RUBY_FL_USER3,
115+
116+
/**
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.
122+
*
123+
* @internal
124+
*
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.
127+
*/
128+
RARRAY_STRIDE_MASK = RUBY_FL_USER11 | RUBY_FL_USER10,
129+
130+
/**
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.
133+
*
134+
* @internal
135+
*
136+
* 3rd parties must not be aware of this.
137+
*/
138+
RARRAY_STRIDE_UNSIGNED = RUBY_FL_USER2
115139
};
116140

117141
/**
@@ -120,9 +144,43 @@ enum ruby_rarray_flags {
120144
*/
121145
enum ruby_rarray_consts {
122146
/** Where ::RARRAY_EMBED_LEN_MASK resides. */
123-
RARRAY_EMBED_LEN_SHIFT = RUBY_FL_USHIFT + 3
147+
RARRAY_EMBED_LEN_SHIFT = RUBY_FL_USHIFT + 3,
148+
149+
/** Where ::RARRAY_STRIDE_MASK resides. */
150+
RARRAY_STRIDE_SHIFT = RUBY_FL_USHIFT + 10
151+
};
152+
153+
/**
154+
* Element widths an array can use, in bits. ::RARRAY_STRIDE_VALUE is the
155+
* default: elements are ::VALUE.
156+
*/
157+
enum ruby_rarray_stride {
158+
RARRAY_STRIDE_VALUE = 0,
159+
RARRAY_STRIDE_W8 = 1,
160+
RARRAY_STRIDE_W16 = 2,
161+
RARRAY_STRIDE_W32 = 3
124162
};
125163

164+
/**
165+
* @private
166+
*
167+
* True when a narrowed array's elements are unsigned.
168+
*/
169+
static inline int
170+
rb_array_stride_unsigned_p(VALUE a)
171+
{
172+
return RB_FL_ANY_RAW(a, RARRAY_STRIDE_UNSIGNED) ? 1 : 0;
173+
}
174+
175+
/**
176+
* Converts a narrowed array back to ::VALUE storage, in place. A no-op for an
177+
* array that already uses ::RARRAY_STRIDE_VALUE. Allocates, so it must not be
178+
* called while allocation is forbidden, for instance during GC marking.
179+
*
180+
* @param[out] ary An object of ::RArray.
181+
*/
182+
void rb_ary_widen(VALUE ary);
183+
126184
/** Ruby's array. */
127185
struct RArray {
128186

@@ -282,13 +340,30 @@ RARRAY_LENINT(VALUE ary)
282340
return rb_long2int(RARRAY_LEN(ary));
283341
}
284342

285-
RBIMPL_ATTR_PURE_UNLESS_DEBUG()
343+
/**
344+
* @private
345+
*
346+
* The element width currently used by the backend storage.
347+
*
348+
* @param[in] a An object of ::RArray.
349+
* @return One of ::ruby_rarray_stride.
350+
*/
351+
static inline int
352+
rb_array_stride(VALUE a)
353+
{
354+
RBIMPL_ASSERT_TYPE(a, RUBY_T_ARRAY);
355+
return (int)((RBASIC(a)->flags & RARRAY_STRIDE_MASK) >> RARRAY_STRIDE_SHIFT);
356+
}
357+
286358
/**
287359
* @private
288360
*
289361
* This is an implementation detail of RARRAY_PTR(). People do not use it
290362
* directly.
291363
*
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.
366+
*
292367
* @param[in] a An object of ::RArray.
293368
* @return Its backend storage.
294369
*/
@@ -297,6 +372,10 @@ rb_array_const_ptr(VALUE a)
297372
{
298373
RBIMPL_ASSERT_TYPE(a, RUBY_T_ARRAY);
299374

375+
if (RB_UNLIKELY(rb_array_stride(a) != RARRAY_STRIDE_VALUE)) {
376+
rb_ary_widen(a);
377+
}
378+
300379
if (RB_FL_ANY_RAW(a, RARRAY_EMBED_FLAG)) {
301380
return FIX_CONST_VALUE_PTR(RARRAY(a)->as.ary);
302381
}

‎internal/array.h‎

Lines changed: 79 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,79 @@
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. */
29+
static inline const void *
30+
rarray_raw_ptr(VALUE ary)
31+
{
32+
RUBY_ASSERT(rb_array_stride(ary) != RARRAY_STRIDE_VALUE);
33+
RUBY_ASSERT(!FL_TEST_RAW(ary, RARRAY_EMBED_FLAG));
34+
return (const void *)RARRAY(ary)->as.heap.ptr;
35+
}
36+
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. */
41+
static inline long
42+
rarray_narrow_load_int(VALUE ary, long i)
43+
{
44+
const void *p = rarray_raw_ptr(ary);
45+
if (rb_array_stride_unsigned_p(ary)) {
46+
switch (rb_array_stride(ary)) {
47+
case RARRAY_STRIDE_W8: return (long)((const uint8_t *)p)[i];
48+
case RARRAY_STRIDE_W16: return (long)((const uint16_t *)p)[i];
49+
default: return (long)((const uint32_t *)p)[i];
50+
}
51+
}
52+
switch (rb_array_stride(ary)) {
53+
case RARRAY_STRIDE_W8: return ((const int8_t *)p)[i];
54+
case RARRAY_STRIDE_W16: return ((const int16_t *)p)[i];
55+
default: return ((const int32_t *)p)[i];
56+
}
57+
}
58+
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. */
61+
static inline VALUE
62+
rarray_narrow_aref(VALUE ary, long i)
63+
{
64+
return LONG2FIX(rarray_narrow_load_int(ary, i));
65+
}
66+
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. */
76+
#undef RARRAY_AREF
77+
RBIMPL_ATTR_ARTIFICIAL()
78+
static inline VALUE
79+
RARRAY_AREF(VALUE ary, long i)
80+
{
81+
VALUE val;
82+
RBIMPL_ASSERT_TYPE(ary, RUBY_T_ARRAY);
83+
84+
RUBY_ASSERT(i < RARRAY_LEN(ary));
85+
86+
if (RB_UNLIKELY(rb_array_stride(ary) != RARRAY_STRIDE_VALUE)) {
87+
return rarray_narrow_aref(ary, i);
88+
}
89+
90+
RBIMPL_WARNING_PUSH();
91+
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ == 13
92+
RBIMPL_WARNING_IGNORED(-Warray-bounds);
93+
#endif
94+
val = RARRAY_CONST_PTR(ary)[i];
95+
RBIMPL_WARNING_POP();
96+
return val;
97+
}
98+
2699
/* array.c */
27100
VALUE rb_ary_hash_values(long len, const VALUE *elements);
28101
VALUE rb_ary_last(int, const VALUE *, VALUE);
@@ -34,6 +107,11 @@ size_t rb_ary_memsize(VALUE);
34107
VALUE rb_to_array_type(VALUE obj);
35108
VALUE rb_to_array(VALUE obj);
36109
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. */
114+
int rb_ary_narrow(VALUE ary);
37115
size_t rb_ary_size_as_embedded(VALUE ary);
38116
void rb_ary_make_embedded(VALUE ary);
39117
bool rb_ary_embeddable_p(VALUE ary);
@@ -59,7 +137,6 @@ static inline VALUE
59137
rb_ary_entry_internal(VALUE ary, long offset)
60138
{
61139
long len = RARRAY_LEN(ary);
62-
const VALUE *ptr = RARRAY_CONST_PTR(ary);
63140
if (len == 0) return Qnil;
64141
if (offset < 0) {
65142
offset += len;
@@ -68,7 +145,7 @@ rb_ary_entry_internal(VALUE ary, long offset)
68145
else if (len <= offset) {
69146
return Qnil;
70147
}
71-
return ptr[offset];
148+
return RARRAY_AREF(ary, offset); /* narrow-aware; does not allocate */
72149
}
73150

74151
static inline bool
@@ -135,24 +212,6 @@ ARY_SHARED_ROOT_REFCNT(VALUE ary)
135212
})
136213
#endif
137214

138-
#undef RARRAY_AREF
139-
RBIMPL_ATTR_PURE_UNLESS_DEBUG()
140-
RBIMPL_ATTR_ARTIFICIAL()
141-
static inline VALUE
142-
RARRAY_AREF(VALUE ary, long i)
143-
{
144-
VALUE val;
145-
RBIMPL_ASSERT_TYPE(ary, RUBY_T_ARRAY);
146-
147-
RUBY_ASSERT(i < RARRAY_LEN(ary));
148215

149-
RBIMPL_WARNING_PUSH();
150-
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ == 13
151-
RBIMPL_WARNING_IGNORED(-Warray-bounds);
152-
#endif
153-
val = RARRAY_CONST_PTR(ary)[i];
154-
RBIMPL_WARNING_POP();
155-
return val;
156-
}
157216

158217
#endif /* INTERNAL_ARRAY_H */

‎internal/hash.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ VALUE rb_hash_default_value(VALUE hash, VALUE key);
7575
VALUE rb_hash_set_default(VALUE hash, VALUE ifnone);
7676
VALUE rb_hash_set_default_proc(VALUE hash, VALUE proc);
7777
long rb_dbl_long_hash(double d);
78+
long rb_fixnum_hash(VALUE fixnum);
7879
st_table *rb_init_identtable(void);
7980
void rb_init_existing_identtable_with_size(st_table *tbl, st_index_t size);
8081
st_index_t rb_any_hash(VALUE a);

‎ractor.c‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1721,6 +1721,11 @@ obj_traverse_i(VALUE obj, struct obj_traverse_data *data)
17211721
{
17221722
rb_ary_cancel_sharing(obj);
17231723

1724+
/* Narrow storage holds unboxed values, never object references:
1725+
* every element would hit the RB_SPECIAL_CONST_P early return in
1726+
* obj_traverse_i, so there is nothing to visit. Skip the loop. */
1727+
if (rb_array_stride(obj) != RARRAY_STRIDE_VALUE) break;
1728+
17241729
for (int i = 0; i < RARRAY_LENINT(obj); i++) {
17251730
VALUE e = RARRAY_AREF(obj, i);
17261731
if (obj_traverse_i(e, data)) return 1;

0 commit comments

Comments
 (0)