-
Notifications
You must be signed in to change notification settings - Fork 784
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
Add overflow-checking variant for primitive arithmetic kernels and explicitly define overflow behavior #2643
Conversation
…erflow behavior for add
41b1637
to
02a2a80
Compare
9548eff
to
314886f
Compare
} | ||
|
||
/// This is similar to `math_checked_op` but just for divide op. | ||
fn math_checked_divide<LT, RT, F>( |
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.
What is the difference between this function and math_checked_divide_op
and why do we need both of them?
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.
Finally I hope we can just have one. Currently math_checked_divide_op
is used by divide_dyn
and I want to limit the range of change to primitive kernels only.
@@ -1042,15 +1196,18 @@ pub fn divide_dyn(left: &dyn Array, right: &dyn Array) -> Result<ArrayRef> { | |||
/// Perform `left / right` operation on two arrays without checking for division by zero. | |||
/// The result of dividing by zero follows normal floating point rules. |
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.
We should update the doc
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.
Do you mean "The result of dividing by zero follows normal floating point rules"? I think this is not changed? It will panic as usual.
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.
Do you mean "The result of dividing by zero follows normal floating point rules"?
Yes. But why follows normal floating point rules
here ? It seems like the function has supported other numeric types. (T: datatypes::ArrowNumericType
)
I think this is not changed? It will panic as usual.
Nope, but float will never panic. Divide by zero
in float type gives inf
or nan
. https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=972d301e807f9a6cfd2ba644b763b86c
Maybe it is better to doc the different behaviour between float
and other types
for float, dividing by zero follows the normal floating point rules,
for other numeric types, dividing be zero will panic,
...
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.
Oh, I see. Yea, let me update the doc.
I'm not a massive fan of forcing users to choose between slow but correct or fast but may have inconsistent behaviour, especially as having parallel kernels increases the likelihood of further divergent behaviour... Taking a step back I wonder if we could just define the overflow behaviour as wrapping, and use explicit wrapping_op to avoid signed overflow panics in non-release builds. This avoids runtime penalties, is consistent with how Rust handles overflow (unlike C++ signed integer overflow is actually defined, the debug panics are just "helpful"), and is what I at least would expect to occur. I'm not sure what SQL says on the topic of overflow, if anything, which may be relevant here? Perhaps @alamb knows? |
I think that you're talking about I was thinking if possibly to do same thing on
Hmm, is that something we want to have? Actually it may cause more difficulty for us to use this crate. As I mentioned below, we actually need two variants: overflow-checking (currently it could be by setting overflow-checks cargo flag) and overflow-as-null. I don't think defining the overflow behavior as wrapping is good idea. It sounds like a regression from current status. Users cannot choose overflow-checking behavior after that.
This is the next think we want to do. Actually it is more important to us. In Spark, once configured, it is allowed to have overflow. Overflowing value will be represented as NULL. That's being said, we can skip this change (overflow-checking variant/non-overflow-checking variant) if it cannot reach consensus. I just thought to have overflow/non-overflow variants like C++ is a good idea. We actually need an overflow-checking variant and an overflow-as-null variant. And the current arithmetic kernels are overflow-checking variant already (if overflow-checks is enabled by users). We just need to add an overflow-as-null variant. |
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.
Apologies I misread what this PR is doing. The important thing for me is the default kernels do not perform expensive overflow checking.
Having checked variants that return an error makes sense to me
Edit: It occurs to me that we still need to update the dyn kernels and the scalar kernels to be consistent, potentially adding checked variants, but that can easily be done as a follow up
Ok(Some(r)) | ||
} else { | ||
// Overflow | ||
Err(ArrowError::ComputeError("Overflow happened".to_string())) |
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.
Perhaps we could print the problematic values
Err(ArrowError::ComputeError("DivideByZero".to_string())) | ||
} else { | ||
// Overflow | ||
Err(ArrowError::ComputeError("Overflow happened".to_string())) |
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 here
T::Native: Add<Output = T::Native>, | ||
T::Native: ArrowNativeTypeOp, | ||
{ | ||
math_op(left, right, |a, b| a.wrapping_add_if_applied(b)) |
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.
👌
arrow/src/datatypes/native.rs
Outdated
macro_rules! native_type_op { | ||
($t:tt) => { | ||
impl ArrowNativeTypeOp for $t { | ||
fn checked_add_if_applied(self, rhs: Self) -> Option<Self> { |
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 the if_applied suffix? Is it because wrapping operations are not applicable to floating point types? Perhaps we could call them add and add_checked to match the kernels?
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.
We can just have one API name here as we cannot distinguish what native type we are calling from the kernels. That's why I make such API name. :)
I can rename it to add_checked
and add some comments to say for floating point types, it is simply add
without check.
arrow/src/datatypes/native.rs
Outdated
@@ -114,6 +115,98 @@ pub trait ArrowPrimitiveType: 'static { | |||
} | |||
} | |||
|
|||
/// Trait for ArrowNativeType to provide overflow-aware operations. | |||
pub trait ArrowNativeTypeOp: |
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.
Does this need to be public?
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.
No, I think. I will remove pub
in next commit.
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.
Ah, it has to be, as it is used as type bound in public APIs.
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.
You could use a trick of putting the trait in a private module, up to you
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 see. Sounds good. Let me update it.
arrow/src/datatypes/native.rs
Outdated
pub trait ArrowNativeTypeOp: | ||
ArrowNativeType | ||
+ Add<Output = Self> | ||
+ Sub<Output = Self> |
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 find the default impls a bit confusing here, it took me a bit to realise they're only used by the floating point types
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.
Yea, I will add some comments.
/// | ||
/// This detects overflow and returns an `Err` for that. For an non-overflow-checking variant, | ||
/// use `multiply` instead. | ||
pub fn multiply_check<T>( |
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.
pub fn multiply_check<T>( | |
pub fn multiply_checked<T>( |
arrow/src/datatypes/native.rs
Outdated
/// The APIs with `wrapping` suffix are the variant of non-overflow-checking. If overflow | ||
/// occurred, they will supposedly wrap around the boundary of the type. | ||
/// | ||
/// The APIs with `_check` suffix are the variant of overflow-checking which return `None` |
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.
_checked?
@@ -1013,18 +1171,21 @@ where | |||
/// Perform `left / right` operation on two arrays. If either left or right value is null | |||
/// then the result is also null. If any right hand value is zero then the result of this | |||
/// operation will be `Err(ArrowError::DivideByZero)`. | |||
pub fn divide<T>( | |||
/// | |||
/// When `simd` feature is not enabled. This detects overflow and returns an `Err` for that. |
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.
What happens when SIMD is enabled?
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.
Got signal: 8, SIGFPE: erroneous arithmetic operation
. This is original behavior.
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.
Interesting, that would imply rust division is always checked 🤔
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.
Yup - and LLVM cannot vectorize it correctly - https://rust.godbolt.org/z/T8eTGM8zn
@@ -1040,17 +1201,21 @@ pub fn divide_dyn(left: &dyn Array, right: &dyn Array) -> Result<ArrayRef> { | |||
} | |||
|
|||
/// Perform `left / right` operation on two arrays without checking for division by zero. | |||
/// The result of dividing by zero follows normal floating point rules. | |||
/// For floating point types, the result of dividing by zero follows normal floating point | |||
/// rules. For other numeric types, dividing by zero will panic, |
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 seems inconsistent with the other APIs, perhaps it should saturate instead
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, saturating_div
also panics when dividing by zero.
bf075f6
to
c930f85
Compare
Benchmark runs are scheduled for baseline = 4c1bb00 and contender = 6d86472. 6d86472 is a master commit associated with this PR. Results will be available as each benchmark for each run completes. |
Thanks for review. |
Which issue does this PR close?
Closes #2641.
Closes #2642.
Rationale for this change
What changes are included in this PR?
Are there any user-facing changes?