This commit is contained in:
Miloslav Ciz 2022-09-09 18:37:16 +02:00
parent 22c5543823
commit f82f156102
6 changed files with 198 additions and 26 deletions

61
hash.md
View file

@ -1,15 +1,17 @@
# Hash
Hash is a number computed by a **hash function**, a function that takes some data and turns it into a number (the hash) that's much smaller than the data itself, has a fixed size (number of [bits](bit.md)) and which has additional properties such as being completely different from hash values computed from very similar data. Thanks to these properties hashes have a very wide use in [computer science](compsci.md) -- they are often used to quickly compare whether two pieces of non-small data, such as documents, are the same, they are used in indexing structures such as **hash tables** which allow for quick search of data, and they find a great use in [cryptocurrencies](crypto.md) and [security](security.md), e.g. for [digital signatures](sigital_signature.md). Hashing is extremely important and as a programmer you won't be able to avoid encountering hashes somewhere in the wild.
Hash is a number that's computed from some data in a [chaotic](chaos.md) way and which is used for many different purposes, e.g. for quick comparisons (instead of comparing big data structures we just compare their hashes) or mapping data structures to table indices.
{ Talking about wilderness, Hyenas and other animals have their specific smells that are created by bacteria in them and are unique to each individual depending on the exact mix of the bacteria. They use these smells to quickly identify each other. The smell is kind of like the animal's hash. But of course the analogy isn't perfect, for example similar mixes of bacteria may produce similar smells, which is not how hashes should behave. ~drummyfish }
Hash is computed by a **hash function**, a function that takes some data and turns it into a number (the hash) that's in terms of [bit](bit.md) width much smaller than the data itself, has a fixed size (number of [bits](bit.md)) and which has additional properties such as being completely different from hash values computed from very similar (but slightly different) data. Thanks to these properties hashes have a very wide use in [computer science](compsci.md) -- they are often used to quickly compare whether two pieces of non-small data, such as documents, are the same, they are used in indexing structures such as **hash tables** which allow for quick search of data, and they find a great use in [cryptocurrencies](crypto.md) and [security](security.md), e.g. for [digital signatures](sigital_signature.md) or storing passwords (for security reasons in databases of users we store just hashes of their passwords, never the passwords themselves). Hashing is extremely important and as a programmer you won't be able to avoid encountering hashes somewhere in the wild.
{ Talking about wilderness, hyenas have their specific smells that are determined by bacteria in them and are unique to each individual depending on the exact mix of the bacteria. They use these smells to quickly identify each other. The smell is kind of like the animal's hash. But of course the analogy isn't perfect, for example similar mixes of bacteria may produce similar smells, which is not how hashes should behave. ~drummyfish }
It is good to know that we distinguish between "normal" hashes used for things such as indexing data and [cryptographic](cryptography.md) hashes that are used in computer security and have to satisfy some stricter mathematical criteria. For the sake of simplicity we will sometimes ignore this distinction here. Just know it exists.
It is generally given that a hash (or hash function) should satisfy the following criteria:
- **Have fixed size** (given in bits), even for data that's potentially of variable size (e.g. text strings).
- **Be fast to compute**. This is mostly important for non-security uses, cryptographic hashes may prioritize other properties to guarantee the hash safety.
- **Be fast to compute**. This is mostly important for non-security uses, cryptographic hashes may prioritize other properties to guarantee the hash safety. But a hash function certainly can't take 10 minutes to compute :)
- **Have uniform mapping**. That is if we hash a lot of different data the hashes we get should be uniformly spread over the space of the hashes, i.e. NOT be centered around some number. This is in order for hash tables to be balanced, and it's also required in security (non-uniform hashes can be easier to reverse).
- **Behave in a [chaotic](chaos.md) manner**, i.e. hashes of similar data should be completely different. This is similar to the point above; a hash should kind of appear as a "random" number associated to the data (but of course, the hash of the same data has to always be the same when computed repeatedly, i.e. be [deterministic](determinism.md)). So if you change just one bit in the hashed data, you should get a completely different hash from it.
- **Minimize collisions**, i.e. the probability of two different values giving the same hash. Mathematically collisions are always possible if we're mapping a big space onto a smaller one, but we should try to reduce collisions that happen in practice. This property should follow from the principle of uniformity and chaotic behavior mentioned above.
@ -27,23 +29,48 @@ Some common uses of hashes are:
## Code Example
Let's say we want a has function for string which for any [ASCII](ascii.md) string will output a 32 bit hash. How to do this? We need to make sure that every character of the string will affect change the resulting hash.
Let's say we want a has function for string which for any [ASCII](ascii.md) string will output a 32 bit hash. How to do this? We need to make sure that every character of the string will affect the resulting hash.
First though that may come to mind could be for example to multiply the ASCII values of all the characters in the string. However there are at least two mistakes in this: firstly short strings will result in small values as we'll get a product of fewer numbers. Secondly reordering the characters in a string (i.e. its [permutations](permutation.md)) will not change the hash at all (as with multiplication order is insignificant)! These violate the properties we want in a hash function. If we used this function to implement a hash table and then tried to store strings such as "abc", "bca" and "cab", all would map to the same hash and cause collisions that would negate the benefits of a hash table.
First thought that may come to mind could be for example to multiply the ASCII values of all the characters in the string. However there are at least two mistakes in this: firstly short strings will result in small values as we'll get a product of fewer numbers (so similar strings such as "A" and "B" will give similar hashes, which we don't want). Secondly reordering the characters in a string (i.e. its [permutations](permutation.md)) will not change the hash at all (as with multiplication order is insignificant)! These violate the properties we want in a hash function. If we used this function to implement a hash table and then tried to store strings such as "abc", "bca" and "cab", all would map to the same hash and cause collisions that would negate the benefits of a hash table.
The following is a better function, a variant of a function used in sdbm:
TODO
## Nice Hashes
{ Reminder: I make sure everything on this Wiki is pretty copy-paste safe, I only copy short (probably uncopyrightable) snippets or public domain code and additionally also reformat and change them a bit, so don't be afraid of the snippets. ~drummyfish }
The [*hash prospector* project](https://github.com/skeeto/hash-prospector) ([unlicense](unlicense.md)) created a way for automatic generation of integer hash functions with nice statistical properties which work by [XORing](xor.md) the input value with a bit-shift of itself, then multiplying it by a constant and repeating this a few times. The functions are of the format:
```
uint32_t stringHash(const char *s)
uint32_t hash(uint32_t n)
{
uint32_t result = 123;
while (*s)
{
r = s + (result << 16) + (result << 6) - result;
s++;
}
return result;
n = A * (n ^ (n >> S1));
n = B * (n ^ (n >> S2));
return n ^ (n >> S3);
}
```
```
Where *A*, *B*, *S1*, *S2* and *S3* are constants specific to each function. Some nice constants found by the project are:
| A | B |S1 |S2 |S3 |
|----------|----------|---|---|---|
|303484085 |985455785 |15 |15 |15 |
|88290731 |342730379 |16 |15 |16 |
|2626628917|1561544373|16 |15 |17 |
|3699747495|1717085643|16 |15 |15 |
The project also explores 16 bit hashes, here is a nice hash that doesn't even use multiplication!
```
uint16_t hash(uint16_t n)
{
n = n + (n << 7);
n = n ^ (n >> 8);
n = n + (n << 3);
n = n ^ (n >> 2);
n = n + (n << 4);
return n ^ (n >> 8);
}
```
TODO: more (strings etc.)