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

   # Which issue does this PR close?
   
   - Part of #11421.
   
   # Rationale for this change
   
   See #11421. This is the first divisor PR in that epic, so it settles the API 
that the `i128` and `i256` divisors, the sealed trait and the kernel changes 
build on.
   
   # What changes are included in this PR?
   
   This PR adds `DivisorU32`, `DivisorI32`, `DivisorU64` and `DivisorI64` to 
`arrow-buffer`, exported at the crate root next to `i256`. No kernel uses them 
yet.
   
   - `new(divisor)` returns `None` for a zero divisor.
   - `div_rem(n)` returns `(n / divisor, n % divisor)`. For the signed types 
that is `(n.wrapping_div(divisor), n.wrapping_rem(divisor))`, so `MIN / -1` 
returns `(MIN, 0)`.
   - `checked_div_rem(n)` on the signed types returns `None` only for `MIN / 
-1`, the same as `checked_div` and `checked_rem`. The unsigned types don't have 
it, because it could never return `None` there.
   
   `new` computes the multiplier from figure 4.1 of [Granlund and Montgomery 
(1994)](https://gmplib.org/~tege/divcnst-pldi94.pdf). A divisor that is a power 
of two uses a shift and a mask instead. That choice is a field of the divisor, 
so `div_rem` branches on it, but the branch doesn't depend on the dividend, and 
LLVM tests it once before the loop and emits a separate loop for each case. The 
signed types divide the magnitudes with the unsigned type and apply the signs 
with `(x ^ s) - s`. Writing the sign handling as `if` expressions made LLVM 
compute the division for both `n` and `-n` in each iteration and select one, 
which made `DivisorI32` slower than `/`.
   
   A macro generates the two unsigned types and another generates the two 
signed types, so each method is written once.
   
   # Are these changes tested?
   
   The unit tests compare `div_rem` with `/` and `%` for the unsigned types, 
and `div_rem` and `checked_div_rem` with `wrapping_div`, `wrapping_rem`, 
`checked_div` and `checked_rem` for the signed types. For the unsigned types 
the divisors are 1 to 2000, every power of two and its neighbors, `10^k - 1`, 
`10^k` and `10^k + 1`, `MAX`, `MAX - 1` and random values, and the dividends 
are 0, 1, 2, `MAX`, `MAX - 1`, `2^(N-1)` and its neighbors, and random values. 
For the signed types the divisors are magnitudes 1 to 1000, powers of two and 
powers of ten with their neighbors, and random values, each with both signs, 
plus `MIN`, `MAX`, -1 and 1. Their dividends are 0, 1, -1, `MIN`, `MIN + 1`, 
`MAX`, and random values with both signs. Each divisor is also tested with the 
dividends next to it and with seven times it. Each public method has a doctest.
   
   The new `divisor` benchmark divides 8192 random values by 13 and by 16 with 
`/` and `%` and with `div_rem`. On an Apple M5 Max, in one run:
   
   <img width="1600" height="1380" alt="divisor-types" 
src="https://github.com/user-attachments/assets/9ca324ac-5ed5-46f2-941f-d9a07fa7f718";
 />
   
   <details><summary>Numbers behind the chart</summary>
   
   | Case | `/` or `%` | `div_rem` | Speedup |
   |---|---|---|---|
   | `u32` div 13 | 3.68 us | 0.71 us | 5.1x |
   | `u32` div 16 | 3.65 us | 0.29 us | 12.7x |
   | `u32` rem 13 | 3.67 us | 0.84 us | 4.4x |
   | `u32` rem 16 | 3.68 us | 0.28 us | 13.3x |
   | `i32` div 13 | 3.76 us | 1.31 us | 2.9x |
   | `i32` div 16 | 3.83 us | 0.72 us | 5.3x |
   | `i32` rem 13 | 3.81 us | 1.33 us | 2.9x |
   | `i32` rem 16 | 3.87 us | 0.61 us | 6.4x |
   | `u64` div 13 | 3.69 us | 1.83 us | 2.0x |
   | `u64` div 16 | 3.69 us | 0.64 us | 5.7x |
   | `u64` rem 13 | 3.74 us | 2.84 us | 1.3x |
   | `u64` rem 16 | 3.77 us | 0.64 us | 5.9x |
   | `i64` div 13 | 4.08 us | 2.75 us | 1.5x |
   | `i64` div 16 | 4.06 us | 1.53 us | 2.7x |
   | `i64` rem 13 | 3.98 us | 4.80 us | 0.8x |
   | `i64` rem 16 | 4.02 us | 3.02 us | 1.3x |
   
   </details>
   
   The 32-bit loops compile to NEON code, because their 32x32 to 64-bit 
multiply has vector forms. The 64-bit multiply-high has no NEON form, so the 
64-bit loops are scalar or use two lanes. `i64` remainder by 13 is slower than 
`%` on this machine, where `%` takes 0.49 ns per value. The integer kernel PR 
will decide per type which kernels use the divisor, with numbers from the 
benchmark bot on both arm64 and x86_64.
   
   # Are there any significant user-facing changes?
   
   Four new public types in `arrow-buffer`. Nothing existing changes.
   
   ## AI Disclosure
   
   I wrote this PR with help from Claude but understand and support the core 
approach, implementation, and test coverage.
   


-- 
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