The reqirements of Hash state that if two values are Eq they must produduce the same hash, but unequal values are allowed to collide and have the same hash. That means this is a valid implementation as well:
Because it’s a no-op, it will be much more performant when calculating hashes. It’s a terrible idea, though, because anything using it will need to fallback to using Eq instead.
Though this is an extreme example, it points to both a flaw in your testing strategy and a potential avenue for performance gain: Some fields can be ignored entirely if they’re unlikely to prevent a colission, but you need to track how the different hashing strategies affect the algorithms you’re trying to use.
In my case the ComponentId is used as a unique lookup in many hashmaps inside of an ECS. Being that the use-case in this instance is almost exclusively ( I'm pretty sure ) to use the hashes for lookups in HashMaps I imagine that the goal would be to always produce unique hashes to avoid ever having to fall back to Eq.
It may be that you can skip putting the discriminant in the hash, though. TypeId is already a hash of sorts, so it’s unlikely to conflict with any of the external ids.
So It's hash function would just boil down to state.write_u64(t), which is essentially the same as the hash implementation of the hash for ComponentId::ExternalId, which just writes the u64.
I guess just because of the very large value of u64::MAX the collision chance would be slim, though. I guess the question then is whether or not the collision chance is low enough that the performance gained by saving the hash of 1 byte is worth it. That hash is probably one of the hottest parts of the code, so it does seem like it could be worth getting any little bit extra performance savings out of it.
Skipping the byte does speed it up a lot more than I thought it would so that seems worth it:
hash rust id time: [5.1949 ns 5.2009 ns 5.2076 ns]
change: [-25.384% -24.028% -22.858%] (p = 0.00 < 0.05)
Performance has improved.
Found 11 outliers among 100 measurements (11.00%)
1 (1.00%) low severe
1 (1.00%) low mild
2 (2.00%) high mild
7 (7.00%) high severe
hash external id time: [4.4540 ns 4.4599 ns 4.4667 ns]
change: [-38.417% -37.437% -36.456%] (p = 0.00 < 0.05)
Performance has improved.
Found 10 outliers among 100 measurements (10.00%)
1 (1.00%) low mild
1 (1.00%) high mild
8 (8.00%) high severe
Next, it's probably not worth it, but I wonder if there is a way to use a union or an unsafe cast or something similar to access the internal u64 without having to do a match to avoid the branching.
If you can work with 63 bit, instead of 64 bit, this topic might be of interest to you:
I also had to deal with an enum for which I wanted to remove the space overhead of the tag, in the past.
The provided solution will enable you to avoid the branch for hashing purposes, but will cost you more time to create and read the value, because it has to transform the space-efficient enum into a proper one and vice-versa.
P.S.: It doesn't map 1:1 to what you need, but it should be close enough to guide you to an individual solution.