1 module cachetools.containers.hashmap; 2 3 import std.traits; 4 import std.experimental.logger; 5 import std.format; 6 import std.typecons; 7 8 import core.memory; 9 import core.bitop; 10 11 private import std.experimental.allocator; 12 private import std.experimental.allocator.mallocator: Mallocator; 13 14 private import cachetools.internal; 15 private import cachetools.hash; 16 private import cachetools.containers.lists; 17 18 class KeyNotFound: Exception 19 { 20 this(string msg = "key not found") @safe 21 { 22 super(msg); 23 } 24 } 25 /// 26 /// Return true if it is worth to store values inline in hash table 27 /// V footprint should be small enough 28 /// 29 package bool SmallValueFootprint(V)() { 30 import std.traits; 31 static if ( 32 isNumeric!V 33 || isSomeString!V 34 || isSomeChar!V 35 || isPointer!V ) 36 { 37 return true; 38 } 39 else static if ( 40 is(V == struct) && V.sizeof <= (void*).sizeof ) 41 { 42 return true; 43 } 44 else static if ( 45 is(V == class ) && __traits(classInstanceSize, V) <= (void*).sizeof) 46 { 47 return true; 48 } 49 else 50 return false; 51 } 52 53 private bool keyEquals(K)(const K a, const K b) 54 { 55 static if ( is(K==class) ) 56 { 57 if (a is b) 58 { 59 return true; 60 } 61 if (a is null || b is null) 62 { 63 return false; 64 } 65 return a.opEquals(b); 66 } 67 else 68 { 69 return a == b; 70 } 71 } 72 /* 73 dub run -b release --compiler ldc2 74 Running ./cachetools 75 AA!(int,int) [137 ms, 348 μs, and 6 hnsecs] 76 OA!(int,int) [77 ms, 913 μs, and 1 hnsec] 77 OA!(int,int) GC [80 ms, 571 μs, and 3 hnsecs] 78 --- 79 AA large [122 ms and 157 μs] 80 OA large [177 ms, 167 μs, and 4 hnsecs] 81 OA large GC [125 ms, 771 μs, and 8 hnsecs] 82 --- 83 AA largeClass [183 ms, 366 μs, and 3 hnsecs] 84 OA largeClass [124 ms, 617 μs, and 3 hnsecs] 85 OA largeClassGC [106 ms, 407 μs, and 7 hnsecs] 86 --- 87 */ 88 89 struct HashMap(K, V, Allocator = Mallocator) { 90 91 enum initial_buckets_num = 32; 92 enum grow_factor = 2; 93 enum inlineValues = true;//SmallValueFootprint!V()|| is(V==class); 94 95 //static if ( is(K==class) && (is(K==immutable) || is(K==const)) ) 96 //{ 97 // alias StoredKeyType = Rebindable!K; 98 //} 99 //else static if (is(K==struct) && (is(K==immutable) || is(K==const)) ) 100 //{ 101 // alias StoredKeyType = Unqual!K; 102 //} 103 //else 104 //{ 105 // alias StoredKeyType = K; 106 //} 107 108 alias StoredKeyType = StoredType!K; 109 alias StoredValueType = StoredType!V; 110 111 private { 112 alias allocator = Allocator.instance; 113 static if (hash_t.sizeof == 8) 114 { 115 enum EMPTY_HASH = 0x00_00_00_00_00_00_00_00; 116 enum DELETED_HASH = 0x10_00_00_00_00_00_00_00; 117 enum ALLOCATED_HASH = 0x20_00_00_00_00_00_00_00; 118 enum TYPE_MASK = 0xF0_00_00_00_00_00_00_00; 119 enum HASH_MASK = 0x0F_FF_FF_FF_FF_FF_FF_FF; 120 } 121 else static if (hash_t.sizeof == 4) 122 { 123 enum EMPTY_HASH = 0x00_00_00_00; 124 enum DELETED_HASH = 0x10_00_00_00; 125 enum ALLOCATED_HASH = 0x20_00_00_00; 126 enum TYPE_MASK = 0xF0_00_00_00; 127 enum HASH_MASK = 0x0F_FF_FF_FF; 128 } 129 130 struct _Bucket { 131 hash_t hash; 132 StoredKeyType key; 133 static if (inlineValues) 134 { 135 StoredValueType value; 136 } 137 else 138 { 139 V* value_ptr; 140 } 141 string toString() const { 142 import std.format; 143 static if (inlineValues) { 144 return "%s, hash: %0x,key: %s, value: %s".format( 145 [EMPTY_HASH:"free", DELETED_HASH:"deleted", ALLOCATED_HASH:"allocated"][cast(long )(hash & TYPE_MASK)], 146 hash, key, value); 147 } else { 148 return "%s, hash: %0x, key: %s, value: %s".format( 149 [EMPTY_HASH:"free", DELETED_HASH:"deleted", ALLOCATED_HASH:"allocated"][cast(long)(hash & TYPE_MASK)], 150 hash, key, 151 value_ptr !is null? format("%s", *value_ptr) : "-"); 152 } 153 } 154 } 155 156 _Bucket[] _buckets; 157 int _buckets_num; 158 int _mask; 159 int _allocated; 160 int _deleted; 161 int _empty; 162 } 163 164 ~this() @safe { 165 if ( _buckets_num > 0 ) 166 { 167 static if ( !inlineValues ) 168 { 169 for(int i=0;i<_buckets_num;i++) 170 { 171 auto t = _buckets[i].hash; 172 if ( t <= DELETED_HASH ) 173 { 174 continue; 175 } 176 (() @trusted {dispose(allocator, _buckets[i].value_ptr);})(); 177 } 178 } 179 (() @trusted { 180 GC.removeRange(_buckets.ptr); 181 dispose(allocator, _buckets); 182 })(); 183 } 184 } 185 invariant { 186 assert(_allocated>=0 && _deleted>=0 && _empty >= 0); 187 assert(_allocated + _deleted + _empty == _buckets_num, "a:%s + d:%s + e:%s != total: %s".format(_allocated, _deleted, _empty, _buckets_num)); 188 } 189 /// 190 /// Find any unallocated bucket starting from start_index (inclusive) 191 /// Returns index on success or hash_t.max on fail 192 /// 193 private hash_t findEmptyIndex(const hash_t start_index) pure const @safe @nogc 194 in 195 { 196 assert(start_index < _buckets_num); 197 } 198 do { 199 hash_t index = start_index; 200 201 do { 202 () @nogc {debug(cachetools) tracef("test index %d for non-ALLOCATED", index);}(); 203 if ( _buckets[index].hash < ALLOCATED_HASH ) 204 { 205 return index; 206 } 207 index = (index + 1) & _mask; 208 } while(index != start_index); 209 return hash_t.max; 210 } 211 /// 212 /// Find allocated bucket for given key and computed hash starting from start_index 213 /// Returns: index if bucket found or hash_t.max otherwise 214 /// 215 /// Inherits @nogc and from K opEquals() 216 /// 217 private hash_t findEntryIndex(const hash_t start_index, const hash_t hash, in K key) pure const @safe 218 in 219 { 220 assert(hash < DELETED_HASH); // we look for real hash 221 assert(start_index < _buckets_num); // start position inside array 222 } 223 do { 224 hash_t index = start_index; 225 226 do { 227 immutable h = _buckets[index].hash; 228 229 () @nogc @trusted {debug(cachetools) tracef("test entry index %d (%s) for key %s", index, _buckets[index].toString, key);}(); 230 231 if ( h == EMPTY_HASH ) { 232 break; 233 } 234 235 if ( h >= ALLOCATED_HASH && (h & HASH_MASK) == hash && keyEquals(_buckets[index].key, key) ) { 236 //() @nogc @trusted {debug(cachetools) tracef("test entry index %d for key %s - success", index, key);}(); 237 return index; 238 } 239 index = (index + 1) & _mask; 240 } while(index != start_index); 241 return hash_t.max; 242 } 243 244 /// 245 /// Find place where we can insert(DELETED or EMPTY bucket) or update existent (ALLOCATED) 246 /// bucket for key k and precomputed hash starting from start_index 247 /// 248 /// 249 /// Inherits @nogc from K opEquals() 250 /// 251 private hash_t findUpdateIndex(const hash_t start_index, const hash_t computed_hash, in K key) pure const @safe 252 in 253 { 254 assert(computed_hash < DELETED_HASH); 255 assert(start_index < _buckets_num); 256 } 257 do { 258 hash_t index = start_index; 259 260 do { 261 immutable h = _buckets[index].hash; 262 263 () @nogc @trusted {debug(cachetools) tracef("test update index %d (%s) for key %s", index, _buckets[index], key);}(); 264 265 if ( h <= DELETED_HASH ) // empty or deleted 266 { 267 () @nogc @trusted {debug(cachetools) tracef("test update index %d (%s) for key %s - success", index, _buckets[index], key);}(); 268 return index; 269 } 270 assert((h & TYPE_MASK) == ALLOCATED_HASH); 271 if ( (h & HASH_MASK) == computed_hash && keyEquals(_buckets[index].key, key) ) 272 { 273 () @nogc @trusted {debug(cachetools) tracef("test update index %d (%s) for key %s - success", index, _buckets[index], key);}(); 274 return index; 275 } 276 index = (index + 1) & _mask; 277 } while(index != start_index); 278 return hash_t.max; 279 } 280 /// 281 /// Find unallocated entry in the buckets slice 282 /// We use this function during resize() only. 283 /// 284 private long findEmptyIndexExtended(const hash_t start_index, in ref _Bucket[] buckets, int new_mask) pure const @safe @nogc 285 in 286 { 287 assert(start_index < buckets.length); 288 } 289 do 290 { 291 hash_t index = start_index; 292 293 do { 294 immutable t = buckets[index].hash; 295 296 () @nogc @trusted {debug(cachetools) tracef("test empty index %d (%s)", 297 index, buckets[index]);}(); 298 299 if ( t <= DELETED_HASH ) // empty or deleted 300 { 301 return index; 302 } 303 304 index = (index + 1) & new_mask; 305 } while(index != start_index); 306 return hash_t.max; 307 } 308 309 private bool tooMuchDeleted() pure const @safe @nogc { 310 // 311 // _deleted > _buckets_num / 8 312 // 313 return _deleted << 3 > _buckets_num; 314 } 315 316 private bool tooHighLoad() pure const @safe @nogc { 317 // 318 // _allocated/_buckets_num > 0.8 319 // 5 * allocated > 4 * buckets_num 320 // 321 return _allocated + (_allocated << 2) > _buckets_num << 2; 322 } 323 324 private void doResize(int dest) @safe { 325 immutable _new_buckets_num = dest; 326 immutable _new_mask = dest - 1; 327 _Bucket[] _new_buckets = makeArray!(_Bucket)(allocator, _new_buckets_num); 328 329 () @trusted {GC.addRange(_new_buckets.ptr, _new_buckets_num * _Bucket.sizeof);}(); 330 331 // iterate over entries 332 333 debug(cachetools) 334 { 335 () @nogc {tracef("start resizing: old loadfactor: %s", (1.0*_allocated) / _buckets_num);}(); 336 } 337 338 for(int i=0;i<_buckets_num;i++) { 339 immutable hash_t h = _buckets[i].hash; 340 if ( h < ALLOCATED_HASH ) { // empty or deleted 341 continue; 342 } 343 344 immutable hash_t start_index = h & _new_mask; 345 immutable new_position = findEmptyIndexExtended(start_index, _new_buckets, _new_mask); 346 347 debug(cachetools) () @nogc {tracef("old hash: %0x, old pos: %d, new_pos: %d", h, i, new_position);}(); 348 349 assert( new_position >= 0 ); 350 assert( _new_buckets[cast(hash_t)new_position].hash == EMPTY_HASH ); 351 352 _new_buckets[cast(hash_t)new_position] = _buckets[i]; 353 } 354 (() @trusted { 355 GC.removeRange(_buckets.ptr); 356 dispose(allocator, _buckets); 357 })(); 358 _buckets = _new_buckets; 359 _buckets_num = _new_buckets_num; 360 assert(_popcnt(_buckets_num) == 1, "Buckets number must be power of 2"); 361 _mask = _buckets_num - 1; 362 _deleted = 0; 363 _empty = _buckets_num - _allocated; 364 365 debug(cachetools) 366 { 367 () @nogc {tracef("resizing done: new loadfactor: %s", (1.0*_allocated) / _buckets_num);}(); 368 } 369 } 370 371 /// 372 /// Lookup methods 373 /// 374 V* opBinaryRight(string op)(in K k) @safe if (op == "in") 375 { 376 377 if ( _buckets_num == 0 ) return null; 378 379 immutable computed_hash = hash_function(k) & HASH_MASK; 380 immutable start_index = computed_hash & _mask; 381 immutable lookup_index = findEntryIndex(start_index, computed_hash, k); 382 if ( lookup_index == hash_t.max) { 383 return null; 384 } 385 static if ( inlineValues ) 386 { 387 static if ( is(V==StoredValueType) ) 388 { 389 return &_buckets[lookup_index].value; 390 } 391 else 392 { 393 V* r = () @trusted {return cast(V*)&_buckets[lookup_index].value;}(); 394 return r; 395 } 396 } 397 else 398 { 399 return _buckets[lookup_index].value_ptr; 400 } 401 } 402 403 ref V getOrAdd(T)(K k, T defaultValue) @safe 404 { 405 V* v = k in this; 406 if ( v ) 407 { 408 return *v; 409 } 410 static if (isAssignable!(V, T)) 411 { 412 return *put(k, defaultValue); 413 } 414 else static if ( isCallable!T && isAssignable!(V, ReturnType!T)) 415 { 416 return *put(k, defaultValue()); 417 } 418 else 419 { 420 static assert(0, "what?"); 421 } 422 } 423 424 alias require = getOrAdd; 425 426 /// Attention: this can't return ref as default value can be rvalue 427 V get(T)(K k, T defaultValue) @safe 428 { 429 V* v = k in this; 430 if ( v ) 431 { 432 return *v; 433 } 434 static if (isAssignable!(V, T)) 435 { 436 return defaultValue; 437 } 438 else static if ( isCallable!T && isAssignable!(V, ReturnType!T)) 439 { 440 return defaultValue(); 441 } 442 else 443 { 444 static assert(0, "You must call 'get' with default value of HashMap 'value' type, or with callable, returning HashMap 'value'"); 445 } 446 } 447 448 /// 449 /// Attention: you can't use this method in @nogc code. 450 /// Usual aa[key] method. 451 /// Throws exception if key not found 452 /// 453 ref V opIndex(in K k) @safe 454 { 455 V* v = k in this; 456 if ( v !is null ) 457 { 458 return *v; 459 } 460 throw new KeyNotFound(); 461 } 462 463 /// 464 /// Modifiers 465 /// 466 void opIndexAssign(V v, K k) @safe 467 { 468 put(k, v); 469 } 470 /// 471 /// put pair (k,v) into hash. 472 /// it must be @safe, it inherits @nogc properties from K and V 473 /// It can resize hashtable it is overloaded or has too much deleted entries 474 /// 475 V* put(K k, V v) @safe 476 out 477 { 478 assert(__result !is null); 479 } 480 do { 481 if ( !_buckets_num ) { 482 _buckets_num = _empty = initial_buckets_num; 483 assert(_popcnt(_buckets_num) == 1, "Buckets number must be power of 2"); 484 _mask = _buckets_num - 1; 485 _buckets = makeArray!(_Bucket)(allocator, _buckets_num); 486 () @trusted {GC.addRange(_buckets.ptr, _buckets_num * _Bucket.sizeof);}(); 487 } 488 489 () @nogc @trusted {debug(cachetools) tracef("put k: %s, v: %s", k,v);}(); 490 491 if ( tooHighLoad ) { 492 doResize(grow_factor * _buckets_num); 493 } 494 495 V* r; //result 496 immutable computed_hash = hash_function(k) & HASH_MASK; 497 immutable start_index = computed_hash & _mask; 498 immutable placement_index = findUpdateIndex(start_index, computed_hash, k); 499 assert(placement_index >= 0); 500 _Bucket* bucket = &_buckets[placement_index]; 501 immutable h = bucket.hash; 502 503 debug(cachetools) () @nogc @trusted {tracef("start_index: %d, placement_index: %d", start_index, placement_index);}(); 504 505 if ( h < ALLOCATED_HASH ) 506 { 507 _allocated++; 508 _empty--; 509 } 510 static if ( inlineValues ) 511 { 512 debug(cachetools) () @nogc @trusted {tracef("place inline buckets[%d] '%s'='%s'", placement_index, k, v);}(); 513 bucket.value = v; 514 static if ( is(V==StoredValueType) ) 515 { 516 r = &bucket.value; 517 } 518 else 519 { 520 () @trusted {r = cast(V*)&bucket.value;}(); 521 } 522 } 523 else 524 { 525 debug(cachetools) () @nogc @trusted {tracef("place with allocation buckets[%d] '%s'='%s'", placement_index, k, v);}(); 526 if ( (bucket.hash & TYPE_MASK) == ALLOCATED_HASH ) 527 { 528 // we just replace what we already allocated 529 r = (bucket.value_ptr); 530 *r = v; 531 } 532 else 533 { 534 r = bucket.value_ptr = make!(V)(allocator); 535 *r = v; 536 } 537 } 538 bucket.hash = computed_hash | ALLOCATED_HASH; 539 bucket.key = k; 540 return r; 541 } 542 543 bool remove(K k) @safe { 544 545 if ( tooMuchDeleted ) { 546 // do not shrink, just compact table 547 doResize(_buckets_num); 548 } 549 550 551 debug(cachetools) () @nogc @trusted {tracef("remove k: %s", k);}(); 552 553 immutable computed_hash = hash_function(k) & HASH_MASK; 554 immutable start_index = computed_hash & _mask; 555 immutable lookup_index = findEntryIndex(start_index, computed_hash, k); 556 if ( lookup_index == hash_t.max ) 557 { 558 // nothing to remove 559 return false; 560 } 561 562 assert((_buckets[lookup_index].hash & TYPE_MASK) == ALLOCATED_HASH, "tried to remove non allocated bucket"); 563 564 static if ( inlineValues ) 565 { 566 // what we have to do with removed values XXX? 567 } 568 else 569 { 570 // what we have to do with removed values XXX? 571 // free space 572 (() @trusted {dispose(allocator, _buckets[lookup_index].value_ptr);})(); 573 _buckets[lookup_index].value_ptr = null; 574 } 575 576 _allocated--; 577 immutable next_index = (lookup_index + 1) & _mask; 578 // if next bucket is EMPTY, then we can convert all DELETED buckets down staring from current to EMPTY buckets 579 if ( _buckets[next_index].hash == EMPTY_HASH ) 580 { 581 _empty++; 582 _buckets[lookup_index].hash = EMPTY_HASH; 583 auto free_index = (lookup_index - 1) & _mask; 584 while (free_index != lookup_index) { 585 if ( _buckets[free_index].hash != DELETED_HASH ) { 586 break; 587 } 588 _buckets[free_index].hash = EMPTY_HASH; 589 _deleted--; 590 _empty++; 591 free_index = (free_index - 1) & _mask; 592 } 593 assert(free_index != lookup_index, "table full of deleted buckets?"); 594 } 595 else 596 { 597 _buckets[lookup_index].hash = DELETED_HASH; 598 _deleted++; 599 } 600 return true; 601 } 602 void clear() @safe 603 { 604 static if ( !inlineValues ) 605 { 606 // dispose every element 607 foreach(k; byKey) 608 { 609 remove(k); 610 } 611 } 612 (() @trusted { 613 GC.removeRange(_buckets.ptr); 614 dispose(allocator, _buckets); 615 })(); 616 _buckets = makeArray!(_Bucket)(allocator, _buckets_num); 617 _allocated = _deleted = 0; 618 _empty = _buckets_num; 619 } 620 auto length() const pure nothrow @nogc @safe 621 { 622 return _allocated; 623 } 624 625 auto size() const pure nothrow @nogc @safe 626 { 627 return _buckets_num; 628 } 629 630 auto byKey() pure @safe @nogc 631 { 632 struct _kvRange { 633 int _pos; 634 ulong _buckets_num; 635 _Bucket[] _buckets; 636 this(_Bucket[] _b) 637 { 638 _buckets = _b; 639 _buckets_num = _b.length; 640 _pos = 0; 641 while( _pos < _buckets_num && _buckets[_pos].hash < ALLOCATED_HASH ) 642 { 643 _pos++; 644 } 645 } 646 bool empty() const pure nothrow @safe @nogc 647 { 648 return _pos == _buckets_num; 649 } 650 K front() 651 { 652 return _buckets[_pos].key; 653 } 654 void popFront() pure nothrow @safe @nogc 655 { 656 _pos++; 657 while( _pos < _buckets_num && _buckets[_pos].hash < ALLOCATED_HASH ) 658 { 659 _pos++; 660 } 661 } 662 } 663 return _kvRange(_buckets); 664 } 665 666 auto byValue() pure @safe 667 { 668 struct _kvRange { 669 int _pos; 670 ulong _buckets_num; 671 _Bucket[] _buckets; 672 this(_Bucket[] _b) 673 { 674 _buckets = _b; 675 _buckets_num = _b.length; 676 _pos = 0; 677 while( _pos < _buckets_num && _buckets[_pos].hash < ALLOCATED_HASH ) 678 { 679 _pos++; 680 } 681 } 682 bool empty() const pure nothrow @safe @nogc 683 { 684 return _pos == _buckets_num; 685 } 686 V front() 687 { 688 static if (inlineValues) 689 { 690 return _buckets[_pos].value; 691 } 692 else 693 { 694 return *(_buckets[_pos].value_ptr); 695 } 696 } 697 void popFront() pure nothrow @safe @nogc 698 { 699 _pos++; 700 while( _pos < _buckets_num && _buckets[_pos].hash < ALLOCATED_HASH ) 701 { 702 _pos++; 703 } 704 } 705 } 706 return _kvRange(_buckets); 707 } 708 709 auto byPair() pure @safe 710 { 711 import std.typecons; 712 713 struct _kvRange { 714 int _pos; 715 ulong _buckets_num; 716 _Bucket[] _buckets; 717 this(_Bucket[] _b) 718 { 719 _buckets = _b; 720 _buckets_num = _b.length; 721 _pos = 0; 722 while( _pos < _buckets_num && _buckets[_pos].hash < ALLOCATED_HASH ) 723 { 724 _pos++; 725 } 726 } 727 bool empty() const pure nothrow @safe @nogc 728 { 729 return _pos == _buckets_num; 730 } 731 auto front() @safe 732 { 733 static if (inlineValues) 734 { 735 return Tuple!(K, "key", V, "value")(_buckets[_pos].key, _buckets[_pos].value); 736 } 737 else 738 { 739 return Tuple!(K, "key", V, "value")(_buckets[_pos].key, *(_buckets[_pos].value_ptr)); 740 } 741 } 742 void popFront() pure nothrow @safe @nogc 743 { 744 _pos++; 745 while( _pos < _buckets_num && _buckets[_pos].hash < ALLOCATED_HASH ) 746 { 747 _pos++; 748 } 749 } 750 } 751 return _kvRange(_buckets); 752 } 753 } 754 755 /// Tests 756 757 /// test immutable struct and class as Key type 758 @safe unittest 759 { 760 () @nogc 761 { 762 struct S 763 { 764 int s; 765 } 766 HashMap!(immutable S, int) hs1; 767 immutable ss = S(1); 768 hs1[ss] = 1; 769 assert(ss in hs1 && *(ss in hs1) == 1); 770 HashMap!(int, immutable S) hs2; 771 hs2[1] = ss; 772 assert(1 in hs2 && *(1 in hs2) == ss); 773 assert(!(2 in hs2)); 774 }(); 775 776 // class 777 class C 778 { 779 int v; 780 this(int _v) pure inout 781 { 782 v = _v; 783 } 784 bool opEquals(const C o) pure const @safe @nogc nothrow { 785 return v == o.v; 786 } 787 override hash_t toHash() const @safe @nogc { 788 return hash_function(v); 789 } 790 } 791 HashMap!(immutable C, int) hc1; 792 immutable cc = new immutable C(1); 793 hc1[cc] = 1; 794 assert(hc1[cc] == 1); 795 HashMap!(int, immutable C) hc2; 796 hc2[1] = cc; 797 assert(hc2[1] is cc); 798 } 799 @safe unittest { 800 // test class as key 801 globalLogLevel = LogLevel.info; 802 class A { 803 int v; 804 805 bool opEquals(const A o) pure const @safe @nogc nothrow { 806 return v == o.v; 807 } 808 override hash_t toHash() const @safe @nogc { 809 return hash_function(v); 810 } 811 this(int v) 812 { 813 this.v = v; 814 } 815 override string toString() const 816 { 817 import std.format; 818 return "A(%d)".format(v); 819 } 820 } 821 822 globalLogLevel = LogLevel.info; 823 auto x = new A(1); 824 auto y = new A(2); 825 HashMap!(A, string) dict; 826 dict.put(x, "x"); 827 dict.put(y, "y"); 828 } 829 830 @safe unittest { 831 globalLogLevel = LogLevel.info; 832 () @nogc { 833 HashMap!(int, int) int2int; 834 foreach(i; 1..5) { 835 int2int.put(i,i); 836 } 837 int2int.put(33,33); // <- follow key 1, move key 2 on pos 3 838 assert(1 in int2int, "1 not in hash"); 839 assert(2 in int2int, "2 not in hash"); 840 assert(1 in int2int, "3 not in hash"); 841 assert(4 in int2int, "4 not in hash"); 842 assert(33 in int2int, "33 not in hash"); 843 int2int.remove(33); 844 int2int.put(2,2); // <- must replace key 2 on pos 3 845 assert(2 in int2int, "2 not in hash"); 846 }(); 847 () @nogc { 848 struct LargeStruct { 849 ulong a; 850 ulong b; 851 } 852 HashMap!(int, LargeStruct) int2ls; 853 foreach(i; 1..5) { 854 int2ls.put(i,LargeStruct(i,i)); 855 } 856 int2ls.put(33,LargeStruct(33,33)); // <- follow key 1, move key 2 on pos 3 857 assert(1 in int2ls, "1 not in hash"); 858 assert(2 in int2ls, "2 not in hash"); 859 assert(3 in int2ls, "3 not in hash"); 860 assert(4 in int2ls, "4 not in hash"); 861 assert(33 in int2ls, "33 not in hash"); 862 int2ls.remove(33); 863 int2ls.put(2,LargeStruct(2,2)); // <- must replace key 2 on pos 3 864 assert(2 in int2ls, "2 not in hash"); 865 }(); 866 } 867 868 @safe unittest { 869 globalLogLevel = LogLevel.info; 870 () @nogc { 871 assert(SmallValueFootprint!int()); 872 assert(SmallValueFootprint!double()); 873 struct SmallStruct { 874 ulong a; 875 } 876 //assert(SmallValueFootprint!SmallStruct); 877 struct LargeStruct { 878 ulong a; 879 ulong b; 880 } 881 assert(!SmallValueFootprint!LargeStruct); 882 class SmallClass { 883 ulong a; 884 } 885 //assert(!SmallValueFootprint!SmallClass); 886 887 HashMap!(int, string) int2string; 888 auto u = int2string.put(1, "one"); 889 { 890 auto v = 1 in int2string; 891 assert(v !is null); 892 assert(*v == "one"); 893 } 894 assert(2 !in int2string); 895 u = int2string.put(32+1, "33"); 896 assert(33 in int2string); 897 assert(int2string.remove(33)); 898 assert(!int2string.remove(33)); 899 900 HashMap!(int, LargeStruct) int2LagreStruct; 901 int2LagreStruct.put(1, LargeStruct(1,2)); 902 { 903 auto v = 1 in int2LagreStruct; 904 assert(v !is null); 905 assert(*v == LargeStruct(1, 2)); 906 } 907 }(); 908 909 globalLogLevel = LogLevel.info; 910 } 911 912 @safe unittest { 913 import std.experimental.allocator.gc_allocator; 914 globalLogLevel = LogLevel.info; 915 static int i; 916 () @safe @nogc { 917 struct LargeStruct { 918 ulong a; 919 ulong b; 920 ~this() @safe @nogc { 921 i++; 922 } 923 } 924 HashMap!(int, LargeStruct) int2LagreStruct; 925 int2LagreStruct.put(1, LargeStruct(1,2)); 926 }(); 927 globalLogLevel = LogLevel.info; 928 } 929 930 @safe unittest 931 { 932 import std.typecons; 933 alias K = Tuple!(int, int); 934 alias V = int; 935 HashMap!(K,V) h; 936 K k0 = K(0,1); 937 V v0 = 1; 938 h.put(k0, v0); 939 int *v = k0 in h; 940 assert(v); 941 assert(*v == 1); 942 h[k0] = v0; 943 assert(h[k0] == v0); 944 } 945 946 @safe unittest 947 { 948 class c { 949 int a; 950 this(int a) 951 { 952 this.a = a; 953 } 954 override hash_t toHash() const pure @nogc @safe 955 { 956 return hash_function(a); 957 } 958 bool opEquals(const c other) pure const @safe @nogc 959 { 960 return this is other || this.a == other.a; 961 } 962 } 963 alias K = c; 964 alias V = int; 965 K k0 = new c(0); 966 V v0 = 1; 967 () @nogc { 968 HashMap!(K,V) h; 969 h.put(k0, v0); 970 int *v = k0 in h; 971 assert(v); 972 assert(*v == 1); 973 h[k0] = 2; 974 v = k0 in h; 975 assert(*v == 2); 976 }(); 977 } 978 979 /// Test if we can work with non-@nogc opEquals for class-key. 980 /// opEquals anyway must be non-@system. 981 @safe unittest 982 { 983 class c { 984 int a; 985 this(int a) 986 { 987 this.a = a; 988 } 989 override hash_t toHash() const pure @safe 990 { 991 int[] _ = [1, 2, 3]; // this cause GC 992 return hash_function(a); 993 } 994 995 bool opEquals(const c other) const pure @safe 996 { 997 auto _ = [1,2,3]; // this cause GC 998 return this is other || this.a == other.a; 999 } 1000 } 1001 alias K = c; 1002 alias V = int; 1003 HashMap!(K,V) h; 1004 K k0 = new c(0); 1005 V v0 = 1; 1006 h.put(k0, v0); 1007 int *v = k0 in h; 1008 assert(v); 1009 assert(*v == 1); 1010 1011 } 1012 1013 @safe unittest 1014 { 1015 import std.algorithm; 1016 import std.array; 1017 import std.stdio; 1018 1019 HashMap!(int, string) m; 1020 m[1] = "one"; 1021 m[2] = "two"; 1022 m[10] = "ten"; 1023 assert(equal(m.byKey.array.sort, [1,2,10])); 1024 assert(equal(m.byValue.array.sort, ["one", "ten", "two"])); 1025 assert(equal( 1026 m.byPair.map!"tuple(a.key, a.value)".array.sort, 1027 [tuple(1, "one"), tuple(2, "two"), tuple(10, "ten")] 1028 )); 1029 m.remove(1); 1030 m.remove(10); 1031 assert(equal( 1032 m.byPair.map!"tuple(a.key, a.value)".array.sort, 1033 [tuple(2, "two")] 1034 )); 1035 m.remove(2); 1036 assert(m.byPair.map!"tuple(a.key, a.value)".array.sort.length() == 0); 1037 m.remove(2); 1038 assert(m.byPair.map!"tuple(a.key, a.value)".array.sort.length() == 0); 1039 } 1040 1041 unittest { 1042 import std.random; 1043 import std.array; 1044 import std.algorithm; 1045 import std.stdio; 1046 1047 enum iterations = 400_000; 1048 1049 globalLogLevel = LogLevel.info; 1050 1051 HashMap!(int, int) hashMap; 1052 int[int] AA; 1053 1054 auto rnd = Random(unpredictableSeed); 1055 1056 foreach(i;0..iterations) { 1057 int k = uniform(0, iterations, rnd); 1058 hashMap.put(k, i); 1059 AA[k] = i; 1060 } 1061 assert(equal(AA.keys().sort(), hashMap.byKey().array.sort())); 1062 assert(equal(AA.values().sort(), hashMap.byValue().array.sort())); 1063 assert(AA.length == hashMap.length); 1064 } 1065 1066 @safe unittest 1067 { 1068 // test removal while iterating 1069 import std.random; 1070 import std.array; 1071 import std.algorithm; 1072 import std.stdio; 1073 1074 enum iterations = 400_000; 1075 1076 globalLogLevel = LogLevel.info; 1077 1078 HashMap!(int, int) hashMap; 1079 1080 auto rnd = Random(unpredictableSeed); 1081 1082 foreach(i;0..iterations) { 1083 int k = uniform(0, iterations, rnd); 1084 hashMap[k] = i; 1085 } 1086 foreach(k; hashMap.byKey) 1087 { 1088 hashMap.remove(k); 1089 } 1090 assert(hashMap.length == 0); 1091 } 1092 1093 @safe @nogc unittest 1094 { 1095 // test clear 1096 1097 HashMap!(int, int) hashMap; 1098 1099 foreach(i;0..100) { 1100 hashMap[i] = i; 1101 } 1102 hashMap.clear(); 1103 assert(hashMap.length == 0); 1104 hashMap[1] = 1; 1105 assert(1 in hashMap && hashMap.length == 1); 1106 } 1107 1108 @safe @nogc unittest 1109 { 1110 // test of nogc getOrAdd 1111 1112 HashMap!(int, int) hashMap; 1113 1114 foreach(i;0..100) { 1115 hashMap[i] = i; 1116 } 1117 auto v = hashMap.getOrAdd(-1, -1); 1118 assert(-1 in hashMap && v == -1); 1119 } 1120 1121 @safe @nogc unittest 1122 { 1123 // test of nogc getOrAdd with lazy default value 1124 1125 HashMap!(int, int) hashMap; 1126 1127 foreach(i;0..100) { 1128 hashMap[i] = i; 1129 } 1130 int v = hashMap.getOrAdd(-1, () => -1); 1131 assert(-1 in hashMap && v == -1); 1132 assert(hashMap.get(-1, 0) == -1); // key -1 is in hash, return value 1133 assert(hashMap.get(-2, 0) == 0); // key -2 not in map, return default value 1134 assert(hashMap.get(-3, () => 0) == 0); // ditto 1135 } 1136 1137 @safe unittest 1138 { 1139 import std.socket; 1140 HashMap!(string, Socket) socketPool; 1141 Socket s0 = socketPool.getOrAdd("http://example.com", () => new Socket(AddressFamily.INET, SocketType.STREAM)); 1142 assert(s0 !is null); 1143 assert(s0.addressFamily == AddressFamily.INET); 1144 Socket s1 = socketPool.getOrAdd("http://example.com", () => new Socket(AddressFamily.INET, SocketType.STREAM)); 1145 assert(s1 !is null); 1146 assert(s1 is s0); 1147 } 1148 1149 @safe unittest 1150 { 1151 import std.socket; 1152 class Connection { 1153 Socket s; 1154 bool opEquals(const Connection other) const pure @safe 1155 { 1156 return s is other.s; 1157 } 1158 override hash_t toHash() const @safe 1159 { 1160 return hash_function(s.handle); 1161 } 1162 } 1163 HashMap!(Connection, string) socketPool; 1164 } 1165 1166 @safe unittest 1167 { 1168 // test of non-@nogc getOrAdd with lazy default value 1169 import std.conv; 1170 import std.exception; 1171 class C { 1172 string v; 1173 this(int _v) @safe 1174 { 1175 v = to!string(_v); 1176 } 1177 } 1178 1179 HashMap!(int, C) hashMap; 1180 1181 foreach(i;0..100) { 1182 hashMap[i] = new C(i); 1183 } 1184 C v = hashMap.getOrAdd(-1, () => new C(-1)); 1185 assert(-1 in hashMap && v.v == "-1"); 1186 assert(hashMap[-1].v == "-1"); 1187 hashMap[-1].v ~= "1"; 1188 assert(hashMap[-1].v == "-11"); 1189 assertThrown!KeyNotFound(hashMap[-2]); 1190 // check lazyness 1191 bool called; 1192 v = hashMap.getOrAdd(-1, delegate C() {called = true; return new C(0);}); 1193 assert(!called); 1194 v = hashMap.getOrAdd(-2, delegate C() {called = true; return new C(0);}); 1195 assert(called); 1196 } 1197 1198 @safe @nogc unittest 1199 { 1200 // test of nogc getOrAdd with lazy default value 1201 // corner case when V is callable 1202 1203 alias F = int function() @safe @nogc; 1204 1205 F one = function() 1206 { 1207 return 1; 1208 }; 1209 F two = function() 1210 { 1211 return 2; 1212 }; 1213 F three = function() 1214 { 1215 return 3; 1216 }; 1217 F four = function() 1218 { 1219 return 4; 1220 }; 1221 HashMap!(int, F) hashMap; 1222 hashMap.put(1, one); 1223 hashMap.put(2, two); 1224 auto p = 1 in hashMap; 1225 assert(p); 1226 assert((*p)() == 1); 1227 p = 2 in hashMap; 1228 assert(p); 1229 assert((*p)() == 2); 1230 auto f3 = hashMap.getOrAdd(3, () => function int() {return 3;}); // used as default() 1231 assert(f3() == 3); 1232 auto f4 = hashMap.getOrAdd(4, four); 1233 assert(f4() == 4); 1234 } 1235 1236 // test get() 1237 @safe @nogc unittest 1238 { 1239 HashMap!(int, int) hashMap; 1240 int i = hashMap.get(1, 55); 1241 assert(i == 55); 1242 i = hashMap.get(1, () => 66); 1243 assert(i == 66); 1244 hashMap[1] = 1; 1245 i = hashMap.get(1, () => 66); 1246 assert(i == 1); 1247 }