Skip to content

Optimize rank for StringView and BinaryView arrays #10604

Description

@YUZHEthefool

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions