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 }