-
Notifications
You must be signed in to change notification settings - Fork 3.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
GH-35273: [C++] Add integer round kernels #36289
Conversation
CC @EpsilonPrime do you think you could take a look? |
Hi, I've been looking through the PR and it looks pretty good so far. I'm going to take another pass to see if any of this makes it harder to implement round for Decimal types (I suspect not but it's worth checking). In the meantime could you add benchmarks for the new kernels? Thanks! |
Actually it looks like the benchmarks are already there. I'll run them locally to see what kind of improvement there is. I suspect there will be a noticeable improvement from this PR because the expensive conversion to float won't need to occur. |
https://gist.github.com/js8544/8471c3106bbaff473fb7bddf4c56b4de I just ran it locally and here's the result. |
docs/source/cpp/compute.rst
Outdated
rounding to the nearest multiple of 100 (zeroing the ones and tens digits). | ||
Default value of ``multiple`` is 1 which rounds to the nearest integer. | ||
multiple has to be a positive value and can be casted to input type. | ||
For example, 100 corresponds to ounding to the nearest multiple of 100 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ounding -> rounding
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
docs/source/cpp/compute.rst
Outdated
which rounds to the nearest integer. For integer inputs a non-negative | ||
``ndigits`` value is ignored and the input is returned unchanged. For integer | ||
inputs, if ``-ndigits`` is larger than the maximum number of digits the | ||
input type can hold, it is truncated to the maximum digit. For example, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maximum ndigits that the type can handle
docs/source/cpp/compute.rst
Outdated
``ndigits`` value is ignored and the input is returned unchanged. For integer | ||
inputs, if ``-ndigits`` is larger than the maximum number of digits the | ||
input type can hold, it is truncated to the maximum digit. For example, | ||
``round([123], ndigits=-4, round_mode=DOWN)`` returns [100] for ``int8`` type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are three potential ways of handling this particular behavior (none of which are specified in the Substrait specification):
- reject the operation as invalid
- using the provided value (always returning overflow)
- using the provided value (always returning max value)
- fixing the value and proceeding
I am going to check other engines to see what they do in this particular case but the precedent within arrow seems to be to reject the operation as an overflow would occur (RoundToMultiple does this).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that we should align with RoundToMultiple. I changed the behavior to rejecting the operation when -ndigits is too large.
docs/source/cpp/compute.rst
Outdated
``ndigits`` value is ignored and the input is returned unchanged. For integer | ||
inputs, if ``-ndigits`` is larger than the maximum number of digits the | ||
input type can hold, it is truncated to the maximum digit. For example, | ||
``round([123], ndigits=-4, round_mode=DOWN)`` returns [100] for ``int8`` type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(as the ndigits value is silently reduced to -2)
I ran the benchmarks as well (locally on an M1 Macbook Pro): The summary version is:
https://gist.github.com/EpsilonPrime/658c90020a5964064e803cfb7e4761b2 |
@EpsilonPrime Sorry for the delay, I've updated the PR according to your suggestions. Please re-review it when it's convenient for you. Thanks! |
The changes look great to me. Thanks for implementing this! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you quickly comment on whether you agree this is a breaking change or not? Then I think we can approve this PR.
template <typename T> | ||
static enable_if_integer_value<T> Pow10(int64_t power) { | ||
DCHECK_GE(power, 0); | ||
DCHECK_LE(power, std::numeric_limits<T>::digits10); | ||
static constexpr uint64_t lut[] = { | ||
Pow10Struct<0>::value, Pow10Struct<1>::value, Pow10Struct<2>::value, | ||
Pow10Struct<3>::value, Pow10Struct<4>::value, Pow10Struct<5>::value, | ||
Pow10Struct<6>::value, Pow10Struct<7>::value, Pow10Struct<8>::value, | ||
Pow10Struct<9>::value, Pow10Struct<10>::value, Pow10Struct<11>::value, | ||
Pow10Struct<12>::value, Pow10Struct<13>::value, Pow10Struct<14>::value, | ||
Pow10Struct<15>::value, Pow10Struct<16>::value, Pow10Struct<17>::value, | ||
Pow10Struct<18>::value, Pow10Struct<19>::value}; | ||
|
||
return static_cast<T>(lut[power]); | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is clever, though I don't know if it is more readable than something like https://github.com/apache/arrow/blob/main/cpp/src/arrow/util/decimal_internal.h#L36-L58
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does seem to be an overkill indeed. I've changed it to the simpler way.
@@ -563,30 +563,32 @@ representation based on the rounding criterion. | |||
+-------------------+------------+-------------+-------------------------+----------------------------------+--------+ | |||
| floor | Unary | Numeric | Float32/Float64/Decimal | | | | |||
+-------------------+------------+-------------+-------------------------+----------------------------------+--------+ | |||
| round | Unary | Numeric | Float32/Float64/Decimal | :struct:`RoundOptions` | (1)(2) | | |||
| round | Unary | Numeric | Input Type | :struct:`RoundOptions` | (1)(2) | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I think this is technically a breaking change right?
Before, if we had something like:
x = pa.array([1, 2], pa.int32())
y = pc.round(x)
Then y
would be a double array. Now, y
will be an int32
array. I think this is correct and the old behavior was unintentional so I think it is an ok breaking change. Still, we should make sure to mark the PR as a breaking change if my understanding is correct so that we document it as such in the release notes.
CC @jorisvandenbossche for second opinion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, it should be a breaking change. @jorisvandenbossche could you please confirm if it's acceptable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, fully agreed with the summary of Weston above: this was unintentional behaviour (because of automatic casting for numeric types), and it's fine to correct this with a breaking change.
Co-authored-by: Weston Pace <[email protected]>
@@ -563,30 +563,32 @@ representation based on the rounding criterion. | |||
+-------------------+------------+-------------+-------------------------+----------------------------------+--------+ | |||
| floor | Unary | Numeric | Float32/Float64/Decimal | | | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would have expected that "floor" kernel is a small wrapper around "round" with a specific RoundOptions value. If the output type of "round" changes, that doesn't also change "floor"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really. Because I didn't add floor kernels for integer types, floor(int) would still be dispatched to floor(float) kernels, and thus calling the round functions for floats.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But now that Round itself supports rounding integers, how hard would it be to expand the floor/trunc registration to integer types as well?
(like MakeUnaryRoundFunction
was updated to loop through all NumericTypes instead of just float32/float64)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it at all useful to expose those functions for integer inputs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to have consistent behaviour with the generic round (i.e. always preserve the input type). But that alone is maybe not worth it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although it might also simplify things, given that the RoundIntegerToFloatingPointFunction
to explicitly cast int to float which is still used for floor/trunc/ceil could then be removed (but didn't check the code in detail)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#36786 I've created another issue for this.
I think this PR is ready to be merged. CI failures are unrelated. @pitrou Would you mind merging this? Thanks! |
// Test different rounding mode | ||
// skip int8 because of its small range | ||
if constexpr (!std::is_same_v<TypeParam, Int8Type>) { | ||
std::string values("[0, 1, -13, -50, 115, -176, 200, 250]"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you test with values on which HALF_TOWARDS_ZERO
and HALF_TO_EVEN
would actually differ?
For example:
std::string values("[0, 1, -13, -50, 115, -176, 200, 250]"); | |
std::string values("[0, 1, -13, 115, -150, -176, 200, 250]"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kept -50 and added -150, so that no two options result in identical results. (Changing -50 to -150 would make HALF_TO_ODD and HALF_UP the same).
} | ||
|
||
// An overly large ndigits would cause an error | ||
if constexpr (std::is_same_v<TypeParam, Int8Type>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why only int8? 100 digits should be out of range for every integer type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. The if constexpr is removed.
// Test different rounding mode | ||
// skip uint8 because of its small range | ||
if constexpr (!std::is_same_v<TypeParam, UInt8Type>) { | ||
std::string values("[0, 1, 13, 50, 115, 176, 200, 250]"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(same comments as above here)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
+-------------------+------------+-------------+-------------------------+----------------------------------+--------+ | ||
| round_to_multiple | Unary | Numeric | Float32/Float64/Decimal | :struct:`RoundToMultipleOptions` | (1)(3) | | ||
| round_to_multiple | Unary | Numeric | Input Type | :struct:`RoundToMultipleOptions` | (1)(3) | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is round_binary
not mentioned in this table? If so, can you add it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
friendly ping :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the update, just two nits
docs/source/cpp/compute.rst
Outdated
@@ -627,8 +635,8 @@ The example values are given for default values of ``ndigits`` and ``multiple``. | |||
+-----------------------+--------------------------------------------------------------+---------------------------+ | |||
|
|||
The following table gives examples of how ``ndigits`` (for the ``round`` | |||
function) and ``multiple`` (for ``round_to_multiple``) influence the operance | |||
performed, respectively. | |||
function) and ``multiple`` (for ``round_to_multiple`` and ``round_binary``) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, the second input for round_binary
is equivalent to ndigits
, not multiple
, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My bad, fixed.
Co-authored-by: Antoine Pitrou <[email protected]>
After merging your PR, Conbench analyzed the 6 benchmarking runs that have been run so far on merge-commit 7c8f398. There were 7 benchmark results indicating a performance regression:
The full Conbench report has more details. |
### Rationale for this change Currently `round` casts integers to floats which causes undesired behavior. ### What changes are included in this PR? Add round kernels for integer types. ### Are these changes tested? Yes. ### Are there any user-facing changes? No. * Closes: apache#35273 Lead-authored-by: Jin Shang <[email protected]> Co-authored-by: Antoine Pitrou <[email protected]> Co-authored-by: Weston Pace <[email protected]> Signed-off-by: Antoine Pitrou <[email protected]>
Rationale for this change
Currently
round
casts integers to floats which causes undesired behavior.What changes are included in this PR?
Add round kernels for integer types.
Are these changes tested?
Yes.
Are there any user-facing changes?
No.