Is your feature request related to a problem or challenge?
arrow_ord::rank currently handles Utf8View and BinaryView by materializing Vec<(&[u8], u32)> and sorting the full slices. This is robust when many values share a prefix, but comparisons repeatedly operate on slice descriptors and can dereference backing buffers for values longer than the 12-byte inline capacity.
#10600 adds the missing fixed- and variable-length StringView rank benchmarks. Those baselines, together with additional long-value and prefix-collision cases, show that there is substantial room to reduce comparison cost while preserving the existing behavior for adversarial collisions.
Describe the solution you'd like
Cache a sortable key once per value before sorting:
- For fully inline arrays, convert each raw view to the same
u128 key used by the byte-view sort path.
- For arrays with backing buffers, cache the first 16 bytes as a big-endian
u128 and resolve the full value only when two keys collide.
- Sample valid values from small local windows in both halves of the array. If roughly one third of sampled keys collide, fall back to the existing slice-based implementation.
- Return early from collision sampling when four equal keys already guarantee the fallback threshold.
The fallback is important: always using the cached-key comparator made a workload where every value shares its first 16 bytes about 71% slower. With adaptive fallback, that workload remains statistically unchanged from the existing implementation.
Criterion results for 4,096 values on x86-64 Windows, comparing a clean main build with the proposed implementation:
| Case |
main |
Proposed |
Change |
string_view[10] |
190.13 us |
38.96 us |
-79.4% |
string_view[10] nulls |
80.46 us |
22.38 us |
-72.3% |
string_view[0-13] sparse long |
244.57 us |
73.98 us |
-69.9% |
string_view[0-400] |
235.66 us |
51.36 us |
-78.2% |
string_view[0-400] nulls |
95.69 us |
27.31 us |
-71.5% |
string_view[13-100] same 7-byte prefix |
480.16 us |
53.34 us |
-89.1% |
string_view[13-100] same prefix, nulls |
206.60 us |
26.19 us |
-87.0% |
string_view[24] same 16-byte prefix |
132.59 us |
132.79 us |
no significant change (p = 0.11) |
string_view[24] same prefix, nulls |
62.76 us |
62.58 us |
no significant change (p = 0.06) |
Describe alternatives you've considered
I prototyped sorting a Vec<u32> of indices and comparing the inline prefix before looking up the backing buffer. It regressed all tested workloads by 20-43% because the sort performs O(n log n) comparisons, and each comparison added branches, view lookups, and slice construction. Materializing a key once per value moves that work out of the comparison loop.
Always using the 16-byte key without sampling performs well for normal and moderately colliding values, but regresses when all keys collide. Keeping the current slice path as an adaptive fallback avoids that tradeoff.
Additional context
Is your feature request related to a problem or challenge?
arrow_ord::rankcurrently handlesUtf8ViewandBinaryViewby materializingVec<(&[u8], u32)>and sorting the full slices. This is robust when many values share a prefix, but comparisons repeatedly operate on slice descriptors and can dereference backing buffers for values longer than the 12-byte inline capacity.#10600 adds the missing fixed- and variable-length
StringViewrank benchmarks. Those baselines, together with additional long-value and prefix-collision cases, show that there is substantial room to reduce comparison cost while preserving the existing behavior for adversarial collisions.Describe the solution you'd like
Cache a sortable key once per value before sorting:
u128key used by the byte-view sort path.u128and resolve the full value only when two keys collide.The fallback is important: always using the cached-key comparator made a workload where every value shares its first 16 bytes about 71% slower. With adaptive fallback, that workload remains statistically unchanged from the existing implementation.
Criterion results for 4,096 values on x86-64 Windows, comparing a clean
mainbuild with the proposed implementation:mainstring_view[10]string_view[10]nullsstring_view[0-13]sparse longstring_view[0-400]string_view[0-400]nullsstring_view[13-100]same 7-byte prefixstring_view[13-100]same prefix, nullsstring_view[24]same 16-byte prefixp = 0.11)string_view[24]same prefix, nullsp = 0.06)Describe alternatives you've considered
I prototyped sorting a
Vec<u32>of indices and comparing the inline prefix before looking up the backing buffer. It regressed all tested workloads by 20-43% because the sort performs O(n log n) comparisons, and each comparison added branches, view lookups, and slice construction. Materializing a key once per value moves that work out of the comparison loop.Always using the 16-byte key without sampling performs well for normal and moderately colliding values, but regresses when all keys collide. Keeping the current slice path as an adaptive fallback avoids that tradeoff.
Additional context
StringViewArrayandBinaryViewArray, including zero bytes, prefix-length relationships, duplicate values, nulls, and values that differ only after byte 16.