Abhisheklearn12 opened a new pull request, #10436:
URL: https://github.com/apache/arrow-rs/pull/10436

   # Which issue does this PR close?
   
   <!--
   We generally require a GitHub issue to be filed for all bug fixes and 
enhancements and this helps us generate change logs for our releases. You can 
link an issue to this PR using the GitHub syntax.
   -->
   
   - Closes #8985
   
   Supersedes #9768, which I closed it implemented these fast paths ungated, 
and benchmarking showed that made things significantly slower. Details below.
   
   # Rationale for this change
   
   <!--
   Why are you proposing this change? If this is already explained clearly in 
the issue then this section is not needed.
   Explaining clearly why changes are proposed helps reviewers understand your 
changes and offer better suggestions for fixes.
   -->
     `dictionary_cast` can build a view array from a dictionary two ways:
   
     - (a) `unpack_dictionary` builds one view per dictionary *value*, then 
gathers with `take`
     - (b) `view_from_dict_values` builds one view per *row* directly against 
the values buffer
   
     The comment above (b) says (a) "incurs unnecessary data copy of the value 
buffer". That
     isn't the case. `impl From<&GenericByteArray> for GenericByteViewArray` 
reuses the buffer
     (it only falls back to copying when the values exceed the `u32` offset a 
view can hold),
     and `take_byte_view` passes `data_buffers().to_vec()` through untouched. 
Neither step
     copies value data, so there was no copy to remove.
   
     What the two actually trade is a bandwidth-bound `u128` gather in `take` 
against a
     per-row `make_view` that reads each row's payload out of the values 
buffer. The gather
     wins once rows reach roughly 0.6x the dictionary size  so (b) is a large 
pessimisation in
     the common case, and only pays off when the dictionary is substantially 
larger than the
     array is long, where (a) spends most of its time building views no row 
references.
   
     This means the existing `Utf8 -> Utf8View` and `Binary -> BinaryView` fast 
paths on main
     are currently **5-8x slower** than `unpack_dictionary` at typical batch 
shapes.
   
   # What changes are included in this PR?
   
   <!--
   There is no need to duplicate the description in the issue here but it is 
sometimes worth providing a summary of the individual changes in this PR.
   -->
     - Gate the direct path on `keys.len() < values.len() / 2`; everything else 
falls through
       to `unpack_dictionary`. The measured crossover is near `rows ≈ 0.6 * 
values`; the gate
       switches short of it, at `rows = values / 2`, where the direct path is 
still ahead by
       15-30%. That margin covers the crossover moving with cache size or 
microarchitecture.
       Being wrong in that direction only forgoes a win; being wrong the other 
way is a
       regression on the common case.
     - Extend the direct path to the remaining combinations #8985 asks for:
       `LargeUtf8`/`LargeBinary` -> `Utf8View`/`BinaryView`, and the `Utf8` <-> 
`Binary` cross
       casts  with the offset-fit check and UTF-8 validation the issue calls 
out.
     - Fix `view_from_dict_values` dropping null dictionary *values*: they 
became empty strings
       rather than nulls, where `unpack_dictionary` and `impl 
From<&GenericByteArray>` both
       produce nulls.
     - Bounds-check the dictionary key before indexing the offsets, turning an 
out-of-range key
       from undefined behaviour into an error. The gate confines this loop to 
short row counts,
       so it is within noise when the dictionary holds no nulls. When the 
dictionary does hold
       nulls, the null check costs 25-40% on the direct path  an unavoidable 
random bitmap
       lookup per row, and the price of emitting nulls rather than the empty 
strings the
       previous code produced.
   
   
   ## Benchmarks
   
   Existing fast paths on main:
   
   | cast | rows | dict values | main | this PR | change |
   |---|---|---|---|---|---|
   | `Utf8->Utf8View` | 8,192 | 100 | 26.3 µs | 3.4 µs | **-87.2%** |
   | `Utf8->Utf8View` | 1,000,000 | 1,000 | 3188.9 µs | 554.6 µs | **-82.6%** |
   | `Binary->BinaryView` | 8,192 | 100 | 24.9 µs | 3.3 µs | **-86.6%** |
   | `Binary->BinaryView` | 1,000,000 | 1,000 | 3296.0 µs | 593.6 µs | 
**-82.0%** |
   
   New arms, sparse shapes (dictionary larger than the array):
   
   | cast | rows | dict values | main | this PR | change |
   |---|---|---|---|---|---|
   | `LargeUtf8->Utf8View` | 1,000 | 100,000 | 271.2 µs | 3.9 µs | **-98.6%** |
   | `LargeUtf8->BinaryView` | 10,000 | 1,000,000 | 15752.5 µs | 78.6 µs | 
**-99.5%** |
   | `LargeBinary->BinaryView` | 1,000 | 100,000 | 276.1 µs | 3.9 µs | 
**-98.6%** |
   | `Utf8->BinaryView` | 10,000 | 1,000,000 | 3005.8 µs | 73.1 µs | **-97.6%** 
|
   | `Binary->Utf8View` | 1,000 | 100,000 | 351.5 µs | 88.6 µs | **-74.8%** |
   | `LargeBinary->Utf8View` | 10,000 | 1,000,000 | 4570.7 µs | 1713.9 µs | 
**-62.5%** |
   
   (`Binary`/`LargeBinary -> Utf8View` gain less because UTF-8 validation of 
the dictionary
   values dominates.)
   
   Dense shapes on the new arms are unchanged, -2.9% to +1.2%  the gate routes 
them to
   `unpack_dictionary`. Worst cell across the whole matrix is +5.0%, on a shape 
where both
   versions take the direct path and the delta is the null and bounds checks 
above.
   
   <details>
   <summary>Method</summary>
   
   Both implementations were compiled into a single binary and timed in 
alternating rounds so
   thermal drift cancels out of the ratio. Cells whose code is identical in 
both versions come
   out at +0.0% to +1.2%, which bounds the method's noise; a two-run criterion 
comparison of
   those same cells reported up to +47% drift, which is why it wasn't used. 
Outputs are
   asserted equal per cell before timing. Measured on an i7-11700F; a second 
independent run
   reproduced every headline result within 3.2 points and produced the 
identical set of cells
   above 50%; the largest shift on any cell was 8.3 points, on a cell that is 
flat in both runs.
   
   
   </details>
   
   # Are these changes tested?
   
   <!--
   We typically require tests for all PRs in order to:
   1. Prevent the code from being accidentally broken by subsequent changes
   2. Serve as another way to document the expected behavior of the code
   
   If tests are not included in your PR, please explain why (for example, are 
they covered by existing tests)?
   
   If this PR claims a performance improvement, please include evidence such as 
benchmark results.
   -->
   Yes. Every arm is exercised through **both** implementations (row counts 
either side of the
   gate), covering null dictionary values, null keys, invalid UTF-8 under both 
`safe` and
   strict `CastOptions`. Results are asserted equal to the
   `unpack_dictionary` reference in each case.
   
   
   # Are there any user-facing changes?
     Casting `Dictionary<_, Utf8> -> Utf8View` and `Dictionary<_, Binary> -> 
BinaryView` becomes
     substantially faster at typical batch shapes  5-8x on the shapes 
benchmarked above.
   
     `Dictionary<_, LargeUtf8/LargeBinary> -> Utf8View/BinaryView` and the 
`Utf8` <-> `Binary`
     cross casts gain a fast path when the dictionary holds more than twice as 
many values as the
     array has rows. Outside that they behave as before, going through 
`unpack_dictionary`.
   
     Two behaviour changes, called out explicitly:
   
     1. A null dictionary *value* now casts to null rather than an empty string:
   
     ```rust
     // values ["aa", NULL, "cc"], keys [0, 1, 2]
     cast(&dict, &DataType::Utf8View)   // before: ["aa", "",   "cc"]
                                        // after:  ["aa", null, "cc"]
   ```
     2. An out-of-range dictionary key now returns an InvalidArgumentError 
instead of being
     undefined behaviour. Only reachable for a dictionary built without 
validation.
   
     Happy to split either into its own PR if you'd prefer this one stay purely 
about the fast path.
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to