1 /** 2 Internal hash map implementation. 3 4 Copyright: © 2013 Sönke Ludwig 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig 7 */ 8 module vibe.utils.hashmap; 9 10 import vibe.internal.utilallocator; 11 12 import std.conv : emplace; 13 import std.traits; 14 15 16 struct DefaultHashMapTraits(Key) { 17 enum clearValue = Key.init; 18 static bool equals(in Key a, in Key b) 19 { 20 static if (__traits(isFinalClass, Key) && &Unqual!Key.init.opEquals is &Object.init.opEquals) 21 return a is b; 22 else static if (is(Key == class)) 23 // BUG: improperly casting away const 24 return () @trusted { return a is b ? true : (a !is null && (cast(Object) a).opEquals(cast(Object) b)); }(); 25 else return a == b; 26 } 27 28 static size_t hashOf(const scope ref Key k) 29 @safe { 30 static if (__traits(isFinalClass, Key) && &Unqual!Key.init.toHash is &Object.init.toHash) 31 return () @trusted { return cast(size_t)cast(void*)k; } (); 32 else static if (__traits(compiles, Key.init.toHash())) 33 return () @trusted { return (cast(Key)k).toHash(); } (); 34 else static if (__traits(compiles, Key.init.toHashShared())) 35 return k.toHashShared(); 36 else static if ((__traits(isScalar, Key) || 37 (isArray!Key && is(Key : E[], E) && __traits(isScalar, E))) && 38 is(typeof((in Key x) @nogc nothrow pure @safe => .object.hashOf(x)))) 39 return .object.hashOf(k); 40 else { 41 // evil casts to be able to get the most basic operations of 42 // HashMap nothrow and @nogc 43 static size_t hashWrapper(const scope ref Key k) { 44 static typeinfo = typeid(Key); 45 return typeinfo.getHash(&k); 46 } 47 static @nogc nothrow size_t properlyTypedWrapper(const scope ref Key k) { return 0; } 48 return () @trusted { return (cast(typeof(&properlyTypedWrapper))&hashWrapper)(k); } (); 49 } 50 } 51 } 52 53 unittest 54 { 55 final class Integer : Object { 56 public const int value; 57 58 this(int x) @nogc nothrow pure @safe { value = x; } 59 60 override bool opEquals(Object rhs) const @nogc nothrow pure @safe { 61 if (auto r = cast(Integer) rhs) 62 return value == r.value; 63 return false; 64 } 65 66 override size_t toHash() const @nogc nothrow pure @safe { 67 return value; 68 } 69 } 70 71 auto hashMap = HashMap!(Object, int)(vibeThreadAllocator()); 72 foreach (x; [2, 4, 8, 16]) 73 hashMap[new Integer(x)] = x; 74 foreach (x; [2, 4, 8, 16]) 75 assert(hashMap[new Integer(x)] == x); 76 } 77 78 struct HashMap(TKey, TValue, Traits = DefaultHashMapTraits!TKey, Allocator = IAllocator) 79 { 80 import core.memory : GC; 81 import vibe.internal.meta.traits : isOpApplyDg; 82 import std.algorithm.iteration : filter, map; 83 84 alias Key = TKey; 85 alias Value = TValue; 86 87 Allocator AW(Allocator a) { return a; } 88 alias AllocatorType = AffixAllocator!(Allocator, int); 89 static if (is(typeof(AllocatorType.instance))) 90 alias AllocatorInstanceType = typeof(AllocatorType.instance); 91 else alias AllocatorInstanceType = AllocatorType; 92 93 struct TableEntry { 94 UnConst!Key key = Traits.clearValue; 95 Value value; 96 97 this(ref Key key, ref Value value) 98 { 99 import std.algorithm.mutation : move; 100 this.key = cast(UnConst!Key)key; 101 static if (is(typeof(value.move))) 102 this.value = value.move; 103 else this.value = value; 104 } 105 } 106 private { 107 TableEntry[] m_table; // NOTE: capacity is always POT 108 size_t m_length; 109 static if (!is(typeof(Allocator.instance))) 110 AllocatorInstanceType m_allocator; 111 bool m_resizing; 112 } 113 114 static if (!is(typeof(Allocator.instance))) { 115 this(Allocator allocator) 116 { 117 m_allocator = typeof(m_allocator)(AW(allocator)); 118 } 119 } 120 121 ~this() 122 { 123 int rc; 124 try rc = m_table is null ? 1 : () @trusted { return --allocator.prefix(m_table); } (); 125 catch (Exception e) assert(false, e.msg); 126 127 if (rc == 0) { 128 clear(); 129 if (m_table.ptr !is null) () @trusted { 130 static if (hasIndirections!TableEntry) GC.removeRange(m_table.ptr); 131 try allocator.dispose(m_table); 132 catch (Exception e) assert(false, e.msg); 133 } (); 134 } 135 } 136 137 this(this) 138 @trusted { 139 if (m_table.ptr) { 140 try allocator.prefix(m_table)++; 141 catch (Exception e) assert(false, e.msg); 142 } 143 } 144 145 @property size_t length() const { return m_length; } 146 147 void remove(Key key) 148 { 149 import std.algorithm.mutation : move; 150 151 auto idx = findIndex(key); 152 assert (idx != size_t.max, "Removing non-existent element."); 153 auto i = idx; 154 while (true) { 155 m_table[i].key = Traits.clearValue; 156 m_table[i].value = Value.init; 157 158 size_t j = i, r; 159 do { 160 if (++i >= m_table.length) i -= m_table.length; 161 if (Traits.equals(m_table[i].key, Traits.clearValue)) { 162 m_length--; 163 return; 164 } 165 r = Traits.hashOf(m_table[i].key) & (m_table.length-1); 166 } while ((j<r && r<=i) || (i<j && j<r) || (r<=i && i<j)); 167 static if (is(typeof(m_table[i].move))) 168 m_table[j] = m_table[i].move; 169 else m_table[j] = m_table[i]; 170 } 171 } 172 173 Value get(Key key, lazy Value default_value = Value.init) 174 { 175 auto idx = findIndex(key); 176 if (idx == size_t.max) return default_value; 177 return m_table[idx].value; 178 } 179 180 /// Workaround #12647 181 package(vibe) Value getNothrow(Key key, Value default_value = Value.init) 182 { 183 auto idx = findIndex(key); 184 if (idx == size_t.max) return default_value; 185 return m_table[idx].value; 186 } 187 188 static if (!is(typeof({ Value v; const(Value) vc; v = vc; }))) { 189 const(Value) get(Key key, lazy const(Value) default_value = Value.init) 190 { 191 auto idx = findIndex(key); 192 if (idx == size_t.max) return default_value; 193 return m_table[idx].value; 194 } 195 } 196 197 void clear() 198 { 199 foreach (i; 0 .. m_table.length) 200 if (!Traits.equals(m_table[i].key, Traits.clearValue)) { 201 m_table[i].key = Traits.clearValue; 202 m_table[i].value = Value.init; 203 } 204 m_length = 0; 205 } 206 207 void opIndexAssign(T)(T value, Key key) 208 { 209 import std.algorithm.mutation : move; 210 211 assert(!Traits.equals(key, Traits.clearValue), "Inserting clear value into hash map."); 212 grow(1); 213 auto i = findInsertIndex(key); 214 if (!Traits.equals(m_table[i].key, key)) m_length++; 215 m_table[i].key = () @trusted { return cast(UnConst!Key)key; } (); 216 m_table[i].value = value; 217 } 218 219 ref inout(Value) opIndex(Key key) 220 inout { 221 auto idx = findIndex(key); 222 assert (idx != size_t.max, "Accessing non-existent key."); 223 return m_table[idx].value; 224 } 225 226 inout(Value)* opBinaryRight(string op)(Key key) 227 inout if (op == "in") { 228 auto idx = findIndex(key); 229 if (idx == size_t.max) return null; 230 return &m_table[idx].value; 231 } 232 233 int opApply(DG)(scope DG del) if (isOpApplyDg!(DG, Key, Value)) 234 { 235 import std.traits : arity; 236 foreach (i; 0 .. m_table.length) 237 if (!Traits.equals(m_table[i].key, Traits.clearValue)) { 238 static assert(arity!del >= 1 && arity!del <= 2, 239 "isOpApplyDg should have prevented this"); 240 static if (arity!del == 1) { 241 if (int ret = del(m_table[i].value)) 242 return ret; 243 } else 244 if (int ret = del(m_table[i].key, m_table[i].value)) 245 return ret; 246 } 247 return 0; 248 } 249 250 auto byKey() { return bySlot.map!(e => e.key); } 251 auto byKey() const { return bySlot.map!(e => e.key); } 252 auto byValue() { return bySlot.map!(e => e.value); } 253 auto byValue() const { return bySlot.map!(e => e.value); } 254 auto byKeyValue() { import std.typecons : Tuple; return bySlot.map!(e => Tuple!(Key, "key", Value, "value")(e.key, e.value)); } 255 auto byKeyValue() const { import std.typecons : Tuple; return bySlot.map!(e => Tuple!(const(Key), "key", const(Value), "value")(e.key, e.value)); } 256 257 private auto bySlot() { return m_table[].filter!(e => !Traits.equals(e.key, Traits.clearValue)); } 258 private auto bySlot() const { return m_table[].filter!(e => !Traits.equals(e.key, Traits.clearValue)); } 259 260 private @property AllocatorInstanceType allocator() 261 { 262 static if (is(typeof(Allocator.instance))) 263 return AllocatorType.instance; 264 else { 265 if (!m_allocator._parent) { 266 static if (is(Allocator == IAllocator)) { 267 try m_allocator = typeof(m_allocator)(AW(vibeThreadAllocator())); 268 catch (Exception e) assert(false, e.msg); 269 } else assert(false, "Allocator not initialized."); 270 } 271 return m_allocator; 272 } 273 } 274 275 private size_t findIndex(Key key) 276 const { 277 if (m_length == 0) return size_t.max; 278 size_t start = Traits.hashOf(key) & (m_table.length-1); 279 auto i = start; 280 while (!Traits.equals(m_table[i].key, key)) { 281 if (Traits.equals(m_table[i].key, Traits.clearValue)) return size_t.max; 282 if (++i >= m_table.length) i -= m_table.length; 283 if (i == start) return size_t.max; 284 } 285 return i; 286 } 287 288 private size_t findInsertIndex(Key key) 289 const { 290 auto hash = Traits.hashOf(key); 291 size_t target = hash & (m_table.length-1); 292 auto i = target; 293 while (!Traits.equals(m_table[i].key, Traits.clearValue) && !Traits.equals(m_table[i].key, key)) { 294 if (++i >= m_table.length) i -= m_table.length; 295 assert (i != target, "No free bucket found, HashMap full!?"); 296 } 297 return i; 298 } 299 300 private void grow(size_t amount) 301 @trusted { 302 auto newsize = m_length + amount; 303 if (newsize < (m_table.length*2)/3) { 304 int rc; 305 try rc = allocator.prefix(m_table); 306 catch (Exception e) assert(false, e.msg); 307 if (rc > 1) { 308 // enforce copy-on-write 309 auto oldtable = m_table; 310 try { 311 m_table = allocator.makeArray!TableEntry(m_table.length); 312 m_table[] = oldtable; 313 allocator.prefix(oldtable)--; 314 assert(allocator.prefix(oldtable) > 0); 315 allocator.prefix(m_table) = 1; 316 } catch (Exception e) { 317 assert(false, e.msg); 318 } 319 } 320 return; 321 } 322 auto newcap = m_table.length ? m_table.length : 16; 323 while (newsize >= (newcap*2)/3) newcap *= 2; 324 resize(newcap); 325 } 326 327 private void resize(size_t new_size) 328 @trusted { 329 assert(!m_resizing); 330 m_resizing = true; 331 scope(exit) m_resizing = false; 332 333 uint pot = 0; 334 while (new_size > 1) { 335 pot++; 336 new_size /= 2; 337 } 338 new_size = 1 << pot; 339 340 auto oldtable = m_table; 341 342 // allocate the new array, automatically initializes with empty entries (Traits.clearValue) 343 try { 344 m_table = allocator.makeArray!TableEntry(new_size); 345 allocator.prefix(m_table) = 1; 346 } catch (Exception e) assert(false, e.msg); 347 static if (hasIndirections!TableEntry) GC.addRange(m_table.ptr, m_table.length * TableEntry.sizeof); 348 // perform a move operation of all non-empty elements from the old array to the new one 349 foreach (ref el; oldtable) 350 if (!Traits.equals(el.key, Traits.clearValue)) { 351 auto idx = findInsertIndex(el.key); 352 (cast(ubyte[])(&m_table[idx])[0 .. 1])[] = (cast(ubyte[])(&el)[0 .. 1])[]; 353 } 354 355 // all elements have been moved to the new array, so free the old one without calling destructors 356 int rc; 357 try rc = oldtable is null ? 1 : --allocator.prefix(oldtable); 358 catch (Exception e) assert(false, e.msg); 359 if (rc == 0) { 360 static if (hasIndirections!TableEntry) GC.removeRange(oldtable.ptr); 361 try allocator.deallocate(oldtable); 362 catch (Exception e) assert(false, e.msg); 363 } 364 } 365 } 366 367 nothrow unittest { 368 import std.conv; 369 370 HashMap!(string, string) map; 371 372 foreach (i; 0 .. 100) { 373 map[to!string(i)] = to!string(i) ~ "+"; 374 assert(map.length == i+1); 375 } 376 377 foreach (i; 0 .. 100) { 378 auto str = to!string(i); 379 auto pe = str in map; 380 assert(pe !is null && *pe == str ~ "+"); 381 assert(map[str] == str ~ "+"); 382 } 383 384 foreach (i; 0 .. 50) { 385 map.remove(to!string(i)); 386 assert(map.length == 100-i-1); 387 } 388 389 foreach (i; 50 .. 100) { 390 auto str = to!string(i); 391 auto pe = str in map; 392 assert(pe !is null && *pe == str ~ "+"); 393 assert(map[str] == str ~ "+"); 394 } 395 } 396 397 // test for nothrow/@nogc compliance 398 nothrow unittest { 399 HashMap!(int, int) map1; 400 HashMap!(string, string) map2; 401 map1[1] = 2; 402 map2["1"] = "2"; 403 404 @nogc nothrow void performNoGCOps() 405 { 406 foreach (int v; map1) {} 407 foreach (int k, int v; map1) {} 408 assert(1 in map1); 409 assert(map1.length == 1); 410 assert(map1[1] == 2); 411 assert(map1.getNothrow(1, -1) == 2); 412 413 foreach (string v; map2) {} 414 foreach (string k, string v; map2) {} 415 assert("1" in map2); 416 assert(map2.length == 1); 417 assert(map2["1"] == "2"); 418 assert(map2.getNothrow("1", "") == "2"); 419 } 420 421 performNoGCOps(); 422 } 423 424 unittest { // test for proper use of constructor/post-blit/destructor 425 static struct Test { 426 static size_t constructedCounter = 0; 427 bool constructed = false; 428 this(int) { constructed = true; constructedCounter++; } 429 this(this) nothrow { if (constructed) constructedCounter++; } 430 ~this() nothrow { if (constructed) constructedCounter--; } 431 } 432 433 assert(Test.constructedCounter == 0); 434 435 { // sanity check 436 Test t; 437 assert(Test.constructedCounter == 0); 438 t = Test(1); 439 assert(Test.constructedCounter == 1); 440 auto u = t; 441 assert(Test.constructedCounter == 2); 442 t = Test.init; 443 assert(Test.constructedCounter == 1); 444 } 445 assert(Test.constructedCounter == 0); 446 447 { // basic insertion and hash map resizing 448 HashMap!(int, Test) map; 449 foreach (i; 1 .. 67) { 450 map[i] = Test(1); 451 assert(Test.constructedCounter == i); 452 } 453 } 454 455 assert(Test.constructedCounter == 0); 456 457 { // test clear() and overwriting existing entries 458 HashMap!(int, Test) map; 459 foreach (i; 1 .. 67) { 460 map[i] = Test(1); 461 assert(Test.constructedCounter == i); 462 } 463 map.clear(); 464 foreach (i; 1 .. 67) { 465 map[i] = Test(1); 466 assert(Test.constructedCounter == i); 467 } 468 foreach (i; 1 .. 67) { 469 map[i] = Test(1); 470 assert(Test.constructedCounter == 66); 471 } 472 } 473 474 assert(Test.constructedCounter == 0); 475 476 { // test removing entries and adding entries after remove 477 HashMap!(int, Test) map; 478 foreach (i; 1 .. 67) { 479 map[i] = Test(1); 480 assert(Test.constructedCounter == i); 481 } 482 foreach (i; 1 .. 33) { 483 map.remove(i); 484 assert(Test.constructedCounter == 66 - i); 485 } 486 foreach (i; 67 .. 130) { 487 map[i] = Test(1); 488 assert(Test.constructedCounter == i - 32); 489 } 490 } 491 492 assert(Test.constructedCounter == 0); 493 } 494 495 private template UnConst(T) { 496 static if (is(T U == const(U))) { 497 alias UnConst = U; 498 } else static if (is(T V == immutable(V))) { 499 alias UnConst = V; 500 } else alias UnConst = T; 501 }