Skip to content

GH-54668: [C++][Compute] Implement equality comparison between Utf8View and other string types - #55171

Open
Awesome06 wants to merge 5 commits into
apache:mainfrom
Awesome06:gh-54668-utf8view-comparison
Open

Awesome06 wants to merge 5 commits into
apache:mainfrom
Awesome06:gh-54668-utf8view-comparison

Conversation

@Awesome06

@Awesome06 Awesome06 commented Oct 24, 2026 •

Copy link
Copy Markdown

Rationale for this change

Currently, Arrow compute supports comparing Utf8 with LargeUtf8 and comparing Utf8View with Utf8View, but attempting to compare a Utf8View array/scalar with either Utf8 or LargeUtf8 fails with:

ArrowNotImplementedError: Function 'equal' has no kernel matching input types (string, string_view)

Rather than promoting string types to views (which allocates 16-byte descriptors and copies scalar heap data), this change adds direct mixed-type comparison kernels between offset-based strings (Utf8, LargeUtf8, Binary, LargeBinary) and view types (Utf8View, BinaryView).

By reading the respective string and view array/scalar buffers in-place via applicator::ScalarBinary, comparison operations execute with zero memory pool allocations beyond the output boolean bitmap and achieve performance parity with same-type comparisons.

What changes are included in this PR?

  1. cpp/src/arrow/compute/kernels/scalar_compare.cc:

    • Registered direct mixed kernels for (utf8, utf8_view), (large_utf8, utf8_view), (binary, binary_view), and (large_binary, binary_view) in both directions using applicator::ScalarBinary.
    • Updated MakeFlippedCompare to only rebuild kernel signatures when input argument types differ (in_types[0] != in_types[1]). This preserves original signature constraints (such as DecimalsHaveSameScale) on symmetric kernels while correctly reversing asymmetric signatures for less and less_equal.
    • Added DispatchExactImpl fast-path in CompareFunction::DispatchBest after dictionary decoding so dictionary-encoded strings compared with views match direct mixed kernels immediately.
  2. cpp/src/arrow/compute/kernels/scalar_compare_test.cc:

    • Added bidirectional mixed comparison tests across equality and ordered comparisons (equal, not_equal, less, less_equal, greater, greater_equal).
    • Added tests for empty arrays, all-null arrays, sliced offsets, array-scalar, and long strings (> 12 bytes).
    • Added regression tests for differing decimal scales across less, less_equal, greater, and greater_equal.
  3. cpp/src/arrow/compute/expression_test.cc:

    • Added regression test TestIssue54668DecimalComparisonDifferentScales verifying bound expressions for < and <= with mismatched decimal scales (1.00 < 9.0).
    • Added expression test for literal on LHS ("banana" < view_col) testing expression simplifier guarantee flipping.
  4. python/pyarrow/tests/test_compute.py:

    • Added test_compare_mixed_string_types and test_compare_mixed_binary_types parameterized across all 9 permutations of string/binary types (including arrays, scalars, slices, and out-of-line strings).

Are these changes tested?

Yes:

  • C++ unit tests in cpp/src/arrow/compute/kernels/scalar_compare_test.cc and cpp/src/arrow/compute/expression_test.cc.
  • Python unit tests in python/pyarrow/tests/test_compute.py.

Are there any user-facing changes?

Yes. Users can now directly compare Utf8View and BinaryView columns against Utf8, LargeUtf8, Binary, and LargeBinary columns and scalars across all comparison functions (equal, not_equal, less, less_equal, greater, greater_equal) without explicit casting.

Was AI used for this PR?

AI was used to diagnose kernel dispatch paths, draft implementation code, and construct test cases under developer direction. The changes and tests were reviewed, compiled, and verified locally.

PR code and description written by:

  • Human
  • AI

Reviewed before submission by:

  • Human
  • AI
  • Not reviewed

…Utf8View and other string types

### Rationale for this change

Currently, Arrow compute supports comparing `Utf8` with `LargeUtf8` and comparing `Utf8View` with `Utf8View`, but attempting to compare a `Utf8View` array/scalar with either `Utf8` or `LargeUtf8` fails with:
```text
ArrowNotImplementedError: Function 'equal' has no kernel matching input types (string, string_view)
```

In `scalar_compare.cc`, comparison operators (`equal`, `not_equal`, `less`, `greater`, etc.) enforce homogeneous symmetric kernel signatures (`{ty, ty}`) and rely on `DispatchBest` to resolve heterogeneous types via `CommonBinary()`. However, `CommonBinary()` did not previously recognize `Type::STRING_VIEW` or `Type::BINARY_VIEW`, returning `nullptr` and causing `DispatchBest` to fail.

When mixing string/binary view types with offset-based string/binary types, promoting to the view type (`utf8_view` or `binary_view`) is optimal:
1. `ZeroCopyCastExec` in `scalar_cast_string.cc` reuses the existing character data buffer directly as variadic buffer 0, only allocating 16-byte descriptors without copying string payload data. (Conversely, promoting to `LargeUtf8` would require a deep copy of all string data into contiguous buffers).
2. For scalar-array comparisons (`string_view_array == "literal"`), only the scalar literal is converted ($O(1)$ zero-allocation), leaving array memory completely untouched.

### What changes are included in this PR?

1. `cpp/src/arrow/compute/kernels/codegen_internal.cc`:
   - Extended `CommonBinary()` to handle `Type::STRING_VIEW` and `Type::BINARY_VIEW`.
   - Promotes mixed string types to `utf8_view()`, and mixed binary/fixed-size types to `binary_view()`.

2. `cpp/src/arrow/compute/kernels/scalar_compare_test.cc`:
   - Added `CheckDispatchBest` assertions for view <-> non-view binary and string combinations (`utf8`, `large_utf8`, `utf8_view`, `binary`, `large_binary`, `binary_view`, `fixed_size_binary`).
   - Added `TEST(TestBinaryViewCompareKernel, MixedStringTypes)` validating:
     - Short strings (<= 12 bytes, testing inline descriptors)
     - Long strings (> 12 bytes, testing out-of-line prefix + buffer lookup)
     - Sliced arrays (`array.Slice(1, 2)`)
     - Empty arrays (`[]`) and all-null arrays (`[null, null]`)
     - Array-scalar and scalar-array operations across `equal`, `not_equal`, `less`, and `greater`.

3. `python/pyarrow/tests/test_compute.py`:
   - Added `test_compare_mixed_string_types` parameterized across all 9 permutations of `string`, `large_string`, and `string_view` (including sliced and >12-byte strings).
   - Added `test_compare_mixed_binary_types` parameterized across all 9 permutations of `binary`, `large_binary`, and `binary_view`.

### Are these changes tested?

Yes:
- C++ unit tests in `arrow-compute-scalar-math-test` (including `TestCompareKernel.DispatchBest` and `TestBinaryViewCompareKernel.MixedStringTypes`). All 594 tests in the test suite pass with 0 failures.
- PyArrow unit tests added to `python/pyarrow/tests/test_compute.py`.

### Are there any user-facing changes?

Yes. Users can now directly compare `Utf8View` / `BinaryView` columns against `Utf8`, `LargeUtf8`, `Binary`, and `LargeBinary` columns or scalars in expressions and compute kernels without explicit casting or `ArrowNotImplementedError`.

### Was AI used for this PR?

AI was used to diagnose the type promotion dispatch path, draft implementation code, and construct test cases under developer direction. The changes and tests were reviewed, compiled, and verified locally.

**PR code and description written by:**
- [X] Human
- [X] AI

**Reviewed before submission by:**
- [X] Human
- [X] AI
- [ ] Not reviewed

* GitHub Issue: apache#54668
Copilot AI balanced review requested due to automatic review settings October 24, 2026 08:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #54668 has been automatically assigned in GitHub to PR creator.

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #54668 has been automatically assigned in GitHub to PR creator.

@dmitry-chirkov-dremio dmitry-chirkov-dremio left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we compare these types directly? Promotion copies the whole scalar string, even when the first byte differs.

I tried mixed kernels using ScalarBinary and borrowed std::string_view values. They reuse the existing comparison operators without conversion.

Local equality benchmarks on Apple ARM64. Times are microseconds per call:

Input Same type This PR Direct mixed
8 B, equal scalars 1.16 2.78 1.23
4 KiB, equal scalars 1.26 2.99 1.29
1 MiB, equal scalars 22.06 36.72 21.74
1 MiB, first byte differs 1.17 15.88 1.19
131,072 × 8 B, equal arrays 318.48 585.41 297.23
131,072 × 64 B, equal arrays 440.54 564.70 402.25
4,096 × 4 KiB, equal arrays 445.84 446.22 424.39

For 131,072 rows, direct comparison also saves 2 MiB of temporary view descriptors. Arrow-pool allocation drops from about 2.02 MiB to 16 KiB. Long array strings already share their payload, so this saves descriptors, not payload copies.

Tested f53968e against the local prototype. The comparison suite and 486 additional checks passed with the prototype.

We could add kernels for utf8/utf8_view and large_utf8/utf8_view, in both directions. When creating less/less_equal, reverse the mixed signature together with the arguments.

…en string and string_view

Avoid promoting string scalars to string_view in CommonBinary during scalar comparisons. Instead, register direct bidirectional kernels for utf8/binary and their view variants using ScalarBinary to compare borrowed string views zero-copy. Also reverse the input signature in MakeFlippedCompare so asymmetric arguments dispatch correctly to flipped comparison kernels.

* Closes: apache#55171
@Awesome06

Awesome06 commented Oct 25, 2026 •

Copy link
Copy Markdown
Author

@dmitry-chirkov-dremio Good catch on both the scalar copying and the array view descriptor allocation overhead. I've updated the patch to compare mixed (large_)utf8 and utf8_view (and binary variants) directly via ScalarBinary in both directions, reverted the CommonBinary promotion, and reversed the signatures in MakeFlippedCompare for less/less_equal.

Verified local benchmark parity across both matching and early-differing strings, and added tests covering bidirectional mixed comparisons and asymmetric ordering in scalar_compare_test.cc.

Copilot AI balanced review requested due to automatic review settings October 25, 2026 09:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

Comment thread cpp/src/arrow/compute/kernels/scalar_compare.cc Outdated
…lipped comparisons

In MakeFlippedCompare, only construct a reversed KernelSignature when input types differ (in_types[0] != in_types[1]). This ensures symmetric kernels retain their original signature and attached MatchConstraints (such as DecimalsHaveSameScale), preventing raw unscaled integer comparisons on decimals with differing scales. Also add regression tests for bound expressions and scalar comparisons with different decimal scales.
Copilot AI balanced review requested due to automatic review settings October 25, 2026 10:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@Awesome06

Copy link
Copy Markdown
Author

I have updated MakeFlippedCompare to only rebuild the kernel signature when argument types differ (in_types[0] != in_types[1]), preserving the decimal same-scale constraint on symmetric kernels.

I've also added regression tests covering < and <= comparisons across differing decimal scales in both expression_test.cc (for bound expressions) and scalar_compare_test.cc.

…als and string view boundary

Extend TestIssue54668 to cover: 1) Negative decimal comparisons with differing scales, ensuring signed two's complement unscaled integers (-100 < -90) do not falsely return true for -1.00 < -9.0. 2) StringView 12-byte inline vs 13-byte out-of-line payload boundary transitions. 3) Shared 4-byte prefix matching with differing out-of-line payloads (prefix collision). 4) Bound expressions with literals on the LHS ('banana' < view_col) testing expression simplifier flipping.
Copilot AI balanced review requested due to automatic review settings October 25, 2026 10:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #54668 has been automatically assigned in GitHub to PR creator.

…scalars, embedded nulls, and dictionary views

Expand TestBinaryViewCompareKernel and TestIssue54668 with additional edge-case test coverage for cross-type comparisons:
- Asymmetric slice start offsets between string and string_view arrays
- Explicit null scalar instances against string_view arrays
- Embedded null bytes (\0) in out-of-line (>12B) variadic buffer views
- Symmetric dictionary-encoded string vs. utf8_view dispatch
Copilot AI balanced review requested due to automatic review settings October 25, 2026 15:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@dmitry-chirkov-dremio

Copy link
Copy Markdown
Contributor

Verified local benchmark parity across both matching and early-differing strings, and added tests covering bidirectional mixed comparisons and asymmetric ordering in scalar_compare_test.cc.

Were you able to replicate performance results I observed?

@Awesome06

Awesome06 commented Oct 25, 2026 •

Copy link
Copy Markdown
Author

Replicated the benchmarks locally on Windows x86_64 (MSVC 2019), and there is a clear, substantial improvement across both execution throughput and memory consumption with the direct mixed approach!

Times are microseconds per call:

Input Same type (utf8_view) CommonBinary promotion Direct mixed (268f038) Speedup
8 B, equal scalars 2.88 3.25 2.92 1.11x faster
4 KiB, equal scalars 3.02 3.44 3.07 1.12x faster
1 MiB, equal scalars 44.26 88.80 45.49 1.95x faster
1 MiB, first byte differs 2.81 44.06 2.90 15.2x faster
131,072 x 8 B, equal arrays 899.77 2098.38 613.50 3.40x faster
131,072 x 64 B, equal arrays 927.11 2236.50 847.05 2.60x faster
4,096 x 4 KiB, equal arrays 687.33 1176.74 1125.24 1.05x faster

Beyond the 2.6x–3.4x speedup on arrays compared to the CommonBinary promotion, for 131,072 rows, Arrow pool allocation drops from ~2.02 MiB down to just 16 KiB by avoiding temporary view descriptors entirely.

On small scalars, fixed kernel dispatch overhead dominates the runtime, while on large payloads (e.g. 4,096 x 4 KiB = 16 MiB), execution is bottlenecked by memcmp throughput scanning raw string data rather than descriptor allocation overhead.

@dmitry-chirkov-dremio dmitry-chirkov-dremio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Fits Arrow C++ compute’s existing design.

p.s.
I think we can gain a bit more performance with prefix-aware comparison though. Could be files as a separate issue to follow up later.

I tried keeping the raw view descriptors for equal/not_equal, so length and the stored 4-byte prefix can reject a pair before reading the view payload. For view/view arrays, reuse util::EqualBinaryView; for mixed offset/view arrays, compare the offset string’s length and first four bytes with the descriptor, then compare the full payload if they match. Always using this path helped early mismatches but slowed some equal strings. The better prototype samples 32 evenly spaced valid pairs per batch: below 4,096 rows, keep the original loop; otherwise use the prefix path when at least 4 samples reject for mixed types, or 8 for view/view, also counting inline view values as opportunities. Keep the original executor in a separate ARROW_NOINLINE fallback. Leave ordered comparisons unchanged. This gave roughly 1.8–2× on tested workloads with 50% early mismatches, but sampling can choose badly, so these thresholds need broader benchmarks before adoption.

@Awesome06

Copy link
Copy Markdown
Author

@dmitry-chirkov-dremio Thanks for the review, approval, and detailed prefix benchmark breakdown!

That early-rejection check makes a lot of sense for high-cardinality mismatch cases. I agree with keeping it separate from this PR to avoid holding up the core mixed kernels with sampling heuristics. I'll open a follow-up issue so we can track and benchmark the prefix-aware fast path.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants