-
Notifications
You must be signed in to change notification settings - Fork 27
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
New best known functions #19
Comments
Fascinating! This is such a big jump from my best, too. Since it's highly
unlikely that there are better permutations outside those shift+popcnt
constraints, and you did an exhaustive search, I suppose that means these
are probably the best possible parameters (by this heuristic)? As in I
should stop searching this particular space (not that I'm actively doing
so now).
I don't understand the role of "len = 1 << 25" so could you elaborate on
that part?
|
I set Also the final exact search only searched within 4 "neighborhood" steps of the initial so I think there may still be some multipliers that were not seen. |
Here are some others for posterity
|
Some more
|
And just for fun, new best known functions for other types: 16-bit 2-round (17.5 minutes, 970,000 unique exact bias calculations): No improvement was found for 16-bit w/o mul 3-round. There are only ~11.5 million so a brute force global search might be feasible though likely unnecessary. I used a generic implementation of this memetic algorithm with dynamic population management for each of these with the following specifics:
|
As mentioned in #12, I found a new best
|
Yet another new best:
which is a local optimum. Note that it's not a local optimum by the math in #12 |
Very impressive results. Have you made any progress with 64 bit hashes? perhaps it would be easier to treat the 64 bit hash as producing two 32 bit hashes, then you'd have two exact bias numbers, instead of one estimate. |
|
Sorry, my bad, I confused 64-bit with 32-bit 3-round hashes. |
FWIW: Here's a structure I keep meaning to put some effort into but haven't gotten around to yet. The constants are all ad-hoc (no attempt at optimization) static inline uint32_t crc32c(uint32_t x, uint32_t k) { return _mm_crc32_u32(x,k); }
uint32_t hash(uint32_t x)
{
x = crc32c(x,0x9e3d2c1b); x *= 0x9e485565;
x = crc32c(x,0x9e3d2c1b); x *= 0x9e485565;
return x;
} |
After a bit of optimization that structure seems quite strong:
About twice as good as the "classic" hash function here and a significant jump from the previous work on CRC #13 (comment) Unfortunately it doesn't vectorize as well so optimization/searching is slower |
Nice. This configuration also as the "cherry on top" feature of having zero fixed points (where my PoC version has two). WRT optimization: yeah I think using a CRC step is only of potential interest as part of bit finalizing. |
I am a random passerby, with a thought. ARM 8.1-A (pretty much ever modern Cortex-A core) has CRC32 ISA extensions. @Marc-B-Reynolds 's code might be fast on those targets? |
@TheIronBorn do you have the accompanying inverse function for the best two-step combination you found? It would be cool to add the best takeaway to the readme. |
I have an implementation of the inverse of CRC32C (function crc32c_inv) here: https://github.com/Marc-B-Reynolds/Stand-alone-junk/blob/master/src/SFH/carryless.h |
@ianstormtaylor xorshift and odd multiplication both have well known inverses: x ^= x >> 15 ^ x >> 30;
x *= 2534613543;
x ^= x >> 15 ^ x >> 30;
x *= 859588901;
x ^= x >> 16; |
What makes I attempted to apply it as a final mixer for the MicroOAAT hash in SMHasher as a test case. It uses two hash variables For So a bunch of bits always show bias in I then tried prospector32 ( But it never shows any biased bits. It seems to perform much better, It doesn't seem like a fluke, as I noticed other combinations of multipliers such as So, what I'm actually trying to do is make the fastest decent 64-bit hash in JavaScript, which is limited to 32-bit integers. It seems possible to mix So far, I came up with this finalizer that mixes h1 ^= (h1 ^ (h2 >> 15)) * 0x735a2d97;
h2 ^= (h2 ^ (h1 >> 15)) * 0xcaf649a9;
h1 ^= h2 >> 16;
h2 ^= h1 >> 16; But again, as soon as I start to adapt the constants So yeah, just some observations. I don't really know what I'm doing. |
The |
I'm not sure what a bias metric is or if I'm using one. 😄 But a breakdown of what I am doing if my post was not clear:
Some more graphs, showing the avalanche after final operation. I ran the test twice for each, just to show that this behavior carries between multiple test runs. Control test using Using Using MurmurHash3's constants Using original lowbias32 from README, Using prospector32 from README, Tested in another way that shows similar results, my experimental hash function, using this altered intermixed construction: My currently chosen finalizer (no clue if its optimal but it avalanches well here): h1 ^= (h1 ^ (h2 >> 15)) * 0x735a2d97;
h2 ^= (h2 ^ (h1 >> 15)) * 0xcaf649a9;
h1 ^= h2 >> 16;
h2 ^= h1 >> 16; Using TheIronBorn lowest bias constants: h1 ^= (h1 ^ (h2 >> 16)) * 0x21f0aaad;
h2 ^= (h2 ^ (h1 >> 15)) * 0x735a2d97;
h1 ^= h2 >> 15;
h2 ^= h1 >> 15; Using MurmurHash3 constants: h1 ^= (h1 ^ (h2 >> 16)) * 0x85ebca6b;
h2 ^= (h2 ^ (h1 >> 13)) * 0xc2b2ae35;
h1 ^= h2 >> 16;
h2 ^= h1 >> 16; Using original lowbias32 from README: h1 ^= (h1 ^ (h2 >> 16)) * 0x7feb352d;
h2 ^= (h2 ^ (h1 >> 15)) * 0x846ca68b;
h1 ^= h2 >> 16;
h2 ^= h1 >> 16; One bit is biased in Using prospector32 from README: h1 ^= (h1 ^ (h2 >> 15)) * 0x2c1b3c6d;
h2 ^= (h2 ^ (h1 >> 12)) * 0x297a2d39;
h1 ^= h2 >> 15;
h2 ^= h1 >> 15;
Keep in mind I am not contesting this. I am simply confused as to why it appears to perform worse than other multipliers when used as finalizer in a string hash function. Maybe something else is at play here. |
Looks like the bias metric that post used is the raw error per "bin". The metric this library uses is root mean square relative error over all bins. |
Perhaps MicroOAAT (of
Pattern mod 16 is clearly observable. Here are values of
Pattern mod 34 (=2*17) is somewhat observable. Code. Note: While Edit: typesetting. |
MicroOAAT author here :) It is intriguing someone reached it.
No, it returns Actually it is designed to be used with same finalizer as GoodOAAT Note: I'm not good at this math at all. I just used semi-scripted "genetic" algo over SMHasher output to find rotation values for finalizer. And doubdfully the finalizer is good for something except this two functions. |
Ah, my apologies, GoodOAAT is the one that returns only |
@bryc Your finalizer isn't bijective (cannot be reversed), if I am reading your code correctly, because of how h1 and h2 are mixed together: h1 ^= (h1 ^ (h2 >> 15)) * 0x735a2d97;
h2 ^= (h2 ^ (h1 >> 15)) * 0xcaf649a9;
h1 ^= h2 >> 16;
h2 ^= h1 >> 16; I believe you could make it reversible with some small changes, but I'd have to ask an expert ( @Marc-B-Reynolds I think wrote a blog post about this?) about the last two lines, since they may be reversible without any changes, but I think they need a small change. h1 ^= (h2 ^ (h2 >> 15)) * 0x735a2d97;
h2 ^= (h1 ^ (h1 >> 15)) * 0xcaf649a9;
h1 ^= (h2 ^ (h2 >> 16));
h2 ^= (h1 ^ (h1 >> 16)); Personally, I would use I can try to write the inverse if you want. Being invertible is important for a finalizer because it ensures all outputs can occur given all inputs to the finalizer. |
Please do; I'd greatly appreciate any insights. 😄 I know almost nothing about reversibility, I was just purely letting the avalanche graphs guide me. The choice for XOR over ADD is mostly because I generally found fewer collisions and better distribution in my naïve tests when using XOR, but completely unscientific of course. I can't really measure a meaningful difference with my crappy tests. It just seemed to work as well or better, and was faster. It's also a bit of a better choice in JavaScript (one of my target platforms), as using addition tends to break out of the 32-bit optimization mode modern JS engines have, that XOR and If I ever figure out how to get SMHasher to run on W10 any potential issues in the hash function will probably be a lot more obvious. |
Great point about XOR on JS; this should be able to stick with just XOR, shifts, and multiplication then. I only relatively-recently learned Math.imul() existed, and I am very thankful it does. As for the reversibility, I should say a) it turned out much simpler than I expected, and b) your original last two lines actually are reversible. If you want to reverse this hash over the JS Numbers h1 and h2, where both are in the 32-bit range: h1 ^= Math.imul((h2 ^ (h2 >>> 15)), 0x735a2d97);
h2 ^= Math.imul((h1 ^ (h1 >>> 15)), 0xcaf649a9);
h1 ^= h2 >>> 16;
h2 ^= h1 >>> 16; This is the reverse: h2 ^= h1 >>> 16;
h1 ^= h2 >>> 16;
h2 ^= Math.imul((h1 ^ (h1 >>> 15)), 0xcaf649a9);
h1 ^= Math.imul((h2 ^ (h2 >>> 15)), 0x735a2d97); Yes, those constants may look familiar. The entire thing probably looks familiar. The only change I had to do to the forward hash was to ensure each xorshift that gets multiplied uses the same variable throughout the right hand side, and a different variable on the left. The only change I then had to do to reverse it was to reverse the lines. I can see about running it through SMHasher; I have gotten it working on Windows before, but I mainly run it on Linux. The fork I use is kind-of tricky to get a hash up and running; this round function is mostly what I need if I can plug it in to replace an existing pair-of-ints-to-pair-of-ints function. |
If If both
It is equivalent to t1 = h1;
h1 ^= (h2 ^ (h2 >> 16));
h2 = t1 ^ (t1 >> 16) ^ (h2 >> 16); i.e. highest bits of |
certainly it is not reversible, since with any |
Considering doubts about "0.10704308166917044" as "best value". Shouldn't "best value" be close to theoretical random result? I thought, true random could not be too "flat". There are fluctuations, which distinct "true random" from obviously synthesized stream of values. So, if we want immitate "true random", then we should not be "too good to be true". So, I'd rather aim for values achievable with strong cryptographic functions: SHA-256, SHA-512, SHA3, and others (SipHash-2-4, SipHash-4-8). Smart people spend their life to generate values that certainly indistinguishable from random. If we get results that differs, we do something wrong. But I'm not professional mathematic, so I could be mistaken. |
I believe, Siphash-4-8 (and even SipHash-2-4) is certainly good reference to compare results with. |
Yes they are indeed both results - the intention is that both The actual JS implementation I have forms a 53-bit integer, discarding some bits, because 64-bit integers is not possible without more complicated and slower logic: const cyrb53a = function(str, seed = 0) {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for(let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 0x85ebca77);
h2 = Math.imul(h2 ^ ch, 0xc2b2ae3d);
}
h1 ^= Math.imul(h1 ^ (h2 >>> 15), 0x735a2d97);
h2 ^= Math.imul(h2 ^ (h1 >>> 15), 0xcaf649a9);
h1 ^= h2 >>> 16; h2 ^= h1 >>> 16;
return 2097152 * (h2 >>> 0) + (h1 >>> 11);
}; Math.imul is just 32-bit multiplication in C. So quick pseudo C code: // init hash
u32 h1 = 0xdeadbeef ^ seed;
u32 h2 = 0x41c6ce57 ^ seed;
// loop each input char to hash, one at a time:
h1 = (h1 ^ char) * 0x85ebca77;
h2 = (h2 ^ char) * 0xc2b2ae3d;
// final mix
h1 ^= (h1 ^ (h2 >> 15)) * 0x735a2d97;
h2 ^= (h2 ^ (h1 >> 15)) * 0xcaf649a9;
h1 ^= h2 >> 16;
h2 ^= h1 >> 16;
// h2 combined with h1 is the final output
return (h2 << 32) | h1; My h1/h2 mixer (which is WIP) is similar to the OP mixer but uses fewer operations. It was my hope that intermixing two 32-bit hash variables could optimize the number of operations in the final mixer down and still allow for a meaningful 64-bit result. I'm not sure if this is misguided (a few extra ops in a final mixer may not be that expensive, even in JS), but it does seem to avalanche decently, so I've been exploring it. My initial post in this thread was basically expressing confusion as to why I was getting worse avalanche results when testing my h1/h2 mixer, despite using the least biased multipliers. There certainly seems to be some sensitivity or pattern to the constants chosen. Also I should probably explain why I was testing using So the idea was to apply my h1/h2 mixer to see if both h1 and h2 still avalanched and produced useful 64-bit hash, without the additional multiplication in the loop, which I had done in my And simply put, more biased multipliers showed less bias in the avalanche results, than the highest scoring multiplier (and best one, supposedly). |
Have you seen #12 (comment)? For a truly random distribution the average is 0.0152587890625. So 0.10704308166917044 is far from "ideal", it's only the best one found so far. |
Could this value (or close to) achieved with SipHash/SHA2/SHA3 measured by software in this repo? If yes, then you're right. If not, then formula in that comment is about some other measurement. Just measure SHA2 or SipHash and prove me wrong. |
I just ran SipHash over the inputs [0, 67108864), seeded with SipHash: 0.16265987941647841 |
Nice result. |
SipHash over all 2^32: 0.0211225759194 |
So, ~0.021 estimate is real. |
I never expected MicroOAAT has enough quality to produce 64bit output. It is built in assumption it has more than “32bit of randomness”, so result truncated to 32bits is good enough. I didn’t calc, but I expect it is as good as 40-48 bit hash. Certainly, properly mixed it could look as decent 64bit hash for simple use cases. Try to apply GoodOAAT finalizer (since it were built for MicroOAAT), and add one more step to mix more 32 bits. I think it will “look like” good 64 bit hash. |
@pspeter3 My posts are purely about how the avalanche behavior of these constants seem to act differently when mixing two hashes together instead of just one hash. That is, the best was no longer the best for my specific use case for performing a partial final mix of two 32-bit hashes into a 64-bit whole. The intended construction for this project (hash-prospector) is to have the operations applied to a single 32-bit hash as the final operation. As such, it is an integer hash. And the best result is still #19 (comment). So in theory, the operation can be done twice in whole for The full set of operations is tested as an integer hash, without any initial mixing applied, so the avalanche results don't rely on any partial mixing. Hope that clears any confusion :) In terms of it being reversible, I believe so: var h1 = 0xdeadbeef, h2 = 0xcafebabe;
// do it
h1 ^= Math.imul((h2 ^ (h2 >>> 15)), 0x735a2d97);
h2 ^= Math.imul((h1 ^ (h1 >>> 15)), 0xcaf649a9);
h1 ^= h2 >>> 16;
h2 ^= h1 >>> 16;
// undo it
h2 ^= h1 >>> 16;
h1 ^= h2 >>> 16;
h2 ^= Math.imul((h1 ^ (h1 >>> 15)), 0xcaf649a9);
h1 ^= Math.imul((h2 ^ (h2 >>> 15)), 0x735a2d97);
console.log((h1>>>0).toString(16), (h2>>>0).toString(16));
// both will return deadbeef and cafebabe |
Thank you for sharing! I think I'm struggling getting the inverse working generally in JavaScript. Copying the example from the README with the values from the earlier comment /**
* Hashes a value.
* @param {number} x
* @returns {number}
*/
function toLowBias32(x) {
x ^= x >>> 16;
x = Math.imul(x, 0x21f0aaad);
x ^= x >>> 15;
x = Math.imul(x, 0xd35a2d97);
x ^= x >>> 15;
return x;
}
/**
* Inverses the hashing function.
* @param {number} x
* @returns {number}
*/
function fromLowBias32(x) {
x ^= x >>> 15;
x = Math.imul(x, 0x37132227);
x ^= x >>> 15;
x = Math.imul(x, 0x333c4925);
x ^= x >>> 16;
return x;
}
// 1337 1394193351 -1106502979
console.log(1337, toLowBias32(1337), fromLowBias32(toLowBias32(1337))); |
Thanks @Marc-B-Reynolds! That was super helpful and I have a working version now. /**
* Hashes a value.
* @param {number} x
* @returns {number}
*/
function toLowBias32(x) {
x ^= x >>> 16;
x = Math.imul(x, 0x21f0aaad);
x ^= x >>> 15;
x = Math.imul(x, 0xd35a2d97)
x ^= x >>> 15;
return x >>> 0;
}
/**
* Inverses the hashing function.
* @param {number} x
* @returns {number}
*/
function fromLowBias32(x) {
x ^= x >>> 15;
x ^= x >>> 30;
x = Math.imul(x, 0x37132227);
x ^= x >>> 15;
x ^= x >>> 30;
x = Math.imul(x, 0x333c4925);
x ^= x >>> 16;
return x >>> 0;
} Now I'm curious based on @bryc's research if I can make |
I used this project to learn combinatorial optimization.
UPDATE: a better function was found in a below comment, the new best is now
0.10704308166917044
A metaheuristic was used with an estimate with
len = 1 << 25
and constrained to be nearby other good functions (shifts [13, 19], muls popcnt [11, 19]). (Unfortunately I don't remember which metaheuristic I used for this). Finally an exhaustive exact search was applied nearby the best found. I verified the final bias with thehash-prospector
library itself.I also used some explicit SIMD acceleration in the bias calculation along with the current multi threading. The hash itself handles 8 "seeds" at once and the counting is faster too.
SIMD counting pseudo code:
The text was updated successfully, but these errors were encountered: