Conversation
`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
|
|
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
Contributor
|
Solved in #1631, closing |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-sideusec_per_callon a release build oflibrejson.so, 65536-element packed f32 array:LLAPI.GETATindex 0LLAPI.GETATindex 65535Identical. 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
nthskip loop, becausenext()on the innerstd::slice::Iteris 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 contract —ijson::array::ArrayIterimplements onlynext(). 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: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_indexis 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:
GETAT8.59µs → 8.38/8.74µsSame
get_indexalso backs JSONPath index/slice evaluation andJSON.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_getAtover every packed array tag, covering the LLAPI entry point RediSearch uses.cargo test -p json_path(299 tests) and-p redis_jsongreen;cargo clippy --all-targetsclean on both crates.Reviewer judgement needed
halfis added as a dev-dependency ofjson_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_indexindex the packed/heterogeneous backing slice directly instead ofiter().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, andJSON.ARRINDEX. Heterogeneous elements stay borrowed; packed tags wrap a primitive into an ownedIValue.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_attests.halfis ajson_pathdev-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.