Keep the slots of short known-count arrays in the array's block (#1746)

This commit is contained in:
Chris Tate
2026-10-21 23:26:08 -05:00
committed by GitHub
2 changed files with 109 additions and 17 deletions
+47 -15
View File
@@ -131,13 +131,6 @@ uint32_t scr_arr_ops_register(ScrArrOps *ops) {
const ScrArrOps *scr_arr_ops_overflow(const ScrArr *a) { return scr_arr_ext_of(a)->ops; }
/* Record `ops` as the element ops of `a` (NULL for none). */
static void scr_arr_set_ops(ScrArr *a, const ScrArrOps *ops) {
uint32_t id = scr_arr_ops_id(ops);
a->spare = (uint16_t)((a->spare & ((1u << SCR_ARR_OPS_SHIFT) - 1)) | (id << SCR_ARR_OPS_SHIFT));
if (id == SCR_ARR_OPS_OVERFLOW) scr_arr_ext(a)->ops = ops;
}
/* Canonical array indices grow indexed storage and may leave holes. Negative,
* fractional, NaN, infinity, and 2^32-1 numeric keys use the ordinary
* property vector; only a missing property or a low-level read of a hole
@@ -402,9 +395,10 @@ static void scr_arr_sparse_drop_prefix(ScrArr *a, size_t count) {
#define SCR_ARR_MIN_GROWTH ((size_t)4)
/* Inline slots: an array built by scr_arr_new_small / scr_arr_new_ops_small
* (empty literals the compiler sees appended to) keeps its first dense
* slots and states in its own block, right after the ScrArr, so it costs
* one allocation and its first elements share the header's cache lines.
* (empty literals the compiler sees appended to), or constructed with a
* known element count of up to SCR_ARR_KNOWN_INLINE_MAX, keeps its first
* dense slots and states in its own block, right after the ScrArr, so it
* costs one allocation and its first elements share the header's cache lines.
* `spare` records it. Growth past the inline capacity moves the slots to an
* ordinary allocation; the inline room then stays unused until the array is
* freed. */
@@ -526,6 +520,30 @@ static size_t scr_arr_small_cap(size_t prefix, bool bools) {
#endif
}
/* The most slots a construction with a known element count (array
* literals, copies, slices and the arrays the runtime builds) keeps inline.
* Larger counts, and blocks past the small-object allocator, keep separate
* storage: an array that then grows leaves its inline room unused, which
* this bound keeps small. */
#define SCR_ARR_KNOWN_INLINE_MAX ((size_t)16)
/* Inline capacity of a construction of `need` elements with `prefix` bytes
* of block before the slots, rounded up to fill the block; 0 when its slots
* stay separate. */
static size_t scr_arr_known_cap(size_t prefix, size_t need, bool bools) {
if (need == 0 || need > SCR_ARR_KNOWN_INLINE_MAX) return 0;
#if SCR_SMALL_ALLOC
size_t unit = bools ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES;
size_t bytes = prefix + need * unit;
if (bytes > SCR_SA_MAX) return 0;
return (scr_sa_class_size(scr_sa_class(bytes)) - prefix) / unit;
#else
(void)prefix;
(void)bools;
return need;
#endif
}
static void scr_arr_use_inline(ScrArr *a, size_t cap) {
a->data = (uint64_t *)((char *)a + SCR_ARR_INLINE_OFFSET);
a->cap = (uint32_t)cap;
@@ -1180,7 +1198,8 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make(ScrElemKind el
size_t initial_cap,
bool small) {
size_t unit = elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES;
size_t inline_cap = small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET, elem == SCR_ELEM_BOOL) : 0;
size_t inline_cap = small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET, elem == SCR_ELEM_BOOL)
: scr_arr_known_cap(SCR_ARR_INLINE_OFFSET, initial_cap, elem == SCR_ELEM_BOOL);
ScrArr *a = scr_mem_alloc(inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr));
if (!a) scr_arr_oom();
a->rc = 1;
@@ -1310,11 +1329,17 @@ static void scr_arr_gc_free(void *a0) {
scr_cyc_free_inline(a);
}
static ScrArr *scr_arr_make_ops(const ScrArrOps *ops, size_t initial_cap, bool small) {
/* Inlined into each constructor like scr_arr_make: with `small` and the
* count constant, scr_arr_new_ops_small's size class folds to a table
* index. */
static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrArrOps *ops,
size_t initial_cap,
bool small) {
ScrArr *a;
bool cyc = ops->trace != NULL;
size_t prefix = SCR_ARR_INLINE_OFFSET + (cyc ? sizeof(ScrCycHdr) : 0);
size_t inline_cap =
small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET + (cyc ? sizeof(ScrCycHdr) : 0), false) : 0;
small ? scr_arr_small_cap(prefix, false) : scr_arr_known_cap(prefix, initial_cap, false);
size_t size = inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * SCR_ARR_SLOT_BYTES : sizeof(ScrArr);
if (cyc) {
static ScrCycType cyc_type = { &scr_arr_trace_v, &scr_arr_gc_free, SCR_CYC_KIND_ARRAY };
@@ -1328,12 +1353,19 @@ static ScrArr *scr_arr_make_ops(const ScrArrOps *ops, size_t initial_cap, bool s
a->cap = 0;
a->elem = SCR_ELEM_REF;
a->cyc = cyc;
a->spare = 0;
/* The ops id is written with the other header fields: reading `spare`
* back after scr_arr_use_inline sets its inline flag stalls the
* store-to-load forward. */
uint32_t id = scr_arr_ops_id(ops);
a->spare = (uint16_t)(id << SCR_ARR_OPS_SHIFT);
a->filled = 0;
a->data = NULL;
scr_arr_set_ops(a, ops);
if (inline_cap) scr_arr_use_inline(a, inline_cap);
else if (initial_cap > 0) scr_arr_grow_dense_to(a, initial_cap, SCR_ARR_GROW_EXACT);
/* After the storage: ops past the registry live in the ext, whose
* pointer precedes the dense allocation, so inline slots move out first
* (scr_arr_ext). */
if (id == SCR_ARR_OPS_OVERFLOW) scr_arr_ext(a)->ops = ops;
#ifdef SCR_RC_AUDIT
scr_live_arrays++;
#endif
+62 -2
View File
@@ -398,6 +398,8 @@ static MockRec *mock_cyc_new(int value) {
static ScrArrOps mock_ops = { &mock_retain, &mock_release, NULL };
static ScrArrOps mock_cyc_ops = { &mock_cyc_retain, &mock_cyc_release, &mock_trace };
/* First used after test_ops_overflow fills the registry: its arrays keep the ops in their ext. */
static ScrArrOps mock_cyc_ops_late = { &mock_cyc_retain, &mock_cyc_release, &mock_trace };
static void test_ref_elements(void) {
#ifdef SCR_RC_AUDIT
@@ -514,6 +516,38 @@ static void test_ref_cycle(void) {
#endif
}
static void test_ref_known_count_cycle(ScrArrOps *ops) {
#ifdef SCR_RC_AUDIT
long arrays0 = scr_arr_live_count();
#endif
/* A known-count construction of cycle-capable elements keeps its slots
* after the collector header: the collector traces them, and both the
* collected array and one released by its count free the one block. */
ScrArr *arr = scr_arr_new_ops(ops, 2);
bool in_block = arr->data == (uint64_t *)((char *)arr + ((sizeof(ScrArr) + 7) & ~(size_t)7));
/* Ops past the registry (once test_ops_overflow has filled it) live in
* the ext, which moves the slots to their own allocation. */
bool overflow = (arr->spare >> SCR_ARR_OPS_SHIFT) == SCR_ARR_OPS_OVERFLOW;
check(arr->cap >= 2 && in_block == !overflow && scr_arr_ops(arr) == ops,
"known-count cycle-capable construction is inline unless its ops need the ext");
MockRec *first = mock_cyc_new(1);
first->owner = (ScrArr *)scr_arr_retain(arr);
scr_arr_push_ref(arr, first);
scr_arr_push_ref(arr, mock_cyc_new(2));
scr_arr_release(arr);
scr_collect_cycles();
check(mock_live == 0, "cycle through an inline array collected");
ScrArr *plain = scr_arr_new_ops(ops, 3);
for (int i = 0; i < 4; i++) scr_arr_push_ref(plain, mock_cyc_new(i));
check(scr_arr_len(plain) == 4, "a cycle-capable inline array grows out of its block");
scr_arr_release(plain);
scr_collect_cycles();
check(mock_live == 0, "released inline cycle-capable array frees its elements");
#ifdef SCR_RC_AUDIT
check(scr_arr_live_count() == arrays0, "known-count cycle: no arrays leaked");
#endif
}
static void test_ref_truncate_cycle(void) {
#ifdef SCR_RC_AUDIT
long arrays0 = scr_arr_live_count();
@@ -1543,9 +1577,33 @@ static void test_inline_slots(void) {
long strings0 = scr_str_live_count();
long arrays0 = scr_arr_live_count();
#endif
ScrArr *plain = scr_arr_new(SCR_ELEM_F64, 4);
check(!inline_slots(plain), "plain constructions keep separate storage");
ScrArr *plain = scr_arr_new(SCR_ELEM_F64, 0);
check(!inline_slots(plain) && plain->data == NULL, "empty plain constructions allocate no slots");
scr_arr_release(plain);
/* A construction with a known count up to 16 slots keeps them in its own
* block; growth past them moves them out like any inline array. */
ScrArr *known = scr_arr_new(SCR_ELEM_F64, 4);
check(inline_slots(known) && known->cap >= 4 && known->len == 0 && !scr_arr_has(known, 0) &&
scr_arr_states_end(known) == (uint8_t *)(known->data + known->cap) + known->cap,
"a known-count construction places its slots in the array block");
size_t known_cap = known->cap;
for (size_t i = 0; i <= known_cap; i++) scr_arr_push_f64(known, (double)i);
check(!inline_slots(known) && scr_arr_len(known) == known_cap + 1 && scr_arr_get_f64(known, 0) == 0 &&
scr_arr_get_f64(known, (double)known_cap) == (double)known_cap,
"growth past a known-count construction moves the slots out");
scr_arr_release(known);
ScrArr *sixteen = scr_arr_new(SCR_ELEM_STR, 16);
for (int i = 0; i < 16; i++) scr_arr_push_ref(sixteen, scr_str_new("s", 1));
check(inline_slots(sixteen) && scr_arr_len(sixteen) == 16, "sixteen known slots stay inline");
scr_arr_release(sixteen);
ScrArr *large = scr_arr_new(SCR_ELEM_F64, 17);
check(!inline_slots(large) && large->cap >= 17, "larger known counts keep separate storage");
scr_arr_release(large);
ScrArr *known_flags = scr_arr_new(SCR_ELEM_BOOL, 5);
scr_arr_push_bool(known_flags, true);
check(inline_slots(known_flags) && known_flags->cap >= 5 && scr_arr_get_bool(known_flags, 0),
"known-count boolean constructions are inline too");
scr_arr_release(known_flags);
ScrArr *a = scr_arr_new_small(SCR_ELEM_F64);
check(inline_slots(a) && a->cap >= 4 && a->len == 0 &&
scr_arr_states_end(a) == (uint8_t *)(a->data + a->cap) + a->cap,
@@ -2205,7 +2263,9 @@ int main(int argc, char **argv) {
test_nested_rc();
test_index_of_includes();
test_ref_elements();
test_ref_known_count_cycle(&mock_cyc_ops);
test_ops_overflow();
test_ref_known_count_cycle(&mock_cyc_ops_late);
test_ext_through_storage_changes();
test_ref_cycle();
test_ref_truncate_cycle();