Author: | Wojciech Muła |
---|---|
Added on: | 2023-04-30 |
Updated on: | 2023-04-02 (grammar corrections) |
Contents
Looking up in a static set of strings is a common problem we encounter when parsing any textual formats. Such sets are often keywords of a programming language or protocol.
Parsing HTTP verbs appeared to be the fastest when we use a compile-time trie: a series of nested switch statements. I could not believe that a perfect hash function is not better, and that led to a novel hashing approach that is based on the instruction PEXT (Parallel Bits Extract).
Briefly, when constructing a perfect hash function, we are looking for the smallest set of input bytes that can be then the input for some function combines them into a single value. The instruction PEXT allows to quickly construct any n-bit subword from a 64-bit word; the latency of the instruction is 3 CPU cycles on the current processors. This allows us to extend the schema for looking for the smallest subset of bits. This n-bit word is then the input for a function that translates the word into the desired value.
Instead of something like:
func hash(s []byte) uint64 { // read bytes at indices a, b, c // and push forward return hash_bytes(s[a], s[b], s[c]) }
We have:
func hash(s []byte) uint64 { // read bytes at indices d and e, and form a temp value uint64 tmp = (uint64(s[d]) << 8) | uint64(s[e]) // from the temp value (bytes d and e) extract crucial bits uint64 val = PEXT(tmp, precomputed_mask) return hash_uint64(val) }
Please note that depending on the strings set, the number of bytes read in both schemas can vary. It is not the rule that a bit-level hash function would touch fewer bytes than a byte-level hash function.
Apart from the above hashing schema, this text describes also constructing a compile-time hash table and compile-time switch.
All source codes are available.
The instruction PEXT gets two arguments: the input word and the input mask. Bits from the input word for which the input mask is 1 are copied to the output. For example:
word: 0010101011010111 mask: 0011100100100010 masked: __101__0__0___1_ PEXT: __________101001
Short recap:
While minimal hash functions are hard to find, perfect hash functions are quite easy to compute. We will describe two methods:
When we build a generic hash table implementation, collisions are unavoidable. The most common solution to this problem is using some kind of container (a linked list or balanced tree) to keep all objects sharing the same hash value.
A less common strategy is open addressing. We store values directly in the table. If there is a collision — that is, the given index is already allocated — we try another index. There are several approaches to pick a new index; it simply may be the next index (called linear probing), but there are of course more sophisticated procedures (quadratic probing, double hashing, etc.).
In our case, where we have a static set of strings, we use the simplest solution. Our goal is to find the minimum size of table (N) in which the number of collisions (k) is also the least possible, preferably without collisions.
Input parameters are: 1) a set of strings and 2) hash function; to avoid calling the hash function over and over, we cache the hash value for each string.
The main procedure is responsible for settings the maximum number of collisions; it starts from 1 and increases it until we succeeded. The number of collisions depends on the quality of hash function, but often it is just 2 or 3 collisions. The Go implementation of procedure is shown below.
Then, for some range of table sizes, we check the actual number of collisions, and we report the minimum table size.
func computerHashParameters(keywords []Keyword, hash func([]byte) uint64) (size int, collisions int) { hashes := make([]uint64, len(keywords)) for i := range keywords { hashes[i] = hash(keywords[i].word) } collisions = 1 for { size = hashtableaux(hashes, collisions) if size > 0 { fmt.Printf("size = %5d, maxcollisions = %d\n", size, collisions) return } collisions += 1 } }
Below is the implementation of a helper function. It checks the table size starting from the size of input set, up to its tenfold; the multiply of 10 appeared to be a quite good upper bound.
func hashtableaux(hashes []uint64, maxcollisions int) int { n := len(hashes) const sizeOverhead = 10 outer: for N := n; N < sizeOverhead*n; N++ { table := make(map[uint64]int) for _, h := range hashes { idx := h % uint64(N) table[idx] += 1 if table[idx] > maxcollisions { continue outer } } return N } return -1 }
Now, having the parameters N and k, we can finally write a procedure for lookup. The following C++ procedure lookups for one of Go keywords.
int lookup_go_hash1(std::string_view s) { const uint64_t idx = (hash1(s) % 38) * 2; static std::string_view lookup[76] = { "chan", // 836 (0x344) "", "", "", "case", // 800 (0x320) "for", // 648 (0x288) "", "", "continue", // 1600 (0x640) "", "", "", "defer", // 1070 (0x42e) "func", // 804 (0x324) "", "", "", "", "package", // 1491 (0x5d3) "", "else", // 808 (0x328) "go", // 428 (0x1ac) "const", // 1075 (0x433) "range", // 1075 (0x433) "var", // 696 (0x2b8) "", "", "", "return", // 1344 (0x540) "", "", "", "", "", "map", // 663 (0x297) "", "select", // 1386 (0x56a) "struct", // 1386 (0x56a) "", "", "goto", // 856 (0x358) "", "", "", "switch", // 1314 (0x522) "", "", "", "fallthrough", // 2266 (0x8da) "", "", "", "", "", "", "", "", "", "", "", "default", // 1512 (0x5e8) "interface", // 1854 (0x73e) "", "", "type", // 868 (0x364) "", "", "", "if", // 414 (0x19e) "import", // 1326 (0x52e) "", "", "", "", "break", // 1025 (0x401) "", }; static int values[76] = { 2, // 836 (0x344) -1, -1, -1, 1, // 800 (0x320) 9, // 648 (0x288) -1, -1, 4, // 1600 (0x640) -1, -1, -1, 6, // 1070 (0x42e) 10, // 804 (0x324) -1, -1, -1, -1, 17, // 1491 (0x5d3) -1, 7, // 808 (0x328) 11, // 428 (0x1ac) 3, // 1075 (0x433) 18, // 1075 (0x433) 24, // 696 (0x2b8) -1, -1, -1, 19, // 1344 (0x540) -1, -1, -1, -1, -1, 16, // 663 (0x297) -1, 20, // 1386 (0x56a) 21, // 1386 (0x56a) -1, -1, 12, // 856 (0x358) -1, -1, -1, 22, // 1314 (0x522) -1, -1, -1, 8, // 2266 (0x8da) -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 5, // 1512 (0x5e8) 15, // 1854 (0x73e) -1, -1, 23, // 868 (0x364) -1, -1, -1, 13, // 414 (0x19e) 14, // 1326 (0x52e) -1, -1, -1, -1, 0, // 1025 (0x401) -1, }; for (int i=0; i < 2; i++) { if (lookup[idx + i] == s) { return values[idx + i]; } } return -1; }
First we have a call to hash1 on the input string. Its result is taken modulo 38 — that is the value N found by the above algorithm. Likewise, the magic constant 2 is parameter k, and it is the number of collisions we have to handle.
The lookup procedure contains two auxiliary tables of size N * k = 76: one for strings and another for values; the not-found-value was set to -1. (Note that we can have a single array of structures, however it's hard to tell what's better for typically small arrays.) After getting the index idx we just run two times the following check:
if (lookup[idx + i] == s) { return values[idx + i]; }
Please note that all parameters for hash table are known in compile time, thus a compiler can rid of division and apply additional optimizations.
Below is the assembly code of the procedure, compiled for Skylake architecture with GCC 12.2.0.
<lookup_go_hash1(std::basic_string_view<char, std::char_traits<char> >)>: push %r14 push %r13 mov %rsi,%r13 push %r12 push %rbp mov %rdi,%rbp push %rbx movsbq (%rsi),%rax // f := first char movsbq -0x1(%rdi,%rsi,1),%rcx // l := last char add %rax,%rcx // their sum imul %rdi,%rcx // (f + l) * s.size() movabs $0xd79435e50d79435f,%rax // mul %rcx // shr $0x5,%rdx // imul $0x26,%rdx,%rax // sub %rax,%rcx // mov %rcx,%rdx // lea (%rcx,%rcx,1),%r12 // shl $0x5,%rdx // rdx = ((f + l) * s.size()) % 38 // The for loop. GCC split the loop -- it quickly compares lengths // of lookup[idx] + i with s.size (rbp) and only if they are equal // jumps to bytes comparison. lea 0x267b47(%rip),%rax // rax = lookup address lea (%rdx,%rax,1),%rbx // rbx = size lea 0x2(%r12),%r14 mov 0x8(%rbx),%rdi cmp (%rbx),%rbp // jump to full compare only if lengths are equal je <lookup_go_hash1(std::basic_string_view<char, std::char_traits<char> >)+0x78> inc %r12 add $0x10,%rbx cmp %r14,%r12 jne <lookup_go_hash1(std::basic_string_view<char, std::char_traits<char> >)+0x52> pop %rbx pop %rbp pop %r12 pop %r13 mov $0xffffffff,%eax pop %r14 ret nopl (%rax) // check if bytes are equal test %rbp,%rbp je <lookup_go_hash1(std::basic_string_view<char, std::char_traits<char> >)+0x8c> mov %rbp,%rdx mov %r13,%rsi call <memcmp@plt> test %eax,%eax jne <lookup_go_hash1(std::basic_string_view<char, std::char_traits<char> >)+0x5b> pop %rbx pop %rbp lea 0x1f56bb(%rip),%rax # <lookup_go_hash1(std::basic_string_view<char, std::char_traits<char> >)::values> mov (%rax,%r12,4),%eax pop %r12 pop %r13 pop %r14 ret
Following hash functions were tested:
I observed that the string length is a decent discriminator: STL: map with string as key — access speedup. In most cases we obtain the length of strings in constant time; for instance this C++ function simplifies to a few CPU instructions.
#include <cstdint> #include <string_view> uint64_t len(std::string_view s) { return s.size(); }
$ g++ -O2 -S test.cpp $ cat test.s ... movq %rdi, %rax ret ...
Taking this into account, we first split the set of strings into the subsets of strings having the same length. These subsets are usually small. We can fine-tune looking up in these subsets independently. One common optimization that is present in sample code is using plain if-ladder of compares when a subset is small (has at most two or three elements).
Additionally, working with same-length strings enables more low-level optimizations:
Note: this method was inspired by the hashing procedure found in GNU gperf.
The presented method requires input string to have the same lengths. We guaranteed that property by pre-classifying the input string by its length, as described in the previous section.
gperf uses the following observation: not all input characters contribute to uniqueness of set. In most cases we may discard many input characters and such modified set still holds unique items.
For instance, five-letter Go keywords are break, const, defer, and range. The last character is sufficient to distinguish them:
break => ____k const => ____t defer => ____r range => ____e
In the case of six-letter keywords, we need two characters, as shown below. The letter 't' repeats at the last position, but the second character makes this set unique.
import => ___o_t return => ___u_n select => ___e_t struct => ___u_t switch => ___t_h
The major problem is that we need to combine somehow these unique characters into a unique and relatively small value. gperf adds the characters after passing them through an auxiliary lookup table. Like: lookup[s[0]] + lookup[s[5]].
Similarly to discarding individual bytes, we may discard individual bits, ending up with a bitmask that can be used to extract significant bits. To do this, we use a very fast instruction PEXT.
Before we dig into details, let us re-examine the examples. For five-letter Go keywords we have:
01100010.01110010.01100101.01100001.01101011 => ________.________.________.________.______11 01100011.01101111.01101110.01110011.01110100 => ________.________.________.________.______00 01100100.01100101.01100110.01100101.01110010 => ________.________.________.________.______10 01110010.01100001.01101110.01100111.01100101 => ________.________.________.________.______01
And for six-letter ones:
01101001.01101101.01110000.01101111.01110010.01110100 => ________.________.________.___0____._______0._____1__ 01110010.01100101.01110100.01110101.01110010.01101110 => ________.________.________.___1____._______0._____1__ 01110011.01100101.01101100.01100101.01100011.01110100 => ________.________.________.___0____._______1._____1__ 01110011.01110100.01110010.01110101.01100011.01110100 => ________.________.________.___1____._______1._____1__ 01110011.01110111.01101001.01110100.01100011.01101000 => ________.________.________.___1____._______1._____0__
Thus, in the first case we need just 2 bits, and in the second bits only 3 bits. The 2-bit subset contains all possible values (00, 01, 11, 10), while 3-bit subset is not full (001, 101, 011, 111, 110).
A skeleton of C++ procedure that uses this approach is:
... /* 1 */ const uint64_t word = load_bytes(s); /* 2 */ const uint64_t idx = _pext_u64(word, mask); /* 3 */ if (memcmp(lookup[idx], s.data(), size) == 0) { return value[idx]; } ...
Once we know which bits are significant, we need to find which bytes have to be loaded (1); a byte is the minimum amount of data we can transfer. As we can see, for five-letter keywords it is sufficient to load just one byte. But for six-letter keywords we need three bytes — in practise it is still a single load, but done on an 32-bit entity.
Then, we extract the bits using the instruction PEXT (2) — these bits form an n-bit index.
We need an auxiliary table(s) of size 2n, having exactly the same meaning as in hashing. We compare the input string with keyword (3) and if they equal, we return the associated value.
This is a snippet from procedure for looking up the Go keywords:
int lookup_go_pext(std::string_view s) { switch (s.size()) { // ... case 5: { static char lookup[4][5] = { {'b', 'r', 'e', 'a', 'k'}, {'r', 'a', 'n', 'g', 'e'}, {'d', 'e', 'f', 'e', 'r'}, {'c', 'o', 'n', 's', 't'}, }; static int value[4] = { 0, 18, 6, 3, }; const uint8_t w2 = s[4]; const uint64_t idx = _pext_u64(w2, 0x14); if (memcmp(lookup[idx], s.data(), sizeof(lookup[0])) == 0) { return value[idx]; } } break; case 6: { static char lookup[8][6] = { {}, // no match {'s', 'w', 'i', 't', 'c', 'h'}, {}, // no match {'r', 'e', 't', 'u', 'r', 'n'}, {'s', 'e', 'l', 'e', 'c', 't'}, {'s', 't', 'r', 'u', 'c', 't'}, {'i', 'm', 'p', 'o', 'r', 't'}, {}, // no match }; static int value[8] = { -1, 22, -1, 19, 20, 21, 14, -1, }; uint32_t w3 = 0; memcpy(&w3, &s[2], 4); const uint64_t idx = _pext_u64(w3, 0x10101000); if (memcmp(lookup[idx], s.data(), sizeof(lookup[0])) == 0) { return value[idx]; } } break; // ... } return -1; }
Having strings of the same length is not a strict requirement. We always can complete the past-end characters with some easy-to-compute values. The following values were tested during writing this text:
Depending on the chosen method and string set, the outcome differs. Including length and one of chars seems to help. However, pre-classifying inputs by their length appeared to be easier and faster, thus this topic is not covered here.
Finding the mask is surprisingly straightforward. We start with a full mask, and iterate over all bits. For each bit we unset it, and then we check if the set of masked bytes is still unique. If it's true, we keep that bit unset, otherwise revert the change.
Below are shown two procedures that compute the mask.
func computeMask(words []Keyword) (mask []byte, err error) { max := 0 for _, w := range words { if n := len(w.word); n > max { max = n } } mask = make([]byte, max) for i := range mask { mask[i] = 0xff } ok := checksamesize(words, mask) if !ok { err = fmt.Errorf("set of words is not unique") return } for byteIdx := 0; byteIdx < max; byteIdx++ { for bitIdx := 0; bitIdx < 8; bitIdx++ { old := mask[byteIdx] mask[byteIdx] &= ^(1 << bitIdx) if !checksamesize(words, mask) { mask[byteIdx] = old } } } return } func checksamesize(words []Keyword, mask []byte) bool { M := make(map[string]struct{}) word := make([]byte, len(mask)) for i := range words { for j := range mask { word[j] = words[i].word[j] & mask[j] } s := string(word) _, ok := M[s] if ok { return false } M[s] = struct{}{} } return true }
As it was shown in examples above, often a single load is sufficient, as the significant bits fit in a 8-, 16-, 32-, or 64-bit word.
However, sometimes more loads are needed. In the generic case we concatenate results of individual loads, forming a 32- or 64-bit word. Below is an example of such code, where we combine 8-bit and 16-bit input words into a single 32-bit word, which is the final argument to PEXT. Note that the we need only to concatenate values in runtime, the mask for PEXT is know in the compile-time.
uint32_t w1; uint16_t w2 = 0; memcpy(&w2, &s[0], 2); w1 = uint32_t(w2); const uint8_t w3 = s[2]; w1 |= (uint32_t(w3) << 16); const uint64_t idx = _pext_u64(w1, 0x1e1104);
Another possibility is when we have exactly two loads and their masks do not collide, their bit-and result is zero. Then we can merge these two words; sample code for such case is shown below.
uint16_t w7 = 0; memcpy(&w7, &s[10], 2); const uint8_t w8 = s[2]; const uint16_t w9 = (uint16_t(w7) & 0x410) | (uint16_t(w8) & 0x4); const uint64_t idx = _pext_u64(w9, 0x414);
To complete the image, we present a better alternative to compile-time trie expansion. As the first step, the method splits input by the length. Then we compute switch expression for same-lengths strings; each subset is processed separately.
Like in the trie approach, we use a single character to select further processing. But since we deal with subset of strings having the same sizes, we can pick a character at any index. We select an index that splits the subset into the greatest number of smaller subsets, ideally into single-element subsets (singletons). The process is repeated recursively, we need only to remember which indices were already used.
For example, the six-letter keywords of Java, can be fully classified by the second character. Five-letter keywords are classified by the last character, but for one case (the letter 't') more tests are needed.
Below is shown the corresponding fragment of lookup procedure.
int lookup_java_split(std::string_view s) { switch (s.size()) { // ... case 5: { switch (s[4]) { case 'w': { // 0x77 if (s == "throw") { return 4; } } break; case 'k': { // 0x6b if (s == "break") { return 10; } } break; case 't': { // 0x74 switch (s[0]) { case 's': { // 0x73 if (s == "short") { return 23; } } break; case 'f': { // 0x66 if (s == "float") { return 36; } } break; case 'c': { // 0x63 if (s == "const") { return 39; } } break; } } break; case 'h': { // 0x68 if (s == "catch") { return 25; } } break; case 'l': { // 0x6c if (s == "final") { return 26; } } break; case 'r': { // 0x72 if (s == "super") { return 33; } } break; case 'e': { // 0x65 if (s == "while") { return 34; } } break; case 's': { // 0x73 if (s == "class") { return 35; } } break; } } break; case 6: { switch (s[1]) { case 'h': { // 0x68 if (s == "throws") { return 9; } } break; case 'o': { // 0x6f if (s == "double") { return 11; } } break; case 'm': { // 0x6d if (s == "import") { return 12; } } break; case 'u': { // 0x75 if (s == "public") { return 13; } } break; case 'e': { // 0x65 if (s == "return") { return 18; } } break; case 't': { // 0x74 if (s == "static") { return 28; } } break; case 'a': { // 0x61 if (s == "native") { return 37; } } break; case 'w': { // 0x77 if (s == "switch") { return 38; } } break; } } break; // ... } return -1; }
The input sets contains mostly keywords from programming languages.
name | number of words | notes |
---|---|---|
ada-pred | 54 | gperf tests |
ada-res | 63 | gperf tests |
ada | 63 | gperf tests |
adadefs | 54 | gperf tests |
c | 32 | gperf tests |
countries | 192 | English names of countries |
cplusplus | 106 | gperf tests |
cpp | 47 | gperf tests |
go | 25 | |
gpc | 35 | gperf tests |
irc | 45 | gperf tests |
java | 47 | gperf tests |
jscript | 53 | gperf tests |
modula | 53 | gperf tests |
modula2 | 40 | gperf tests |
modula3 | 53 | gperf tests |
pascal | 36 | gperf tests |
sneller | 240 | SQL keywords and some functions from Sneller |
snellerbuiltin | 119 | builtin functions from Sneller |
states | 50 | USA state names |
The input words were repeated in random order to produce approx 1MB block of text, not counting the new line character.
Keywords were mutated by changing a single letter (at random position) into a random character or digit. Chances that keyword is added intact were set to: 0%, 25%, 50% and 75%. This is what we call "valid words density".
procedure | comments |
---|---|
pext | PEXT-based, bit-level function |
split | Compile-time switches |
hash1 | Compile-time hash table with hash1 function |
hash2 | Compile-time hash table with hash2 function |
hash3 | Compile-time hash table with hash3 function |
hash_sum0 | Compile-time hash table with hash_sum0 function |
hash_sumN | Compile-time hash table with hash_sumN function |
hash_djb2 | Compile-time hash table with hash_djb2 function |
hash_sdb | Compile-time hash table with hash_sdb function |
CPU: | Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz |
---|---|
Compiler: | g++-9 (Ubuntu 9.3.0-10ubuntu2~16.04) 9.3.0 |
The numbers are given in CPU cycles.
valid words density | ||||||||
---|---|---|---|---|---|---|---|---|
procedure | 0% | speed-up | 25% | speed-up | 50% | speed-up | 75% | speed-up |
dataset ada | ||||||||
pext | 33.20 | 1.000 | 41.47 | 1.000 | 45.67 | 1.000 | 39.57 | 1.000 |
split | 61.81 | 0.537 | 67.50 | 0.614 | 67.69 | 0.675 | 65.62 | 0.603 |
hash1 | 75.38 | 0.440 | 77.63 | 0.534 | 73.33 | 0.623 | 64.66 | 0.612 |
hash2 | 72.78 | 0.456 | 76.73 | 0.540 | 72.23 | 0.632 | 64.01 | 0.618 |
hash3 | 55.63 | 0.597 | 67.57 | 0.614 | 68.79 | 0.664 | 63.79 | 0.620 |
hash_sum0 | 47.63 | 0.697 | 60.64 | 0.684 | 69.10 | 0.661 | 69.96 | 0.566 |
hash_sumN | 50.68 | 0.655 | 61.91 | 0.670 | 70.02 | 0.652 | 68.14 | 0.581 |
hash_sdb | 39.91 | 0.832 | 50.90 | 0.815 | 61.03 | 0.748 | 56.61 | 0.699 |
hash_djb2 | 41.22 | 0.805 | 51.66 | 0.803 | 60.32 | 0.757 | 55.12 | 0.718 |
dataset ada-pred | ||||||||
pext | 37.41 | 1.000 | 45.55 | 1.000 | 46.60 | 1.000 | 41.09 | 1.000 |
split | 63.65 | 0.588 | 68.54 | 0.665 | 66.71 | 0.699 | 62.59 | 0.657 |
hash1 | 87.09 | 0.430 | 88.23 | 0.516 | 84.02 | 0.555 | 70.16 | 0.586 |
hash2 | 88.20 | 0.424 | 89.78 | 0.507 | 79.61 | 0.585 | 70.49 | 0.583 |
hash3 | 64.07 | 0.584 | 64.80 | 0.703 | 60.81 | 0.766 | 49.78 | 0.825 |
hash_sum0 | 49.99 | 0.748 | 62.74 | 0.726 | 70.37 | 0.662 | 71.98 | 0.571 |
hash_sumN | 52.95 | 0.706 | 64.19 | 0.710 | 75.90 | 0.614 | 74.72 | 0.550 |
hash_sdb | 49.55 | 0.755 | 61.30 | 0.743 | 72.63 | 0.642 | 66.59 | 0.617 |
hash_djb2 | 46.79 | 0.800 | 58.65 | 0.777 | 66.92 | 0.696 | 64.21 | 0.640 |
dataset ada-res | ||||||||
pext | 33.28 | 1.000 | 41.21 | 1.000 | 45.13 | 1.000 | 39.77 | 1.000 |
split | 65.65 | 0.507 | 71.41 | 0.577 | 67.72 | 0.666 | 69.91 | 0.569 |
hash1 | 74.25 | 0.448 | 76.05 | 0.542 | 73.33 | 0.615 | 62.88 | 0.632 |
hash2 | 73.24 | 0.454 | 78.54 | 0.525 | 72.33 | 0.624 | 65.06 | 0.611 |
hash3 | 54.56 | 0.610 | 66.09 | 0.623 | 69.05 | 0.654 | 62.02 | 0.641 |
hash_sum0 | 47.62 | 0.699 | 58.82 | 0.700 | 72.94 | 0.619 | 68.58 | 0.580 |
hash_sumN | 50.34 | 0.661 | 63.12 | 0.653 | 70.00 | 0.645 | 65.14 | 0.610 |
hash_sdb | 40.05 | 0.831 | 51.05 | 0.807 | 61.46 | 0.734 | 56.13 | 0.709 |
hash_djb2 | 41.53 | 0.801 | 52.13 | 0.790 | 60.56 | 0.745 | 56.34 | 0.706 |
dataset adadefs | ||||||||
pext | 37.85 | 1.000 | 45.68 | 1.000 | 46.70 | 1.000 | 41.30 | 1.000 |
split | 62.59 | 0.605 | 67.59 | 0.676 | 67.50 | 0.692 | 63.26 | 0.653 |
hash1 | 90.23 | 0.419 | 91.55 | 0.499 | 82.56 | 0.566 | 72.14 | 0.573 |
hash2 | 85.91 | 0.441 | 86.50 | 0.528 | 82.34 | 0.567 | 67.33 | 0.613 |
hash3 | 66.51 | 0.569 | 67.66 | 0.675 | 58.06 | 0.804 | 52.81 | 0.782 |
hash_sum0 | 48.92 | 0.774 | 60.24 | 0.758 | 74.57 | 0.626 | 68.25 | 0.605 |
hash_sumN | 53.62 | 0.706 | 66.31 | 0.689 | 73.98 | 0.631 | 78.04 | 0.529 |
hash_sdb | 50.43 | 0.750 | 62.91 | 0.726 | 72.20 | 0.647 | 64.81 | 0.637 |
hash_djb2 | 45.90 | 0.824 | 58.03 | 0.787 | 69.26 | 0.674 | 62.68 | 0.659 |
dataset c | ||||||||
pext | 31.99 | 1.000 | 40.50 | 1.000 | 44.03 | 1.000 | 38.06 | 1.000 |
split | 50.58 | 0.632 | 55.28 | 0.733 | 58.78 | 0.749 | 53.15 | 0.716 |
hash1 | 61.26 | 0.522 | 62.69 | 0.646 | 58.98 | 0.746 | 49.40 | 0.770 |
hash2 | 61.59 | 0.519 | 62.57 | 0.647 | 53.50 | 0.823 | 48.16 | 0.790 |
hash3 | 51.56 | 0.620 | 58.94 | 0.687 | 56.15 | 0.784 | 45.65 | 0.834 |
hash_sum0 | 50.07 | 0.639 | 62.08 | 0.652 | 69.28 | 0.635 | 67.87 | 0.561 |
hash_sumN | 45.62 | 0.701 | 58.30 | 0.695 | 66.23 | 0.665 | 61.98 | 0.614 |
hash_sdb | 42.02 | 0.761 | 52.80 | 0.767 | 61.86 | 0.712 | 52.40 | 0.726 |
hash_djb2 | 40.45 | 0.791 | 51.62 | 0.784 | 58.87 | 0.748 | 55.34 | 0.688 |
dataset countries | ||||||||
pext | 36.51 | 1.000 | 45.13 | 1.000 | 47.62 | 1.000 | 41.73 | 1.000 |
split | 97.77 | 0.373 | 103.25 | 0.437 | 102.72 | 0.464 | 94.96 | 0.439 |
hash1 | 132.88 | 0.275 | 129.03 | 0.350 | 116.18 | 0.410 | 100.02 | 0.417 |
hash2 | 129.50 | 0.282 | 126.89 | 0.356 | 118.28 | 0.403 | 99.82 | 0.418 |
hash3 | 92.53 | 0.395 | 97.42 | 0.463 | 88.54 | 0.538 | 79.79 | 0.523 |
hash_sum0 | 62.07 | 0.588 | 78.25 | 0.577 | 86.10 | 0.553 | 88.92 | 0.469 |
hash_sumN | 63.41 | 0.576 | 80.48 | 0.561 | 84.76 | 0.562 | 89.56 | 0.466 |
hash_sdb | 50.96 | 0.716 | 64.70 | 0.698 | 76.18 | 0.625 | 76.97 | 0.542 |
hash_djb2 | 63.47 | 0.575 | 74.61 | 0.605 | 87.23 | 0.546 | 86.02 | 0.485 |
dataset cplusplus | ||||||||
pext | 33.82 | 1.000 | 42.58 | 1.000 | 46.32 | 1.000 | 40.12 | 1.000 |
split | 67.07 | 0.504 | 72.82 | 0.585 | 72.88 | 0.636 | 71.07 | 0.565 |
hash1 | 109.82 | 0.308 | 109.25 | 0.390 | 101.32 | 0.457 | 90.27 | 0.444 |
hash2 | 96.47 | 0.351 | 97.92 | 0.435 | 92.32 | 0.502 | 81.82 | 0.490 |
hash3 | 86.25 | 0.392 | 91.60 | 0.465 | 87.83 | 0.527 | 78.80 | 0.509 |
hash_sum0 | 51.95 | 0.651 | 66.86 | 0.637 | 76.79 | 0.603 | 80.85 | 0.496 |
hash_sumN | 57.29 | 0.590 | 70.77 | 0.602 | 81.49 | 0.568 | 87.37 | 0.459 |
hash_sdb | 44.25 | 0.764 | 56.91 | 0.748 | 67.76 | 0.684 | 59.27 | 0.677 |
hash_djb2 | 45.77 | 0.739 | 56.94 | 0.748 | 66.75 | 0.694 | 62.54 | 0.642 |
dataset cpp | ||||||||
pext | 31.13 | 1.000 | 39.15 | 1.000 | 43.77 | 1.000 | 37.27 | 1.000 |
split | 60.01 | 0.519 | 64.33 | 0.609 | 61.03 | 0.717 | 61.17 | 0.609 |
hash1 | 79.09 | 0.394 | 80.43 | 0.487 | 79.58 | 0.550 | 64.79 | 0.575 |
hash2 | 77.41 | 0.402 | 79.82 | 0.490 | 71.89 | 0.609 | 64.33 | 0.579 |
hash3 | 52.87 | 0.589 | 58.08 | 0.674 | 54.88 | 0.798 | 45.60 | 0.817 |
hash_sum0 | 47.12 | 0.661 | 60.08 | 0.652 | 67.08 | 0.653 | 65.49 | 0.569 |
hash_sumN | 48.65 | 0.640 | 62.08 | 0.631 | 68.92 | 0.635 | 67.31 | 0.554 |
hash_sdb | 42.10 | 0.740 | 52.74 | 0.742 | 61.94 | 0.707 | 55.81 | 0.668 |
hash_djb2 | 40.41 | 0.770 | 50.86 | 0.770 | 58.90 | 0.743 | 55.69 | 0.669 |
dataset go | ||||||||
pext | 34.09 | 1.000 | 42.02 | 1.000 | 44.33 | 1.000 | 38.98 | 1.000 |
split | 53.48 | 0.638 | 57.68 | 0.729 | 53.66 | 0.826 | 52.37 | 0.744 |
hash1 | 77.18 | 0.442 | 80.18 | 0.524 | 73.74 | 0.601 | 67.21 | 0.580 |
hash2 | 75.88 | 0.449 | 79.44 | 0.529 | 77.16 | 0.575 | 64.39 | 0.605 |
hash3 | 50.11 | 0.680 | 60.97 | 0.689 | 52.59 | 0.843 | 49.99 | 0.780 |
hash_sum0 | 45.55 | 0.748 | 55.76 | 0.754 | 64.22 | 0.690 | 59.05 | 0.660 |
hash_sumN | 43.86 | 0.777 | 54.65 | 0.769 | 60.94 | 0.728 | 56.83 | 0.686 |
hash_sdb | 44.85 | 0.760 | 56.65 | 0.742 | 65.52 | 0.677 | 55.36 | 0.704 |
hash_djb2 | 47.05 | 0.725 | 57.45 | 0.731 | 65.85 | 0.673 | 59.28 | 0.658 |
dataset gpc | ||||||||
pext | 31.63 | 1.000 | 39.37 | 1.000 | 43.57 | 1.000 | 37.99 | 1.000 |
split | 54.04 | 0.585 | 58.34 | 0.675 | 60.28 | 0.723 | 55.08 | 0.690 |
hash1 | 67.64 | 0.468 | 77.23 | 0.510 | 77.72 | 0.561 | 67.73 | 0.561 |
hash2 | 56.41 | 0.561 | 61.56 | 0.640 | 55.65 | 0.783 | 49.95 | 0.761 |
hash3 | 36.41 | 0.869 | 51.94 | 0.758 | 53.33 | 0.817 | 44.76 | 0.849 |
hash_sum0 | 48.02 | 0.659 | 59.74 | 0.659 | 68.07 | 0.640 | 69.51 | 0.547 |
hash_sumN | 48.15 | 0.657 | 59.59 | 0.661 | 67.78 | 0.643 | 64.33 | 0.591 |
hash_sdb | 39.74 | 0.796 | 51.98 | 0.757 | 60.72 | 0.718 | 55.70 | 0.682 |
hash_djb2 | 45.31 | 0.698 | 56.10 | 0.702 | 62.26 | 0.700 | 56.72 | 0.670 |
dataset irc | ||||||||
pext | 30.92 | 1.000 | 39.45 | 1.000 | 43.61 | 1.000 | 35.90 | 1.000 |
split | 61.98 | 0.499 | 66.63 | 0.592 | 67.25 | 0.649 | 63.39 | 0.566 |
hash1 | 91.91 | 0.336 | 91.33 | 0.432 | 84.69 | 0.515 | 75.90 | 0.473 |
hash2 | 73.70 | 0.420 | 76.54 | 0.515 | 75.02 | 0.581 | 61.11 | 0.587 |
hash3 | 49.63 | 0.623 | 58.92 | 0.670 | 50.82 | 0.858 | 46.29 | 0.776 |
hash_sum0 | 52.71 | 0.587 | 65.81 | 0.600 | 73.05 | 0.597 | 67.20 | 0.534 |
hash_sumN | 57.54 | 0.537 | 71.23 | 0.554 | 75.28 | 0.579 | 70.94 | 0.506 |
hash_sdb | 41.56 | 0.744 | 52.76 | 0.748 | 61.69 | 0.707 | 56.09 | 0.640 |
hash_djb2 | 41.20 | 0.751 | 52.06 | 0.758 | 62.05 | 0.703 | 56.49 | 0.636 |
dataset java | ||||||||
pext | 32.81 | 1.000 | 41.97 | 1.000 | 44.86 | 1.000 | 40.05 | 1.000 |
split | 61.79 | 0.531 | 63.11 | 0.665 | 67.20 | 0.668 | 60.71 | 0.660 |
hash1 | 84.10 | 0.390 | 87.53 | 0.479 | 80.63 | 0.556 | 70.61 | 0.567 |
hash2 | 79.72 | 0.412 | 79.53 | 0.528 | 74.78 | 0.600 | 61.47 | 0.652 |
hash3 | 54.22 | 0.605 | 61.39 | 0.684 | 52.91 | 0.848 | 48.92 | 0.819 |
hash_sum0 | 50.32 | 0.652 | 64.39 | 0.652 | 73.70 | 0.609 | 79.53 | 0.504 |
hash_sumN | 51.68 | 0.635 | 64.23 | 0.654 | 70.61 | 0.635 | 72.76 | 0.550 |
hash_sdb | 43.18 | 0.760 | 55.42 | 0.757 | 64.60 | 0.694 | 57.78 | 0.693 |
hash_djb2 | 43.35 | 0.757 | 53.60 | 0.783 | 64.25 | 0.698 | 59.08 | 0.678 |
dataset jscript | ||||||||
pext | 32.62 | 1.000 | 41.07 | 1.000 | 45.48 | 1.000 | 39.26 | 1.000 |
split | 55.46 | 0.588 | 60.59 | 0.678 | 63.60 | 0.715 | 57.62 | 0.681 |
hash1 | 76.26 | 0.428 | 78.06 | 0.526 | 76.60 | 0.594 | 63.68 | 0.617 |
hash2 | 74.21 | 0.440 | 75.27 | 0.546 | 67.39 | 0.675 | 60.76 | 0.646 |
hash3 | 53.13 | 0.614 | 60.49 | 0.679 | 57.44 | 0.792 | 49.12 | 0.799 |
hash_sum0 | 50.57 | 0.645 | 62.13 | 0.661 | 71.23 | 0.639 | 71.21 | 0.551 |
hash_sumN | 53.58 | 0.609 | 64.87 | 0.633 | 76.94 | 0.591 | 76.87 | 0.511 |
hash_sdb | 46.41 | 0.703 | 57.42 | 0.715 | 68.15 | 0.667 | 58.71 | 0.669 |
hash_djb2 | 44.39 | 0.735 | 54.69 | 0.751 | 63.35 | 0.718 | 59.26 | 0.663 |
dataset modula | ||||||||
pext | 32.74 | 1.000 | 40.76 | 1.000 | 45.30 | 1.000 | 39.60 | 1.000 |
split | 60.07 | 0.545 | 64.43 | 0.633 | 63.79 | 0.710 | 61.26 | 0.646 |
hash1 | 69.22 | 0.473 | 71.27 | 0.572 | 66.81 | 0.678 | 58.11 | 0.682 |
hash2 | 71.44 | 0.458 | 76.04 | 0.536 | 71.22 | 0.636 | 62.85 | 0.630 |
hash3 | 43.89 | 0.746 | 56.34 | 0.723 | 51.91 | 0.873 | 45.92 | 0.862 |
hash_sum0 | 48.84 | 0.670 | 60.48 | 0.674 | 69.19 | 0.655 | 69.94 | 0.566 |
hash_sumN | 50.78 | 0.645 | 61.46 | 0.663 | 70.09 | 0.646 | 70.17 | 0.564 |
hash_sdb | 44.64 | 0.733 | 55.30 | 0.737 | 63.62 | 0.712 | 57.84 | 0.685 |
hash_djb2 | 42.33 | 0.773 | 52.38 | 0.778 | 61.94 | 0.731 | 56.49 | 0.701 |
dataset modula2 | ||||||||
pext | 31.87 | 1.000 | 40.22 | 1.000 | 44.70 | 1.000 | 38.37 | 1.000 |
split | 58.01 | 0.549 | 62.07 | 0.648 | 57.86 | 0.773 | 58.33 | 0.658 |
hash1 | 69.44 | 0.459 | 74.37 | 0.541 | 67.05 | 0.667 | 60.74 | 0.632 |
hash2 | 69.66 | 0.458 | 78.43 | 0.513 | 77.69 | 0.575 | 65.48 | 0.586 |
hash3 | 41.98 | 0.759 | 56.11 | 0.717 | 51.12 | 0.874 | 47.33 | 0.811 |
hash_sum0 | 45.71 | 0.697 | 57.02 | 0.705 | 71.58 | 0.624 | 64.89 | 0.591 |
hash_sumN | 53.05 | 0.601 | 63.66 | 0.632 | 74.12 | 0.603 | 68.24 | 0.562 |
hash_sdb | 45.22 | 0.705 | 55.33 | 0.727 | 63.21 | 0.707 | 59.78 | 0.642 |
hash_djb2 | 40.88 | 0.780 | 51.74 | 0.777 | 61.84 | 0.723 | 56.80 | 0.676 |
dataset modula3 | ||||||||
pext | 32.15 | 1.000 | 40.28 | 1.000 | 45.30 | 1.000 | 39.02 | 1.000 |
split | 56.67 | 0.567 | 61.46 | 0.655 | 63.68 | 0.711 | 58.65 | 0.665 |
hash1 | 70.24 | 0.458 | 73.43 | 0.549 | 70.79 | 0.640 | 60.96 | 0.640 |
hash2 | 71.59 | 0.449 | 76.39 | 0.527 | 69.77 | 0.649 | 63.72 | 0.612 |
hash3 | 43.70 | 0.736 | 56.68 | 0.711 | 55.22 | 0.820 | 46.28 | 0.843 |
hash_sum0 | 50.16 | 0.641 | 61.09 | 0.659 | 68.90 | 0.657 | 70.24 | 0.556 |
hash_sumN | 50.20 | 0.640 | 61.30 | 0.657 | 69.35 | 0.653 | 69.73 | 0.560 |
hash_sdb | 40.66 | 0.791 | 51.76 | 0.778 | 61.05 | 0.742 | 51.66 | 0.755 |
hash_djb2 | 40.96 | 0.785 | 51.04 | 0.789 | 59.26 | 0.764 | 56.20 | 0.694 |
dataset pascal | ||||||||
pext | 32.61 | 1.000 | 40.32 | 1.000 | 44.18 | 1.000 | 38.97 | 1.000 |
split | 56.47 | 0.578 | 61.20 | 0.659 | 58.03 | 0.761 | 58.26 | 0.669 |
hash1 | 66.03 | 0.494 | 75.19 | 0.536 | 70.14 | 0.630 | 65.05 | 0.599 |
hash2 | 57.36 | 0.569 | 59.72 | 0.675 | 56.49 | 0.782 | 46.41 | 0.840 |
hash3 | 39.59 | 0.824 | 55.37 | 0.728 | 51.04 | 0.865 | 48.82 | 0.798 |
hash_sum0 | 42.79 | 0.762 | 52.50 | 0.768 | 59.01 | 0.749 | 54.74 | 0.712 |
hash_sumN | 49.75 | 0.656 | 60.97 | 0.661 | 67.10 | 0.658 | 66.58 | 0.585 |
hash_sdb | 45.78 | 0.712 | 56.05 | 0.719 | 63.83 | 0.692 | 56.62 | 0.688 |
hash_djb2 | 41.33 | 0.789 | 52.38 | 0.770 | 62.20 | 0.710 | 55.77 | 0.699 |
dataset sneller | ||||||||
pext | 36.32 | 1.000 | 45.18 | 1.000 | 48.15 | 1.000 | 41.59 | 1.000 |
split | 84.98 | 0.427 | 91.96 | 0.491 | 91.27 | 0.528 | 85.14 | 0.489 |
hash1 | 111.37 | 0.326 | 109.84 | 0.411 | 101.95 | 0.472 | 87.03 | 0.478 |
hash2 | 107.71 | 0.337 | 108.25 | 0.417 | 99.64 | 0.483 | 84.69 | 0.491 |
hash3 | 85.25 | 0.426 | 91.38 | 0.494 | 86.67 | 0.556 | 76.52 | 0.543 |
hash_sum0 | 63.46 | 0.572 | 79.08 | 0.571 | 87.85 | 0.548 | 90.40 | 0.460 |
hash_sumN | 68.17 | 0.533 | 81.57 | 0.554 | 90.65 | 0.531 | 92.30 | 0.451 |
hash_sdb | 50.37 | 0.721 | 64.13 | 0.705 | 77.16 | 0.624 | 80.36 | 0.518 |
hash_djb2 | 67.26 | 0.540 | 80.46 | 0.562 | 92.01 | 0.523 | 94.85 | 0.438 |
dataset snellerbuiltin | ||||||||
pext | 39.24 | 1.000 | 47.75 | 1.000 | 49.16 | 1.000 | 43.15 | 1.000 |
split | 67.34 | 0.583 | 72.82 | 0.656 | 70.72 | 0.695 | 70.04 | 0.616 |
hash1 | 118.59 | 0.331 | 116.16 | 0.411 | 109.08 | 0.451 | 89.63 | 0.481 |
hash2 | 123.17 | 0.319 | 120.14 | 0.397 | 105.14 | 0.468 | 90.62 | 0.476 |
hash3 | 105.75 | 0.371 | 109.16 | 0.437 | 102.81 | 0.478 | 87.78 | 0.492 |
hash_sum0 | 62.24 | 0.630 | 74.92 | 0.637 | 84.30 | 0.583 | 86.59 | 0.498 |
hash_sumN | 59.27 | 0.662 | 70.96 | 0.673 | 82.69 | 0.594 | 81.20 | 0.531 |
hash_sdb | 53.43 | 0.734 | 68.37 | 0.698 | 81.32 | 0.604 | 74.38 | 0.580 |
hash_djb2 | 59.08 | 0.664 | 72.51 | 0.658 | 83.62 | 0.588 | 86.05 | 0.501 |
dataset states | ||||||||
pext | 37.22 | 1.000 | 45.84 | 1.000 | 47.53 | 1.000 | 41.11 | 1.000 |
split | 66.25 | 0.562 | 71.06 | 0.645 | 69.24 | 0.686 | 64.75 | 0.635 |
hash1 | 88.44 | 0.421 | 88.77 | 0.516 | 80.99 | 0.587 | 67.90 | 0.605 |
hash2 | 95.52 | 0.390 | 95.42 | 0.480 | 87.17 | 0.545 | 73.57 | 0.559 |
hash3 | 66.76 | 0.557 | 67.49 | 0.679 | 60.97 | 0.779 | 52.47 | 0.784 |
hash_sum0 | 48.00 | 0.775 | 62.88 | 0.729 | 69.39 | 0.685 | 64.14 | 0.641 |
hash_sumN | 51.32 | 0.725 | 63.15 | 0.726 | 68.72 | 0.692 | 64.42 | 0.638 |
hash_sdb | 45.26 | 0.822 | 59.31 | 0.773 | 71.25 | 0.667 | 60.47 | 0.680 |
hash_djb2 | 48.32 | 0.770 | 59.57 | 0.770 | 70.71 | 0.672 | 64.98 | 0.633 |
All sources are available on Github: https://github.com/WojciechMula/toys/tree/master/lookup-in-string-set.