Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 29.4k
[SPARK-45599][CORE] Use object equality in OpenHashSet#45036
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
905e1740d5c2ce2bfc605029045521cff3b16d9aef0c47d4c760cb497ac5e3e5bbde8337647053b5118d6b4f0919fe698b60f698a15a091ca76629c3a0b194f0764c4File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -249,4 +249,34 @@ class OpenHashMapSuite extends SparkFunSuite with Matchers { | ||
| map(null) = null | ||
| assert(map.get(null) === Some(null)) | ||
| } | ||
| test("SPARK-45599: 0.0 and -0.0 should count distinctly; NaNs should count together") { | ||
| // Exactly these elements provided in roughly this order trigger a condition where lookups of | ||
| // 0.0 and -0.0 in the bitset happen to collide, causing their counts to be merged incorrectly | ||
| // and inconsistently if `==` is used to check for key equality. | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shall we mention the NaN behavior as well? All NaN values are all the same. ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tweaked the test name. Is that what you had in mind? This comment explains why we need exactly the following elements to trigger the 0.0/-0.0 miscount. It doesn't always happen (which is part of what kept this bug hidden for so long). | ||
| val spark45599Repro = Seq( | ||
| Double.NaN, | ||
| 2.0, | ||
| 168.0, | ||
| Double.NaN, | ||
| Double.NaN, | ||
| -0.0, | ||
| 153.0, | ||
| 0.0 | ||
| ) | ||
| val map1 = new OpenHashMap[Double, Int]() | ||
| spark45599Repro.foreach(map1.changeValue(_, 1, {_ + 1})) | ||
| assert(map1(0.0) == 1) | ||
| assert(map1(-0.0) == 1) | ||
| assert(map1(Double.NaN) == 3) | ||
| val map2 = new OpenHashMap[Double, Int]() | ||
| // Simply changing the order in which the elements are added to the map should not change the | ||
| // counts for 0.0 and -0.0. | ||
| spark45599Repro.reverse.foreach(map2.changeValue(_, 1, {_ + 1})) | ||
| assert(map2(0.0) == 1) | ||
| assert(map2(-0.0) == 1) | ||
nchammas marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| assert(map2(Double.NaN) == 3) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -269,4 +269,43 @@ class OpenHashSetSuite extends SparkFunSuite with Matchers { | ||||||
| assert(pos1 == pos2) | ||||||
| } | ||||||
| } | ||||||
| test("SPARK-45599: 0.0 and -0.0 are equal but not the same") { | ||||||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bit tricky and it's better if we can find a reference system that defines this semantic. In Spark, Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, probably make sense if we just fix this particular issue as ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
scala>importjava.util.HashSetimportjava.util.HashSet
scala>valh=newHashSet[Double]()
valh: java.util.HashSet[Double] = []
scala> h.add(0.0)
valres0:Boolean=true
scala> h.add(-0.0)
valres1:Boolean=true
scala> h.size()
valres2:Int=2The doc for HashSet.add states:
In other words, So this PR brings ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This PR does not change this behavior. I noticed, however, that we do not have any tests currently to check that -0.0 is normalized and grouped as you describe, so I went ahead and added such a test in 2bfc605. Does this address your concern? Or are you suggesting that we should normalize -0.0 to 0.0 across the board?
| ||||||
| * storage for four primitive types (Long, Int, Double, and Float). It is much faster than Java's | |
| * standard HashSet while incurring much less memory overhead. This can serve as building blocks |
If that's true, then we should perhaps add property based tests to ensure alignment between the two implementations, but I'll leave that as a potential future improvement.
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.
Spark's OpenHashSet does not have to match java.util.HashSet. What matters is the SQL semantic. Can you highlight which functions/operators are using this OpenHashSet and what is the impact of this change to the SQL semantic?
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.
Spark's
OpenHashSetdoes not have to matchjava.util.HashSet. What matters is the SQL semantic.
Whether or not OpenHashSet matches java.util.HashSet, I want to emphasize for the record that OpenHashSet mishandles 0.0/-0.0 and NaN. Its behavior is simply incorrect. Thesetests fail on master in ways that can only be described as bugs, regardless of whatever SQL semantics we want to preserve.
This comment explains the root cause. Basically, it is a mistake to combine hash code-based lookups with cooperative equality, at least in the way we are doing it in OpenHashSet.
But I understand what you are saying. Fixing bugs in OpenHashSet doesn't help us if it also breaks users' SQL.
Can you highlight which functions/operators are using this
OpenHashSet
I've updated the PR description with a summary of what uses OpenHashSet.
As a side note, I believe that if we accept the change proposed here, we should be able to eliminate SQLOpenHashSet. SQLOpenHashSet was created specifically to work around the bugs in OpenHashSet that we are addressing in this PR. See #33955 and #33993.
and what is the impact of this change to the SQL semantic?
I've updated the PR description with a diff of what tests pass or fail on master vs. this branch. Please take a look and let me know if you think we need any more tests. I know we are touching a sensitive code path and I appreciate the need for caution.
Uh oh!
There was an error while loading. Please reload this page.
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 we actually waste space in OpenHashSet to store all the NaN values?
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 sir. On master, this is the actual behavior of OpenHashSet:
// ...OpenHashSet$mcD$sp@21b327e6 did not contain NaN
assert(set.contains(Double.NaN))
// ...OpenHashSet$mcD$sp@1f09db1e had size 2 instead of expected size 1
assert(set.size ==1)Every NaN will get its own entry in OpenHashSet on master. So if we add 1,000,000 NaNs to the set, NaN will have 1,000,000 entries in there. And .contains() will still return false. :D
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 was told that in scala
==is the same asequals, buteqis a different operator. I need to refresh my knowledge now :)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, the differences are subtle:
There is a long discussion on the Scala forums from 2017 about this difference and some of the problems it causes:
Can we get rid of cooperative equality?