Skip to content

Index the backing slice in IValue::get_index instead of iter().nth() - #1630

Closed
gabsow wants to merge 1 commit into
masterfrom
claude/get-index-slice-indexing
Closed

gabsow wants to merge 1 commit into
masterfrom
claude/get-index-slice-indexing

Conversation

@gabsow

@gabsow gabsow commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to RED-213492, split out of #1629.

Please read this first — it is not the fix the ticket asks for

The ticket hypothesis was that getAt(index) on packed typed arrays became linear, turning a per-element vector read into O(dim²). I measured it and that is not happening in the shipped module. Server-side usec_per_call on a release build of librejson.so, 65536-element packed f32 array:

usec_per_call
LLAPI.GETAT index 0 8.59
LLAPI.GETAT index 65535 8.59

Identical. Per-element cost across a full scan is flat too — 66ns/element at dim 1280, 92ns at 8192, 58ns at 65536 — so total cost is linear in elements read, not quadratic. The optimizer collapses the nth skip loop, because next() on the inner std::slice::Iter is a pointer bump and the discarded item construction is side-effect-free.

So: this PR does not speed up production, and the slow vector loads in the ticket are not caused by indexed-access complexity. That still needs a separate answer.

What this PR is for

arr.iter().nth(index) is O(index) by contractijson::array::ArrayIter implements only next(). Today it is fast because of an optimization we do not control, and that optimization already fails in another real build of this workspace. Same source, cargo test --release -p json_path, single read of the last element:

n index 0 last index ratio
1280 18.67ns 19,167.69ns ~1,000x
65536 17.70ns 966,498.40ns ~54,000x

A full index-by-index scan there is genuinely quadratic. Whether that cliff shows up depends on inlining, so any future change to codegen settings, crate boundaries, or how get_index is reached could surface it in the module too.

The change

Index the backing slice directly via as_slice(): borrow the element for heterogeneous arrays, build the number for the 12 packed tags. Cost is index-independent by construction instead of by optimizer behaviour.

Measured after the change:

  • the slow build above: 17.74ns for the last element at n=65536 (was 966,498ns)
  • the shipped module: unchanged within noise — scan of 1280 elements 84.93µs before, 85.62µs after; GETAT 8.59µs → 8.38/8.74µs

Same get_index also backs JSONPath index/slice evaluation and JSON.ARRINDEX, so those get the same guarantee.

Tests

  • json_node.rs: indexing over all 12 packed array tags; heterogeneous arrays return a borrow rather than a clone (asserted by pointer identity); out-of-bounds and non-array cases; and a guard that per-element cost does not grow with array length (512 vs 8192 elements, best-of-5 passes, asserts under 4x where linear would be ~16x). That guard fails on the previous implementation in this build.
  • c_api.rs: JSONAPI_getAt over every packed array tag, covering the LLAPI entry point RediSearch uses.

cargo test -p json_path (299 tests) and -p redis_json green; cargo clippy --all-targets clean on both crates.

Reviewer judgement needed

  1. Is a no-op-in-production change worth taking? The argument is robustness, not speed. If you would rather not carry a timing-sensitive test for a cliff that is currently latent, closing this is a reasonable call.
  2. The timing guard. It is best-of-5 with a 4x threshold against an expected 16x, so it has headroom, but it is still wall-clock in a unit test and CI runners are noisy. Happy to drop it and keep only the deterministic per-type tests.
  3. half is added as a dev-dependency of json_path (Cargo.toml + Cargo.lock) so the tests can construct f16/bf16 arrays.

Generated by Claude Code


Note

Medium Risk
Touches the shared array-index path used by the LLAPI and JSONPath. Behavior should be equivalent, but packed-array element wrapping and a timing-sensitive unit test are the main review points.

Overview
Makes IValue::get_index index the packed/heterogeneous backing slice directly instead of iter().nth(index), so cost is O(1) by construction rather than depending on the optimizer collapsing a linear skip.

That path backs LLAPI JSONAPI_getAt (RediSearch vector reads), JSONPath index/slice, and JSON.ARRINDEX. Heterogeneous elements stay borrowed; packed tags wrap a primitive into an owned IValue.

Adds coverage for all 12 packed tags, borrow identity on mixed arrays, out-of-bounds/non-array, a wall-clock guard that per-element cost stays flat (512 vs 8192), and matching json_api_get_at tests. half is a json_path dev-dependency for f16/bf16 fixtures.

Reviewed by Cursor Bugbot for commit 9212a34. Bugbot is set up for automated code reviews on this repo. Configure here.

`IValue::get_index` was `arr.iter().nth(index)`. `ijson::array::ArrayIter` does
not override `Iterator::nth`, so by contract that walks the array one element at
a time: O(index) per read, and O(N^2) for an index-by-index pass.

In the module as shipped this does not happen - the optimizer collapses the skip
loop, and measurements on a release build of librejson.so show indexed access is
already index-independent:

  LLAPI.GETAT on a 65536-element packed f32 array (usec_per_call, server-side)
    index 0      8.59us
    index 65535  8.59us

So this is not a fix for a production regression, and it is not the cause of the
slow vector loads in RED-213492 - per-element cost there is flat (~60-90ns
through the LLAPI, linear in the number of elements read, not quadratic).

It is a fix for depending on an optimization we do not control. The collapse is
not guaranteed, and it already does not happen in every build of this workspace:
in the json_path release test binary the same source walks the array, and a
single read of the last element costs

    n=1280    index 0 18.67ns   last 19167.69ns    (1000x)
    n=65536   index 0 17.70ns   last 966498.40ns   (54000x)

Indexing the backing slice directly makes the cost index-independent by
construction rather than by optimizer behaviour: borrow the element for
heterogeneous arrays, build the number for packed ones. After the change the
same build reports 17.74ns for the last element at n=65536, and the shipped
module is unchanged within noise (scan of 1280 elements: 84.93us before,
85.62us after).

Tests:
- `json_node.rs`: indexing over all 12 packed array tags, borrowing rather than
  cloning for heterogeneous arrays, out-of-bounds and non-array cases, and a
  guard that per-element cost does not grow with array length (512 vs 8192,
  best-of-5, fails on the previous implementation in this build).
- `c_api.rs`: `JSONAPI_getAt` over every packed array tag, covering the LLAPI
  entry point RediSearch uses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZUWRHkrd9XtKdKqENRs9r
@gabsow
gabsow requested a review from AvivDavid23 August 20, 2026 17:53
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.97%. Comparing base (28f4bb1) to head (9212a34).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1630   +/-   ##
=======================================
  Coverage   85.97%   85.97%           
=======================================
  Files          15       15           
  Lines        5305     5306    +1     
=======================================
+ Hits         4561     4562    +1     
  Misses        744      744           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@AvivDavid23

Copy link
Copy Markdown
Contributor

Solved in #1631, closing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants