-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[stdlib] Set, Dictionary: Enable per-instance hash seeds #19533
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
Conversation
@swift-ci please test |
We may or may not have benchmarks that trigger this issue. @swift-ci please smoke benchmark |
Build comment file:Performance: -O
Code size: -O
Performance: -Osize
Code size: -Osize
Performance: -Onone
How to read the dataThe tables contain differences in performance which are larger than 8% and differences in code size which are larger than 1%.If you see any unexpected regressions, you should consider fixing the regressions before you merge the PR. Noise: Sometimes the performance results (not code size!) contain false alarms. Unexpected regressions which are marked with '(?)' are probably noise. If you see regressions which you cannot explain you can try to run the benchmarks again. If regressions still show up, please consult with the performance team (@eeckstein). Hardware Overview
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
stdlib/public/core/HashTable.swift
Outdated
// When we're using deterministic hashing, the scale value as the seed is | ||
// still allowed, and it covers most cases. (Unfortunately some operations | ||
// that merge two similar-sized hash tables will still be quadratic.) | ||
return scale |
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.
If we wanted to get rid of quadratic behavior in the deterministic hashing case, we would use a (per-thread?) atomic counter here, I guess, though that's not quite strong enough for some cases? Do we care?
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.
That's a really clever idea. Not sure it's worth the trouble though -- deterministic hashing is intended as a debugging/testing tool, and it's supposed to never be enabled in production.
What if we used an atomic counter for all cases?
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.
LGTM. This is a nice improvement for a hard-to-notice perf problem.
Ugh, that's funny -- it looks like this actually breaks the package manager. 😮 |
No, that was my fault -- I broke This was one of those rare bugs that don't happen with deterministic hashing, which is why it wasn't caught by the test suite. |
cc46dea
to
2381183
Compare
However, using an atomic counter as the seed would've definitely caught the issue. One more reason to add one? |
eaba464
to
09c24f5
Compare
@swift-ci please test |
This comment has been minimized.
This comment has been minimized.
@swift-ci please test source compatibility |
This comment has been minimized.
This comment has been minimized.
Build failed |
09c24f5
to
603ded3
Compare
@swift-ci please test |
Build failed |
Hm, there could be a correctness issue with one of the high-level operations. Weird that it only happens on Linux, though. |
@swift-ci please test |
Build failed |
The performance of operations merging two similar-sized sets or dictionaries is currently quadratic. Enabling per-instance seeds makes most of these linear, which is what most people expect. This affects Set.union, Set.symmetricDifference and similar operations, including the for-loop based forms that people write manually. The only remaining quadratic cases are when two sets/dictionaries are merged that are mutated copy-on-write copies of the same original instance. (We can’t change the hash seed without a full rehash,
603ded3
to
08e0ad6
Compare
@swift-ci please test |
This comment has been minimized.
This comment has been minimized.
Build failed |
I suspect the Linux package manifest caching test failures might be caused by an unrelated issue with the filesystem timestamp resolution -- except they seem to be reliably triggered by this change somehow. It's all somewhat bizarre; perhaps there's a Foundation issue? @aciidb0mb3r, can you think of a reason why manifest caching would fail when the order of elements in a Set/Dictionary changes between instances? |
hmm, nope. It may be something very subtle somewhere. Do you want me to investigate? |
Thanks! I have a couple of ideas to narrow it down a bit — I’ll let you know if I hit a wall. 👍 |
The failure is because The Package Manager uses Fixing this is easy in SPM -- just encode keys with a different encoder. But this shouldn't be necessary -- |
@lorentey JSONEncoder does have a .sortedKeys option for output formatting. Do you think that'll fix the issue or is that broken on linux? |
@aciidb0mb3r Oh, nice, that should fix it! I did not know about that option. 👌 I submitted swiftlang/swift-package-manager#1802 to verify that enabling it resolves the test failure. |
Please test with the following PR: |
@swift-ci test and merge |
Set and Dictionary operations that copy elements from two hash tables of the same size are still quadratic with certain load factors.
This makes simple-looking loops like this extremely slow: (adapted from stdlib’s current implementation for union)
This is because
a
andb
above share the same hash seed, both being sets of the same size. This causes the loop to fill the hash table ofa
extremely unevenly; the initial part ofa
’s hash table gets a load factor well over 100% — essentially converting it into an unsorted array until the overall alpha increases enough to trigger a storage resize and full rehash.This performance pitfall can be eliminated by choosing a new hash seed for every new Set/Dictionary, so we should do that. (Except for copy-on-write copies, of course.) This makes the above code 11 times faster:
See below: log-log chart of amortized per-element performance of
Set.union
for two non-overlapping sets of the same size, before & after this change.rdar://problem/44760778