nostrdb

an unfairly fast embedded nostr database backed by lmdb
git clone git://jb55.com/nostrdb
Log | Files | Refs | Submodules | README | LICENSE

hash_table_impl_rh.h (11973B)


      1 /*
      2  * The MIT License (MIT)
      3  *
      4  * Copyright (c) 2015 Mikkel F. Jørgensen, dvide.com
      5  *
      6  * Permission is hereby granted, free of charge, to any person obtaining a copy
      7  * of this software and associated documentation files (the "Software"), to deal
      8  * in the Software without restriction, including without limitation the rights
      9  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     10  * copies of the Software, and to permit persons to whom the Software is
     11  * furnished to do so, subject to the following conditions:
     12  *
     13  * The above copyright notice and this permission notice shall be included in all
     14  * copies or substantial portions of the Software.
     15  *
     16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     19  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     21  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     22  * SOFTWARE.
     23  */
     24 
     25 /* We use the same define for all implementations */
     26 #ifdef HASH_TABLE_IMPL
     27 #error "cannot have multiple implementations in same compilation unit"
     28 #endif
     29 #define HASH_TABLE_IMPL
     30 /* Robin Hood (with offset table) */
     31 #define HT_RH
     32 
     33 #if defined(_MSC_VER)
     34 #pragma warning(disable: 4127) /* conditional expression is constant */
     35 #endif
     36 
     37 #include <stdlib.h>
     38 #include <assert.h>
     39 
     40 /*
     41  * A variation of Robin Hashing:
     42  * We do not calcute distance from buckets, nor do we cache
     43  * hash keys. Instead we maintain a 7-bit offset that points
     44  * to where the first entry of a bucket is stored. In Robin Hood hashing
     45  * all entries conceptually chained to the same bucket are stored
     46  * immediately after each other in order of insertion. The offset of
     47  * the next bucket is naturally the end of the previous bucket, off by
     48  * one. This breaks down when the bucket offset is 0 and the bucket is
     49  * empty because it suggests there is an element. We cannot distinguish
     50  * between a single used and unused entry, except by looking at the
     51  * content or otherwise tag the information on. This is not a problem,
     52  * just a special case to deal with.
     53  *
     54  * The offsets are stored separately which might lead to more cache line
     55  * traffic, but the alternative is not very elegant - either wasting
     56  * space or trying to pack offsets on a per cache line basis. We only
     57  * need 8 bits for offsets. If the offset overflows, bit 7 will be set
     58  * which we can easily detect. During insertion, offsets are increated
     59  * on all affected buckets, and likewise decrement on remove. In
     60  * principle we can use bit parallel increments to update most offsets
     61  * in a single operation, but it is hardly worthwhile due to setup
     62  * cost. The approach bears some resemblance to hopscotch hashing which
     63  * uses local offsets for chaining, but we prefer the simpler Robin
     64  * Hood approach.
     65  *
     66  * If the offset overflows, the table is resized. We expect the packed
     67  * chains to behave like a special case of a hopscotch layout and
     68  * consequently have the same bounds, meaning we are unlikely to have
     69  * neither long offsets nor long chains if we resize below very full
     70  * so resizing on an offset of 128 should be ok.
     71  *
     72  * Our main motivation for this hashing is actually to get rid of
     73  * tombstones in quadruatic and linear probing. Avoiding tombstones
     74  * is much simpler when sorting chains Robin Hood style, and we avoid
     75  * checking for tombstones. We loose this benefit by having to inspect
     76  * offsets, but then also avoid checking keys before the chain, and
     77  * after because we can zero in on exactly the entries belonging to
     78  * bucket.
     79  *
     80  * Unlike traditional Robin Hood, we can find a missing key very quickly
     81  * without any heuristics: we only need to inspect exactly the number
     82  * of entries in the bucket (or at most 1 if the bucket is empty).
     83  *
     84  * Find operations start exactly at an entry with a matching hash key
     85  * unlike normal Robin Hood which must scan past a few earlier entries
     86  * on average, or guestimate where to start and seek both ways.
     87  *
     88  * We can also very quickly insert a key that is known to be unique
     89  * because we can add it directly to the end (but possibly requiring
     90  * a shift of later entries Robin Hood style).
     91  *
     92  * Whether these benefits outweighs the cost of a separate offset
     93  * lookup is unclear, but the reduced memory consumption certainly
     94  * allows for a lower load factor, which also helps a lot.
     95  *
     96  * Traditional Robin Hood Hashing actually permits a chain to become
     97  * very long. We do not permit this, in line with hopscotch hashing.
     98  * This is a drawback from a security perspective because worst case
     99  * this can trigger resizing ad infinitum iff the hash function can
    100  * be hacked or massive duplicate key insertion can be triggered. By
    101  * used the provided hash functions and seeding them randomly at
    102  * startup, and avoiding the multi key feature, it is very unlikely to
    103  * be a problem with what is known about hash table attacks so far.
    104  *
    105  * Values and keys are not stored, only item pointers. Custom macroes
    106  * or inline functions provide access to key data from the item. We
    107  * could add a separate value array and treat the item strictly as a
    108  * key, but we can have a smaller load factor instead, and can more
    109  * easily avoid copying complex key structures, such as start end
    110  * pointers to token data for parser.
    111  *
    112  * A typical hash table has: key pointer or key value, value pointer
    113  * or value, a cached hash key or bitmap (for Robin Hood or Hopscotch)
    114  * which on 64 bit platforms easily amounts to 20 bytes or more per
    115  * bucket. We use 9 bytes on 64 bit platforms and 5 bytes on 32 bit.
    116  * This gets us down to a max load of 0.5 and on average about 0.37.
    117  * This should make it very likely that the first bucket inspected is
    118  * a direct hit negating the benefit of caching hash keys. In addition,
    119  * when it is not a direct hit, we get pointers loaded in a cache line
    120  * to inspect, all known to have the same hash key.
    121  */
    122 
    123 int ht_init(hash_table_t *ht, size_t count)
    124 {
    125     size_t buckets = 4;
    126 
    127     if ((HT_LOAD_FACTOR_FRAC) > 256 || (HT_LOAD_FACTOR_FRAC) < 1) {
    128         /*
    129          * 101% will never terminate insertion.
    130          * 0% will never terminate resize.
    131          */
    132         HT_PANIC("robin hood hash table failed with impossible load factor");
    133         return -1;
    134     }
    135     while (count > buckets * (HT_LOAD_FACTOR_FRAC) / 256) {
    136         buckets *= 2;
    137     }
    138     ht->table = calloc(buckets, sizeof(ht_item_t));
    139     if (ht->table == 0) {
    140         return -1;
    141     }
    142     ht->offsets = calloc(buckets, sizeof(char));
    143     if (ht->offsets == 0) {
    144         free(ht->table);
    145         ht->table = 0;
    146         return -1;
    147     }
    148     ht->buckets = buckets;
    149     ht->count = 0;
    150     return 0;
    151 }
    152 
    153 int ht_resize(hash_table_t *ht, size_t count)
    154 {
    155     size_t i;
    156     hash_table_t ht2;
    157     ht_item_t *T = ht->table;
    158     ht_item_t item;
    159 
    160     if (count < ht->count) {
    161         count = ht->count;
    162     }
    163     if (ht_init(&ht2, count)) {
    164         return -1;
    165     }
    166     for (i = 0; i < ht->buckets; ++i) {
    167         item = T[i];
    168         if (item > (ht_item_t)1) {
    169             ht_insert(&ht2, ht_key(item), ht_key_len(item), item, ht_multi);
    170         }
    171     }
    172     ht_clear(ht);
    173     memcpy(ht, &ht2, sizeof(*ht));
    174     return 0;
    175 }
    176 
    177 ht_item_t ht_insert(hash_table_t *ht,
    178         const void *key, size_t len, ht_item_t item, int mode)
    179 {
    180     ht_item_t *T;
    181     size_t N, n, j, k, offset;
    182     ht_item_t new_item;
    183     char overflow = 0;
    184 
    185     new_item = item;
    186     if (ht->count >= ht->buckets * (HT_LOAD_FACTOR_FRAC) / 256) {
    187         if (ht_resize(ht, ht->count * 2)) {
    188             HT_PANIC("robin hood hash table failed to allocate memory during resize");
    189             return HT_NOMEM;
    190         }
    191     }
    192     T = ht->table;
    193     N = ht->buckets - 1;
    194     k = HT_HASH_FUNCTION(key, len) & N;
    195     offset = ht->offsets[k];
    196     j = (k + offset) & N;
    197     /*
    198      * T[j] == 0 is a special case because we cannot count
    199      * zero probe length, and because we should not increment
    200      * the offset at insertion point in this case.
    201      *
    202      * T[j] == 0 implies offset == 0, but this way we avoid
    203      * hitting memory that we don't need.
    204      */
    205     if (offset == 0 && T[j] == 0) {
    206         ++ht->count;
    207         T[j] = new_item;
    208         return 0;
    209     }
    210     n = ht->offsets[(k + 1) & N] - offset + 1;
    211     if (mode == ht_multi) {
    212         /* Don't search for match before inserting. */
    213         j = (j + n) & N;
    214         n = 0;
    215     }
    216     while (n--) {
    217         item = T[j];
    218         if (ht_match(key, len, item)) {
    219             if (mode == ht_replace) {
    220                 T[j] = new_item;
    221             }
    222             return item;
    223         }
    224         j = (j + 1) & N;
    225     }
    226     ++ht->count;
    227     while (k != j) {
    228         /* Only increment buckets after own bucket. */
    229         k = (k + 1) & N;
    230         overflow |= ++ht->offsets[k];
    231     }
    232     while ((item = T[j])) {
    233         T[j] = new_item;
    234         new_item = item;
    235         j = (j + 1) & N;
    236         overflow |= ++ht->offsets[j];
    237     }
    238     T[j] = new_item;
    239 
    240     if (overflow < 0) {
    241         /*
    242          * At least one offset overflowed, so we need to
    243          * resize the table.
    244          */
    245         if (ht->count * 10 < ht->buckets) {
    246             HT_PANIC("FATAL: hash table resize on low utilization would explode\n"\
    247                    "  possible collision DoS or bad hash function");
    248             return HT_NOMEM;
    249         }
    250         if (ht_resize(ht, ht->count * 2)) {
    251             HT_PANIC("FATAL: hash table resize failed and left hash table inconsistent");\
    252             /*
    253              * This renders the hash table in a bad state
    254              * because we have updated to an inconsistent
    255              * state.
    256              */
    257             return HT_NOMEM;
    258         }
    259     }
    260     return item;
    261 }
    262 
    263 ht_item_t ht_find(hash_table_t *ht, const void *key, size_t len)
    264 {
    265     ht_item_t *T = ht->table;
    266     size_t N, n, j, k, offset;
    267     ht_item_t item;
    268 
    269     if (T == 0) {
    270         return 0;
    271     }
    272     N = ht->buckets - 1;
    273     k = HT_HASH_FUNCTION(key, len) & N;
    274     offset = ht->offsets[k];
    275     j = (k + offset) & N;
    276     if (offset == 0 && T[j] == 0) {
    277         /* Special case because we cannot count zero probe length. */
    278         return 0;
    279     }
    280     n = ht->offsets[(k + 1) & N] - offset + 1;
    281     while (n--) {
    282         item = T[j];
    283         if (ht_match(key, len, item)) {
    284             return item;
    285         }
    286         j = (j + 1) & N;
    287     }
    288     return 0;
    289 }
    290 
    291 ht_item_t ht_remove(hash_table_t *ht, const void *key, size_t len)
    292 {
    293     ht_item_t *T = ht->table;
    294     size_t N, n, j, k, offset;
    295     ht_item_t item, *next_item;
    296 
    297     if (T == 0) {
    298         return 0;
    299     }
    300     N = ht->buckets - 1;
    301     k = HT_HASH_FUNCTION(key, len) & N;
    302     offset = ht->offsets[k];
    303     j = (k + offset) & N;
    304     if (offset == 0 && T[j] == 0) {
    305         return 0;
    306     }
    307     n = ht->offsets[(k + 1) & N] - offset + 1;
    308     while (n) {
    309         item = T[j];
    310         if (ht_match(key, len, item)) {
    311             break;
    312         }
    313         j = (j + 1) & N;
    314         --n;
    315     }
    316     if (n == 0) {
    317         return 0;
    318     }
    319     --ht->count;
    320     while (k != j) {
    321         /* Do not update the offset of the bucket that we own. */
    322         k = (k + 1) & N;
    323         --ht->offsets[k];
    324     }
    325     for (;;) {
    326         j = (j + 1) & N;
    327         if (ht->offsets[j] == 0) {
    328             T[k] = 0;
    329             return item;
    330         }
    331         --ht->offsets[j];
    332         T[k] = T[j];
    333         k = j;
    334     }
    335 }
    336 
    337 void ht_visit(hash_table_t *ht, ht_visitor_f *visitor, void *context)
    338 {
    339     size_t i;
    340     ht_item_t *T = ht->table;
    341     ht_item_t item;
    342 
    343     for (i = 0; i < ht->buckets; ++i) {
    344         item = T[i];
    345         if (item > (ht_item_t)1) {
    346             visitor(context, item);
    347         }
    348     }
    349 }
    350 
    351 void ht_clear(hash_table_t *ht)
    352 {
    353     if (ht->table) {
    354         free(ht->table);
    355     }
    356     if (ht->offsets) {
    357         free(ht->offsets);
    358     }
    359     memset(ht, 0, sizeof(*ht));
    360 }