-
Notifications
You must be signed in to change notification settings - Fork 13.4k
[ADT] Update hash function of uint64_t for DenseMap #95734
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
@llvm/pr-subscribers-llvm-adt Author: Chuanqi Xu (ChuanqiXu9) Changes(Background: See the comment: It looks like the hash function for 64bits integers are not very good:
Since the result is truncated to 32 bits. It looks like the higher 32 bits won't contribute to the result. So that Then we may meet a lot collisions in such cases. I feel it should generally good to include higher 32 bits for hashing functions. Full diff: https://github.com/llvm/llvm-project/pull/95734.diff 1 Files Affected:
diff --git a/llvm/include/llvm/ADT/DenseMapInfo.h b/llvm/include/llvm/ADT/DenseMapInfo.h
index 5b7dce7b53c62..61869d8e7fbb0 100644
--- a/llvm/include/llvm/ADT/DenseMapInfo.h
+++ b/llvm/include/llvm/ADT/DenseMapInfo.h
@@ -151,7 +151,7 @@ template<> struct DenseMapInfo<unsigned long long> {
static inline unsigned long long getTombstoneKey() { return ~0ULL - 1ULL; }
static unsigned getHashValue(const unsigned long long& Val) {
- return (unsigned)(Val * 37ULL);
+ return DenseMapInfo<unsigned>(Val) ^ DenseMapInfo<unsigned>(Val >> 32);
}
static bool isEqual(const unsigned long long& LHS,
|
llvm/include/llvm/ADT/DenseMapInfo.h
Outdated
@@ -151,7 +151,7 @@ template<> struct DenseMapInfo<unsigned long long> { | |||
static inline unsigned long long getTombstoneKey() { return ~0ULL - 1ULL; } | |||
|
|||
static unsigned getHashValue(const unsigned long long& Val) { | |||
return (unsigned)(Val * 37ULL); | |||
return DenseMapInfo<unsigned>(Val) ^ DenseMapInfo<unsigned>(Val >> 32); |
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 we just use some code from ADT/Hashing.h
? It seems to contain some well-mixing routines. Seems like something like llvm::hash_value(Val)
would be enough.
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 looks good too. But I am not a hashing expert so I don't have an opinion here. Let's see what others propose.
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 concur with the suggestion to use llvm::hash_value from ADT/Hashing.h. It implements a murmur-like algorithm, which mixes bits enough.
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.
While I think that DenseMaps hash functions could use some overhaul, I don't think that switching to Hashing.h in this one place would be appropriate. If we want to do that, we should do so for all the DenseMapInfo hashes, after properly analyzing the cost of the more expensive hash vs the benefit of better hash distribution.
We may also have to refactor Hashing.h to reduce the build-time overhead for places that like this that don't need the full infrastructure and its build overhead.
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 looks good too. But I am not a hashing expert so I don't have an opinion here. Let's see what others propose.
Here is the easy thing to reason about this just in case. ^
mixes very poor: single bit change of input changes only single bit of output. Ideally when we mix / combine hash values (or do similar things) should produce values that are not dependent on the original hash values being combined. This greatly reduce the possibility of collisions from "related" 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.
Done with using detail::combineHashValue
but not use other functions from ADT/Hashing.h.
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.
Maybe we should switch all DenseMapInfo
for all integer types to llvm::hash_value
?
@chandlerc suggested to use it in his response and he does not expect this to cause any problems.
The code would be simple and we will get to reuse it in more places.
I am not sure how much worse the build times will get, but this should be easy to address by having declarations with functions hashing integers in a separate header.
I feel it should not be a blocker to using the hash function that we feel is better otherwise.
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.
The current version should also relieve the immediate pain, but I wonder if other people also feel that using murmur-like hashing is a better long-term option anyway.
llvm/include/llvm/ADT/DenseMapInfo.h
Outdated
@@ -151,7 +151,8 @@ template<> struct DenseMapInfo<unsigned long long> { | |||
static inline unsigned long long getTombstoneKey() { return ~0ULL - 1ULL; } | |||
|
|||
static unsigned getHashValue(const unsigned long long& Val) { | |||
return (unsigned)(Val * 37ULL); | |||
return DenseMapInfo<unsigned>::getHashValue(Val) ^ |
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.
the easy way is detail::combineHashValue
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.
+1 to 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.
Done
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 looks conceptually fine, but I think you need to do the same thing for the unsigned long
type for the case where it is 64-bits large. On 64-bit Linux targets uint64_t will typically be a typedef for unsigned long, not unsigned long long.
Are we worried about implications of changing the hash function for something as widely used as 64 bit ints?
I wonder if this change should be postponed until after Clang 19? Or am I too pessimistic in my predictions and things will just work out? Also a few open questions:
It would be nice to get someone expert in state-of-the-art hash functions and hash tables to review this. |
@chandlerc -- any chance you could weigh in here? |
I don't think there's particular cause to worry.
We would of course confirm this first -- the patch as-is does not have any impact on compile-time, but I think this is mostly because it modifies the wrong overload. I can re-test this after the patch is updated.
There may be some test fallout to deal with, but I think we should be in a fairly good position thanks to reverse-iteration testing.
We currently only use the low bits of the hash, so extending it to 64 bits is not useful at present.
LLVM is definitely behind the state of the art -- if someone finds the time to port DenseMap and SmallPtrSet to something like swiss tables, that would be great. (And not just for performance, we could also get rid of the need to specify explicit tombstone and empty keys for types.) |
Thanks for the pointers, I didn't know we had this. Another potential problem is downstream uses that aren't covered by the upstream tests we have. I am sure it's manageable, but we would want to announce the change a little in advance so that people know it's coming.
Sorry for the confusing wording on my part, I referred to not just extending the returned hash function to 64 bit, but also making sure we use all 64bits in our hash table implementation.
+1, I know that @chandlerc had some new ideas for hash tables in the works too, but I'm not sure how fleshed out they are and whether he has time to land them / describe them in enough detail that someone else can pick them up. |
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 think this makes sense on its own and we can improve the hash function without going all in on a better dense map implementation.
However, it would be good to have data on how this actually affects performance on real-world programs. For example, you could manually instrument dense map-ish containers and count the number of key operations (resize here?) when bootstrapping clang or compiling some other larger inputs.
The patch was prompted by a recent ~5x regression in compilation speed, the corresponding profiles are quoted here: #92083 (comment) @ilya-biryukov found that changing the hash function effectively resolves the regression (which I would expect to more or less remove the DenseMap methods from the hot code). But if necessary, I can profile clang on the same compilation with and without this PR. |
I'd like to confirm not only this improves the overall performance, but that the speedup can be attributed to 'better' hashing instead of, say, the elements being (coincidentally) in a better order. I don't know the low-level details, but wouldn't this reduce the number of collisions and number of resizes? |
What's updated:
For performances, we need benchmarking. I tested local simple workloads but didn't any observable change. I feel this should be conceptually fine too since the previous implementation is clearly not good for 64 bits integer. |
Happy to, I've been studying this freshly for the past 6 months. Generally, none of the old multiplication, or the shift are going to work well. But they may get lucky with the current inputs and appear to work well. Not sure why the concern over ADT/Hashing.h -- that code has held up quite well and remains a reasonably strong balance of strong hashing at reasonable cost. In particular, for a 64-bit integer, I wouldn't expect it to be much slower than the I have recently developed a hashing function that is only slightly worse than ADT/Hashing.h and is very, very competitive (I suspect faster, but the proof will take time to tell) to the very best. It is open source in Carbon and under the LLVM license: This routine is dramatically faster than LLVM's Hashing.h, and equal or faster to essentially everything else I've been able to evaluate for small objects (integers, pointers, tuples of those). For long strings there are a few faster approaches using specialized hardware (AES building blocks), but not enough to matter for a compiler I strongly suspect. The lower quality hashing should largely be fine as long as the hash table load factor is low enough. I've done a good amount of DenseMap benchmarking with that hash function, you can see code that benchmarks it directly here: Carbon also just got a new hashtable implementation that tries to be as close to DenseMap as I could possibly make it for small tables and very sparse tables (low load factor), while operating with a very high load factor (7/8) and with superb performance on large tables. It's based on SwissTable, and comparable or better performance. If there are hashtables that are struggling with DenseMap's design, that would be what I would suggest. But it is very, very hard to compete with DenseMap -- the performance is fantastic and almost unbeatable for very small tables and low load factors. |
It looks like there are some discussion about how to implement the hash for DenseMap best in practice. This is great and valuable. But I feel I don't have the capability to handle that. So how about opening this in a separate RFC and in this page, let's try to focus on if the patch itself is good to go? |
Compile-time for the PR as proposed: http://llvm-compile-time-tracker.com/compare.php?from=3ca17443ef4af21bdb1f3b4fbcfff672cbc6176c&to=9cd0b838265006ff699153bfbb1a1a39ebfb9cdd&stat=instructions:u Compile-time using xor mixing (that is, 820edb5 applied on top): http://llvm-compile-time-tracker.com/compare.php?from=3ca17443ef4af21bdb1f3b4fbcfff672cbc6176c&to=820edb52ce1abc090f41c5210dcb04edb6203f36&stat=instructions%3Au It doesn't seem like we get any benefit out of the better mixing using combineHashValue() for average-case compilation, so I think you should stick to your first variant using xor. |
Not sure the % changes in the stage2 builds are large enough to be a significant worry... But more importantly, while the XOR mixing looks good with a small test (compiling Clang itself), that doesn't make it robust when used with much broader inputs. FWIW, when working on hashtables, it is easy to have benchmarks and test cases that show simple solutions are faster that then struggle when the wrong input shows up, and I suspect that's how the original use of multiply ran into issues. |
People use metaheuristics to find good bit mixer functions that achieve good ratings on some metrics (e.g. PractRand)
This patch can still use I agree that we should remove the empty and tombstone keys from DenseMap. ~2 years ago I tried different hash map implementations to improve the performance of the global symbol table in lld/ELF. I think I have measured negligible performance difference. |
If you want to improve the hash functions used in LLVM, that seems like an interesting project. Again, I posted up thread a link to a very good hash function I have been developing based on experience with Abseil's, and it works very well with But I don't understand why the goal isn't to use the LLVM hashing library that is currently in the codebase, and if desired, add improvements to it. That seems better than slowly re-creating another hashing library here. =[ The original intent of the ADT/Hashing.h code was to provide a good replacement for DenseMap and other usages. If it needs to be improved to do that, by all means. |
Done |
Thanks for these pointers!
A lot of work can be done to both Incorporating I have read some code of carbon-lang/common/hashing.h and absl/hash for integer types and std::pair.
Waiting for Hashing.h and DenseMap improvement would take too long. When we are ready to switch more stuff to Carbon style hashing, we can probably use the following multiplication fallback for non-GCC-non-Clang compilers. std::pair<uint64_t, uint64_t> mul64(uint64_t a, uint64_t b) {
uint64_t a0 = a & 0xffffffff, a1 = a >> 32;
uint64_t b0 = b & 0xffffffff, b1 = b >> 32;
uint64_t t = a0 * b0;
uint64_t u = t & 0xffffffff;
t = a1 * b0 + (t >> 32);
uint64_t v = t >> 32;
t = (a0 * b1) + (t & 0xffffffff);
return {(t << 32) + u, a1 * b1 + v + (t >> 32)};
} |
@@ -137,7 +137,10 @@ template<> struct DenseMapInfo<unsigned long> { | |||
static inline unsigned long getTombstoneKey() { return ~0UL - 1L; } | |||
|
|||
static unsigned getHashValue(const unsigned long& Val) { | |||
return (unsigned)(Val * 37UL); | |||
if constexpr (sizeof(Val) == 4) |
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.
Now we have densemap::detail::mix
with very low latency, we can use it unconditionally for 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.
I think it's best to keep the conditional so you get the same hashing behavior if you use a different spelling for what is essentially the same 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.
Yeah, I'd like to keep the condition too.
Thanks for the high quality summary. Looking forward for further improvements! |
LLVM Buildbot has detected a new failure on builder Full details are available at: https://lab.llvm.org/buildbot/#/builders/72/builds/235 Here is the relevant piece of the build log for the reference:
|
I feel like the failure may not be related to the one. Since it looks like compiler rt has its own implementation for DenseMap: https://github.com/llvm/llvm-project/blob/main/compiler-rt/lib/sanitizer_common/sanitizer_dense_map.h and hash functions:https://github.com/llvm/llvm-project/blob/main/compiler-rt/lib/sanitizer_common/sanitizer_dense_map_info.h |
@ChuanqiXu9 I'm seeing warnings on MSVC builds:
|
if constexpr (sizeof(Val) == 4) | ||
return DenseMapInfo<unsigned>::getHashValue(Val); | ||
else | ||
return detail::combineHashValue(Val >> 32, Val); |
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.
@ChuanqiXu9 To silence MSVC warnings, could we change this to:
return detail::combineHashValue(Val >> (4 * sizeof(Val)), Val);
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. You can land that directly to make the build green. But I don't understand in what cases it is a problem. I think I've already handled the case for its size is 4. I don't feel it is possible that sizeof(Val)
may evaluate to 2
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.
MSVC has a tendency to analyze additional paths such as the non-constexpr else clause - we've hit similar problems before :(
…ounds shift warning Fixes MSVC warning after llvm#95734 - despite it taking the `sizeof(Val) == 4` path, it still warns that the 32-bit unsigned long shift by 32 is out of bounds, so avoid it by converting the hard coded shift amount to be based off sizeof() instead.
+1 to this
@MaskRay what do you mean by this? Wouldn't it be a matter of applying these changes to Hashing.h instead of here? |
Current bit mixers in We've switched to Initially, I thought changing I have now fixed these clients (10+ commits) and proposed #96282 for improvement. Should we include |
…:getHashValue The FIXME says to revert this when the underlying issue got fixed. And now the underlying issue got fixed in #95734. So I think it should be fine to rever that one now.
(Background: See the comment: llvm#92083 (comment)) It looks like the hash function for 64bits integers are not very good: ``` static unsigned getHashValue(const unsigned long long& Val) { return (unsigned)(Val * 37ULL); } ``` Since the result is truncated to 32 bits. It looks like the higher 32 bits won't contribute to the result. So that `0x1'00000001` will have the the same results to `0x2'00000001`, `0x3'00000001`, ... Then we may meet a lot collisions in such cases. I feel it should generally good to include higher 32 bits for hashing functions. Not sure who's the appropriate reviewer, adding some people by impressions.
…ounds shift warning (llvm#96173) Fixes MSVC warning after llvm#95734 - despite it taking the `sizeof(Val) == 4` path, it still warns that the 32-bit unsigned long shift by 32 is out of bounds.
…:getHashValue The FIXME says to revert this when the underlying issue got fixed. And now the underlying issue got fixed in llvm#95734. So I think it should be fine to rever that one now.
(Background: See the comment:
#92083 (comment))
It looks like the hash function for 64bits integers are not very good:
Since the result is truncated to 32 bits. It looks like the higher 32 bits won't contribute to the result. So that
0x1'00000001
will have the the same results to0x2'00000001
,0x3'00000001
, ...Then we may meet a lot collisions in such cases. I feel it should generally good to include higher 32 bits for hashing functions.
Not sure who's the appropriate reviewer, adding some people by impressions.