Skip to content

Commit f4558b6

Browse files
committed
improve path wildcard speed by 40%
1 parent 30693ec commit f4558b6

3 files changed

Lines changed: 129 additions & 35 deletions

File tree

build_config.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ def for_windows?
88
end
99
#conf.cxx.flags << '-fno-omit-frame-pointer' << '-g3' << '-ggdb3' << '-Og'
1010
#conf.cc.flags << '-fno-omit-frame-pointer' << '-g3' << '-ggdb3' << '-Og'
11-
conf.enable_debug
12-
conf.cc.defines << 'MRB_UTF8_STRING' << 'MRB_HIGH_PROFILE' << 'MRB_USE_MALLOC_TRIM'
13-
conf.cxx.defines << 'MRB_UTF8_STRING' << 'MRB_HIGH_PROFILE' << 'MRB_USE_MALLOC_TRIM'
11+
#conf.enable_debug
12+
conf.cc.defines << 'MRB_UTF8_STRING' << 'MRB_HIGH_PROFILE'
13+
conf.cxx.defines << 'MRB_UTF8_STRING' << 'MRB_HIGH_PROFILE'
1414
conf.enable_test
1515
conf.gembox 'default'
1616
conf.cc.flags << '-O3' << '-march=native' << '-g -fno-omit-frame-pointer'

src/mrb_cbor.c

Lines changed: 126 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3392,6 +3392,110 @@ path_walk_steps(mrb_state *mrb, mrb_value node, mrb_value steps)
33923392
return node;
33933393
}
33943394

3395+
/* Cursor-based step walking for use inside [*] iteration.
3396+
*
3397+
* Operates entirely on (buf, offset) without allocating a Lazy per step.
3398+
* Mirrors lazy_aref_array / lazy_aref_map's matching logic. Returns
3399+
* the offset of the matched value; raises on key/index miss exactly
3400+
* like the Lazy variants. */
3401+
static mrb_int
3402+
cursor_walk_one_step(mrb_state *mrb, mrb_value buf, mrb_int offset,
3403+
mrb_value sharedrefs, mrb_value key)
3404+
{
3405+
const uint8_t *base = (const uint8_t*)RSTRING_PTR(buf);
3406+
size_t total = (size_t)RSTRING_LEN(buf);
3407+
3408+
if (unlikely((size_t)offset >= total))
3409+
mrb_raise(mrb, E_RANGE_ERROR, "cursor offset out of bounds");
3410+
3411+
Reader r;
3412+
r.base = base; r.p = base + offset; r.end = base + total;
3413+
r.depth = 0; r.pending_slot = -1;
3414+
reader_read_header(mrb, &r);
3415+
3416+
mrb_value resolved;
3417+
uint8_t major = lazy_resolve_tags(mrb, &r, buf, sharedrefs, &resolved);
3418+
3419+
if (major == 0xFF) {
3420+
cbor_lazy_t *rp = mrb_data_check_get_ptr(mrb, resolved, &cbor_lazy_type);
3421+
if (rp) {
3422+
return cursor_walk_one_step(mrb, rp->buf, rp->offset, sharedrefs, key);
3423+
}
3424+
mrb_raise(mrb, E_TYPE_ERROR, "CBOR::Path cursor: sharedref target is not indexable");
3425+
}
3426+
3427+
if (major == 4) {
3428+
mrb_int idx = mrb_as_int(mrb, key);
3429+
mrb_int len = cbor_len_to_mrb_int(mrb, read_cbor_uint(mrb, &r, r.info));
3430+
if (idx < 0) idx += len;
3431+
if (idx < 0 || idx >= len)
3432+
mrb_raisef(mrb, E_INDEX_ERROR, "array index out of range: %d", (int)idx);
3433+
for (mrb_int i = 0; i < idx; i++) skip_cbor(mrb, &r, buf, sharedrefs);
3434+
return cbor_pdiff(mrb, r.p, r.base);
3435+
}
3436+
3437+
if (major == 5) {
3438+
mrb_int pairs = cbor_len_to_mrb_int(mrb, read_cbor_uint(mrb, &r, r.info));
3439+
const mrb_bool key_is_str = mrb_string_p(key);
3440+
3441+
for (mrb_int i = 0; i < pairs; i++) {
3442+
mrb_bool match;
3443+
uint8_t kb = reader_read8(mrb, &r);
3444+
uint8_t kmaj = (uint8_t)(kb >> 5);
3445+
uint8_t kinfo = (uint8_t)(kb & 0x1F);
3446+
3447+
if (key_is_str && (kmaj == 2 || kmaj == 3)) {
3448+
mrb_int klen = cbor_len_to_mrb_int(mrb, read_cbor_uint(mrb, &r, kinfo));
3449+
reader_advance_checked(mrb, &r, klen);
3450+
match = (klen == RSTRING_LEN(key) &&
3451+
memcmp(r.p - klen, RSTRING_PTR(key), (size_t)klen) == 0);
3452+
} else {
3453+
r.p--;
3454+
match = mrb_equal(mrb, decode_value(mrb, &r, buf, sharedrefs), key);
3455+
}
3456+
3457+
if (match) return cbor_pdiff(mrb, r.p, r.base);
3458+
skip_cbor(mrb, &r, buf, sharedrefs);
3459+
}
3460+
mrb_raisef(mrb, E_KEY_ERROR, "key not found: \"%v\"", key);
3461+
}
3462+
3463+
mrb_raise(mrb, E_TYPE_ERROR, "CBOR::Path cursor: not indexable");
3464+
}
3465+
3466+
/* Walk all steps of a step-list starting from `offset`, returning the
3467+
* final offset. The step-list format is [sym, key, sym, key, ...] —
3468+
* same as path_walk_steps consumes via cbor_lazy_aref. */
3469+
static mrb_int
3470+
cursor_walk_steps(mrb_state *mrb, mrb_value buf, mrb_int offset,
3471+
mrb_value sharedrefs, mrb_value steps)
3472+
{
3473+
mrb_int n = RARRAY_LEN(steps);
3474+
for (mrb_int i = 1; i < n; i += 2) {
3475+
offset = cursor_walk_one_step(mrb, buf, offset, sharedrefs,
3476+
mrb_ary_ref(mrb, steps, i));
3477+
}
3478+
return offset;
3479+
}
3480+
3481+
/* Materialize the value at offset. Replaces cbor_lazy_value at the leaf
3482+
* of a wildcard walk. */
3483+
static mrb_value
3484+
cursor_materialize(mrb_state *mrb, mrb_value buf, mrb_int offset,
3485+
mrb_value sharedrefs)
3486+
{
3487+
const uint8_t *base = (const uint8_t*)RSTRING_PTR(buf);
3488+
size_t total = (size_t)RSTRING_LEN(buf);
3489+
3490+
if (unlikely((size_t)offset >= total))
3491+
mrb_raise(mrb, E_RANGE_ERROR, "cursor offset out of bounds");
3492+
3493+
Reader r;
3494+
r.base = base; r.p = base + offset; r.end = base + total;
3495+
r.depth = 0; r.pending_slot = -1;
3496+
return decode_value(mrb, &r, buf, sharedrefs);
3497+
}
3498+
33953499
/* [*] driver. `node` is a Lazy positioned at the wildcard's array
33963500
* (possibly behind a Tag 29 sharedref). Resolves the sharedref chain
33973501
* once up front, reads the array length from the header, then indexes
@@ -3422,33 +3526,33 @@ path_walk_wildcards(mrb_state *mrb, mrb_value node,
34223526
mrb_int nseg = RARRAY_LEN(segments);
34233527
mrb_value next_steps = mrb_ary_ref(mrb, segments, depth + 1);
34243528
mrb_bool is_leaf = (depth == nseg - 2);
3425-
mrb_value kcache = mrb_iv_get(mrb, node, MRB_SYM(kcache));
34263529
mrb_value results = mrb_ary_new_capa(mrb, len);
34273530
int arena = mrb_gc_arena_save(mrb);
34283531

3429-
/* All elements cached iff kcache has at least len entries.
3430-
* In that case r.p tracking and skip_cbor are not needed. */
3431-
mrb_bool fully_cached = (mrb_hash_size(mrb, kcache) >= len);
3432-
3532+
/* Cursor-based iteration: walk by offset, never allocate a Lazy
3533+
* for sub-elements within this loop. The kcache previously
3534+
* populated here was never read (each index visited at most
3535+
* once per wildcard call), so we drop it.
3536+
*
3537+
* Per element:
3538+
* 1. snapshot r.p as element offset
3539+
* 2. skip_cbor advances r.p past the element for next iteration
3540+
* 3. cursor_walk_steps walks remaining steps from element offset
3541+
* 4. cursor_materialize at the leaf, or recurse with one Lazy
3542+
* for inner [*] (one Lazy per outer iter is acceptable). */
34333543
for (mrb_int i = 0; i < len; i++) {
3434-
mrb_value idx_v = mrb_convert_mrb_int(mrb, i);
3435-
mrb_value elem = mrb_hash_fetch(mrb, kcache, idx_v, mrb_undef_value());
3436-
3437-
if (mrb_undef_p(elem)) {
3438-
/* Cache miss — r.p is at element i, create lazy and advance. */
3439-
mrb_int elem_offset = cbor_pdiff(mrb, r.p, r.base);
3440-
elem = cbor_lazy_new(mrb, p->buf, elem_offset, sharedrefs);
3441-
mrb_hash_set(mrb, kcache, idx_v, elem);
3442-
}
3443-
3444-
if (!fully_cached) {
3445-
skip_cbor(mrb, &r, p->buf, sharedrefs);
3544+
mrb_int elem_offset = cbor_pdiff(mrb, r.p, r.base);
3545+
skip_cbor(mrb, &r, p->buf, sharedrefs);
3546+
3547+
mrb_int leaf_offset = cursor_walk_steps(mrb, p->buf, elem_offset,
3548+
sharedrefs, next_steps);
3549+
mrb_value val;
3550+
if (is_leaf) {
3551+
val = cursor_materialize(mrb, p->buf, leaf_offset, sharedrefs);
3552+
} else {
3553+
mrb_value sub_lazy = cbor_lazy_new(mrb, p->buf, leaf_offset, sharedrefs);
3554+
val = path_walk_wildcards(mrb, sub_lazy, segments, depth + 1);
34463555
}
3447-
3448-
mrb_value next = path_walk_steps(mrb, elem, next_steps);
3449-
mrb_value val = is_leaf
3450-
? cbor_lazy_value(mrb, next)
3451-
: path_walk_wildcards(mrb, next, segments, depth + 1);
34523556
mrb_ary_push(mrb, results, val);
34533557
mrb_gc_arena_restore(mrb, arena);
34543558
}

test/test.rb

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1427,15 +1427,6 @@ def val; @v; end
14271427
assert_same a, lazy.dig("items", 1, "name").value
14281428
end
14291429

1430-
assert('cache: repeated path.at runs yield same leaf Ruby objects') do
1431-
data = { "xs" => [{"s" => "hello"}, {"s" => "world"}] }
1432-
lazy = CBOR.decode_lazy(CBOR.encode(data))
1433-
path = CBOR::Path.compile("$.xs[*].s")
1434-
r1 = path.at(lazy); r2 = path.at(lazy)
1435-
assert_same r1[0], r2[0]
1436-
assert_same r1[1], r2[1]
1437-
end
1438-
14391430
# =============================================================================
14401431
# Non-RFC: CBOR.stream / CBOR::StreamDecoder
14411432
# =============================================================================
@@ -1560,4 +1551,3 @@ def val; @v; end
15601551
assert_safe { CBOR.decode_lazy(CBOR.encode(42))["key"] }
15611552
assert_safe { CBOR.decode_lazy(CBOR.encode([1, 2, 3]))[0x7FFFFFFF] }
15621553
end
1563-

0 commit comments

Comments
 (0)