1 (* Copyright (C) Doom 2D: Forever Developers
3 * This program is free software: you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation, version 3 of the License ONLY.
7 * This program is distributed in the hope that it will be useful,
8 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 * GNU General Public License for more details.
12 * You should have received a copy of the GNU General Public License
13 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 {$INCLUDE a_modes.inc}
16 {.$DEFINE RBHASH_DEBUG_RESIZE}
17 {.$DEFINE RBHASH_DEBUG_INSERT}
18 {.$DEFINE RBHASH_DEBUG_DELETE}
19 {.$DEFINE RBHASH_DEBUG_COMPACT}
20 {$IF DEFINED(D2F_DEBUG)}
21 {.$DEFINE RBHASH_SANITY_CHECKS}
23 // hash table (robin hood)
29 * HashObjT: class that contains class methods:
30 * class function hash (const[ref] k: KeyT): LongWord;
31 * class function equ (const[ref] a, b: KeyT): Boolean;
32 * class procedure freekey (var k: KeyT); // this may free key
35 // WARNING! don't put structures into hash, use ponters or ids!
36 generic THashBase
<KeyT
, ValueT
, HashObjT
> = class(TObject
)
38 const InitSize
= {$IF DEFINED(RBHASH_SANITY_CHECKS)}16{$ELSE}256{$ENDIF}; // *MUST* be power of two
39 const LoadFactorPrc
= 90; // it is ok for robin hood hashes
49 hash
: LongWord; // key hash or 0
50 nextFree
: PEntry
; // next free entry
52 function getEmpty (): Boolean; inline;
54 property empty
: Boolean read getEmpty
;
55 property keyhash
: LongWord read hash
; // cannot be 0
58 type TFreeValueFn
= procedure (var v
: ValueT
); // this may free value
59 type TIteratorFn
= function (constref k
: KeyT
; constref v
: ValueT
): Boolean is nested
; // return `true` to stop
60 type TIteratorExFn
= function (constref k
: KeyT
; constref v
: ValueT
; keyhash
: LongWord): Boolean is nested
; // return `true` to stop
64 TEntryArray
= array of TEntry
;
68 TValEnumerator
= record
70 mEntries
: TEntryArray
;
71 mFirstEntry
, mLastEntry
, cur
: Integer;
73 constructor Create (const aents
: TEntryArray
; afirst
, alast
: Integer);
74 function MoveNext (): Boolean; inline;
75 function getCurrent (): ValueT
; inline;
76 function GetEnumerator (): TValEnumerator
; inline;
77 property Current
: ValueT read getCurrent
;
80 TKeyEnumerator
= record
82 mEntries
: TEntryArray
;
83 mFirstEntry
, mLastEntry
, cur
: Integer;
85 constructor Create (const aents
: TEntryArray
; afirst
, alast
: Integer);
86 function MoveNext (): Boolean; inline;
87 function getCurrent (): KeyT
; inline;
88 function GetEnumerator (): TKeyEnumerator
; inline;
89 property Current
: KeyT read getCurrent
;
92 TKeyValEnumerator
= record
94 mEntries
: TEntryArray
;
95 mFirstEntry
, mLastEntry
, cur
: Integer;
97 constructor Create (const aents
: TEntryArray
; afirst
, alast
: Integer);
98 function MoveNext (): Boolean; inline;
99 function getCurrent (): PEntry
; inline;
100 function GetEnumerator (): TKeyValEnumerator
; inline;
101 property Current
: PEntry read getCurrent
;
105 freevalfn
: TFreeValueFn
;
106 mBuckets
: array of PEntry
; // entries, points to mEntries elements
107 mBucketsUsed
: Integer;
108 mEntries
: TEntryArray
;
109 {$IFDEF RBHASH_SANITY_CHECKS}
110 mEntriesUsed
: Integer;
112 mFreeEntryHead
: PEntry
;
113 mFirstEntry
, mLastEntry
: Integer;
117 function allocEntry (): PEntry
;
118 procedure releaseEntry (e
: PEntry
);
120 function distToStIdx (idx
: LongWord): LongWord; inline;
122 procedure putEntryInternal (swpe
: PEntry
);
124 function getCapacity (): Integer; inline;
126 procedure freeEntries ();
129 constructor Create (afreevalfn
: TFreeValueFn
=nil);
130 destructor Destroy (); override;
133 procedure reset (); // don't shrink buckets
136 procedure compact (); // call this instead of `rehash()` after alot of deletions
138 // you may pass `keyhash` to bypass hash calculation
139 function get (constref akey
: KeyT
; out rval
: ValueT
; keyhashin
: PLongWord=nil): Boolean; // `true`: found
140 // the function may return calculated value hash in `keyhash`
141 function put (constref akey
: KeyT
; constref aval
: ValueT
; keyhashout
: PLongWord=nil): Boolean; // `true`: replaced
142 // you may pass `keyhash` to bypass hash calculation
143 function has (constref akey
: KeyT
; keyhashin
: PLongWord=nil): Boolean; // `true`: found
144 // you may pass `keyhash` to bypass hash calculation
145 function del (constref akey
: KeyT
; keyhashin
: PLongWord=nil): Boolean; // `true`: deleted
147 //WARNING! don't modify table in iterator (queries are ok, though)
148 function forEach (it
: TIteratorFn
): Boolean; overload
;
149 function forEach (it
: TIteratorExFn
): Boolean; overload
;
151 // default `for ... in` enums values
152 function GetEnumerator (): TValEnumerator
;
153 function byKey (): TKeyEnumerator
;
154 function byValue (): TValEnumerator
;
155 function byKeyValue (): TKeyValEnumerator
; // PEntry
157 property count
: Integer read mBucketsUsed
;
158 property capacity
: Integer read getCapacity
;
162 TJoaatHasher
= record
164 seed
: LongWord; // initial seed value; MUST BE FIRST
165 hash
: LongWord; // current value
168 constructor Create (aseed
: LongWord);
170 procedure reset (); inline; overload
;
171 procedure reset (aseed
: LongWord); inline; overload
;
173 procedure put (constref buf
; len
: LongWord);
175 // current hash value
176 // you can continue putting data, as this is not destructive
177 function value
: LongWord; inline;
184 class function hash (const k
: Integer): LongWord; inline;
185 class function equ (const a
, b
: Integer): Boolean; inline;
186 class procedure freekey (k
: Integer); inline;
191 class function hash (const k
: AnsiString): LongWord; inline;
192 class function equ (const a
, b
: AnsiString): Boolean; inline;
193 class procedure freekey (var k
: AnsiString); inline;
196 // case-insensitive (ansi)
197 THashKeyStrAnsiCI
= class
199 class function hash (const k
: AnsiString): LongWord; inline;
200 class function equ (const a
, b
: AnsiString): Boolean; inline;
201 class procedure freekey (var k
: AnsiString); inline;
205 THashIntInt
= specialize THashBase
<Integer, Integer, THashKeyInt
>;
206 THashStrInt
= specialize THashBase
<AnsiString, Integer, THashKeyStr
>;
207 THashStrCIInt
= specialize THashBase
<AnsiString, Integer, THashKeyStrAnsiCI
>;
208 THashIntStr
= specialize THashBase
<Integer, AnsiString, THashKeyInt
>;
209 THashStrStr
= specialize THashBase
<AnsiString, AnsiString, THashKeyStr
>;
210 THashStrCIStr
= specialize THashBase
<AnsiString, AnsiString, THashKeyStrAnsiCI
>;
211 THashStrVariant
= specialize THashBase
<AnsiString, Variant, THashKeyStr
>;
212 THashStrCIVariant
= specialize THashBase
<AnsiString, Variant, THashKeyStrAnsiCI
>;
215 function u32Hash (a
: LongWord): LongWord; inline;
216 function fnvHash (constref buf
; len
: LongWord): LongWord;
217 function joaatHash (constref buf
; len
: LongWord; seed
: LongWord=0): LongWord;
218 function joaatHashPtr (buf
: Pointer; len
: LongWord; seed
: LongWord=0): LongWord;
220 // has to be public due to FPC generics limitation
221 function nextPOTU32 (x
: LongWord): LongWord; inline;
230 // ////////////////////////////////////////////////////////////////////////// //
233 function nextPOTU32 (x
: LongWord): LongWord; inline;
236 result
:= result
or (result
shr 1);
237 result
:= result
or (result
shr 2);
238 result
:= result
or (result
shr 4);
239 result
:= result
or (result
shr 8);
240 result
:= result
or (result
shr 16);
242 if (x
<> 0) and ((x
and (x
-1)) = 0) then result
:= result
and (not (result
shr 1)) else result
+= 1;
247 // ////////////////////////////////////////////////////////////////////////// //
250 constructor TJoaatHasher
.Create (aseed
: LongWord);
255 procedure TJoaatHasher
.reset (); inline; overload
;
260 procedure TJoaatHasher
.reset (aseed
: LongWord); inline; overload
;
266 procedure TJoaatHasher
.put (constref buf
; len
: LongWord);
271 if (len
< 1) then exit
;
272 bytes
:= PByte(@buf
);
278 h
:= h
xor (h
shr 6);
285 function TJoaatHasher
.value
: LongWord; inline;
288 result
+= (result
shl 3);
289 result
:= result
xor (result
shr 11);
290 result
+= (result
shl 15);
295 // ////////////////////////////////////////////////////////////////////////// //
298 function joaatHash (constref buf
; len
: LongWord; seed
: LongWord=0): LongWord;
308 result
+= (result
shl 10);
309 result
:= result
xor (result
shr 6);
313 result
+= (result
shl 3);
314 result
:= result
xor (result
shr 11);
315 result
+= (result
shl 15);
318 function joaatHashPtr (buf
: Pointer; len
: LongWord; seed
: LongWord=0): LongWord;
328 result
+= (result
shl 10);
329 result
:= result
xor (result
shr 6);
333 result
+= (result
shl 3);
334 result
:= result
xor (result
shr 11);
335 result
+= (result
shl 15);
341 // fnv-1a: http://www.isthe.com/chongo/tech/comp/fnv/
342 function fnvHash (constref buf
; len
: LongWord): LongWord;
347 result
:= 2166136261; // fnv offset basis
350 result
:= result
xor b
^;
351 result
:= result
*16777619; // 32-bit fnv prime
360 function u32Hash (a
: LongWord): LongWord; inline;
363 result
-= (result
shl 6);
364 result
:= result
xor (result
shr 17);
365 result
-= (result
shl 9);
366 result
:= result
xor (result
shl 4);
367 result
-= (result
shl 3);
368 result
:= result
xor (result
shl 10);
369 result
:= result
xor (result
shr 15);
373 function locase1251 (ch
: AnsiChar): AnsiChar; inline;
377 if (ch
>= 'A') and (ch
<= 'Z') then Inc(ch
, 32);
381 if (ch
>= #192) and (ch
<= #223) then
388 #168, #170, #175: Inc(ch
, 16);
397 // ////////////////////////////////////////////////////////////////////////// //
399 class function THashKeyInt
.hash (const k
: Integer): LongWord; inline;
401 result
:= LongWord(k
);
402 result
-= (result
shl 6);
403 result
:= result
xor (result
shr 17);
404 result
-= (result
shl 9);
405 result
:= result
xor (result
shl 4);
406 result
-= (result
shl 3);
407 result
:= result
xor (result
shl 10);
408 result
:= result
xor (result
shr 15);
411 class function THashKeyInt
.equ (const a
, b
: Integer): Boolean; inline; begin result
:= (a
= b
); end;
412 class procedure THashKeyInt
.freekey (k
: Integer); inline; begin end;
415 // ////////////////////////////////////////////////////////////////////////// //
417 class function THashKeyStr
.hash (const k
: AnsiString): LongWord; inline; begin if (Length(k
) > 0) then result
:= fnvHash((@k
[1])^, Length(k
)) else result
:= 0; end;
418 class function THashKeyStr
.equ (const a
, b
: AnsiString): Boolean; inline; begin result
:= (a
= b
); end;
419 class procedure THashKeyStr
.freekey (var k
: AnsiString); inline; begin k
:= ''; end;
422 // ////////////////////////////////////////////////////////////////////////// //
423 // case-insensitive (ansi)
426 // fnv-1a: http://www.isthe.com/chongo/tech/comp/fnv/
427 function fnvHashLo (constref buf
; len
: LongWord): LongWord;
432 result
:= 2166136261; // fnv offset basis
435 result
:= result
xor Byte(locase1251(b
^));
436 result
:= result
*16777619; // 32-bit fnv prime
443 class function THashKeyStrAnsiCI
.hash (const k
: AnsiString): LongWord; inline; begin if (Length(k
) > 0) then result
:= fnvHashLo((@k
[1])^, Length(k
)) else result
:= 0; end;
444 class function THashKeyStrAnsiCI
.equ (const a
, b
: AnsiString): Boolean; inline;
449 if (Length(a
) = Length(b
)) then
451 for f
:= 1 to Length(a
) do if (locase1251(a
[f
]) <> locase1251(b
[f
])) then exit
;
455 class procedure THashKeyStrAnsiCI
.freekey (var k
: AnsiString); inline; begin k
:= ''; end;
458 // ////////////////////////////////////////////////////////////////////////// //
459 function THashBase
.TEntry
.getEmpty (): Boolean; inline; begin result
:= (hash
= 0); end;
462 // ////////////////////////////////////////////////////////////////////////// //
463 function THashBase
.getCapacity (): Integer; inline; begin result
:= Length(mBuckets
); end;
466 constructor THashBase
.Create (afreevalfn
: TFreeValueFn
=nil);
468 freevalfn
:= afreevalfn
;
469 mSeed
:= u32Hash($29a);
471 mFreeEntryHead
:= nil;
478 destructor THashBase
.Destroy ();
487 procedure THashBase
.freeEntries ();
492 if (mFirstEntry
>= 0) then
494 for f
:= mFirstEntry
to mLastEntry
do
499 HashObjT
.freekey(e
.key
);
500 if assigned(freevalfn
) then freevalfn(e
.value
) else e
.value
:= Default(ValueT
);
501 e
.key
:= Default(KeyT
);
502 e
.value
:= Default(ValueT
);
507 else if (Length(mEntries
) > 0) then
509 FillChar(mEntries
[0], Length(mEntries
)*sizeof(mEntries
[0]), 0);
511 mFreeEntryHead
:= nil;
514 {$IFDEF RBHASH_SANITY_CHECKS}
520 procedure THashBase
.clear ();
524 SetLength(mBuckets, InitSize);
525 FillChar(mBuckets[0], InitSize*sizeof(mBuckets[0]), 0);
526 SetLength(mEntries, InitSize);
527 FillChar(mEntries[0], InitSize*sizeof(mEntries[0]), 0);
529 mFreeEntryHead
:= nil;
538 procedure THashBase
.reset ();
542 if (mBucketsUsed
> 0) then
544 //for idx := 0 to High(mBuckets) do mBuckets[idx] := nil;
545 FillChar(mBuckets
[0], Length(mBuckets
)*sizeof(mBuckets
[0]), 0);
551 function THashBase
.allocEntry (): PEntry
;
555 if (mFreeEntryHead
= nil) then
557 // nothing was allocated, so allocate something now
558 if (Length(mBuckets
) = 0) then
560 assert(Length(mEntries
) = 0);
561 assert(mFirstEntry
= -1);
562 assert(mLastEntry
= -1);
563 assert(mBucketsUsed
= 0);
564 {$IFDEF RBHASH_SANITY_CHECKS}
567 SetLength(mBuckets
, InitSize
);
568 FillChar(mBuckets
[0], InitSize
*sizeof(mBuckets
[0]), 0);
569 SetLength(mEntries
, InitSize
);
570 FillChar(mEntries
[0], InitSize
*sizeof(mEntries
[0]), 0);
572 if (mLastEntry
= High(mEntries
)) then raise Exception
.Create('internal error in hash entry allocator (0.0)');
574 if (mFirstEntry
= -1) then
576 if (mLastEntry
<> 0) then raise Exception
.Create('internal error in hash entry allocator (0.1)');
579 result
:= @mEntries
[mLastEntry
];
580 result
.nextFree
:= nil; // just in case
581 {$IFDEF RBHASH_SANITY_CHECKS}
586 {$IFDEF RBHASH_SANITY_CHECKS}
587 if (mFreeEntryHead
= nil) then raise Exception
.Create('internal error in hash entry allocator (0)');
588 if (not mFreeEntryHead
.empty
) then raise Exception
.Create('internal error in hash entry allocator (1)');
590 result
:= mFreeEntryHead
;
591 mFreeEntryHead
:= result
.nextFree
;
592 {$IFDEF RBHASH_SANITY_CHECKS}
595 result
.nextFree
:= nil; // just in case
596 // fix mFirstEntry and mLastEntry
597 idx
:= Integer((PtrUInt(result
)-PtrUInt(@mEntries
[0])) div sizeof(mEntries
[0]));
598 {$IFDEF RBHASH_SANITY_CHECKS}
599 if (idx
< 0) or (idx
> High(mEntries
)) then raise Exception
.Create('internal error in hash entry allocator (invalid entry address)');
600 if (result
<> @mEntries
[idx
]) then raise Exception
.Create('internal error in hash entry allocator (wtf?!)');
602 if (mFirstEntry
< 0) or (idx
< mFirstEntry
) then mFirstEntry
:= idx
;
603 if (idx
> mLastEntry
) then mLastEntry
:= idx
;
607 procedure THashBase
.releaseEntry (e
: PEntry
);
611 {$IFDEF RBHASH_SANITY_CHECKS}
612 if (mEntriesUsed
= 0) then raise Exception
.Create('internal error in hash entry allocator');
613 if (mEntriesUsed
<> mBucketsUsed
) then raise Exception
.Create('internal error in hash entry allocator (entry/bucket count mismatch)');
614 if (e
= nil) then raise Exception
.Create('internal error in hash entry allocator (trying to release nil entry)');
615 if (e
.empty
) then raise Exception
.Create('internal error in hash entry allocator (trying to release unallocated entry)');
617 idx
:= Integer((PtrUInt(e
)-PtrUInt(@mEntries
[0])) div sizeof(mEntries
[0]));
618 {$IFDEF RBHASH_SANITY_CHECKS}
619 if (idx
< 0) or (idx
> High(mEntries
)) then raise Exception
.Create('internal error in hash entry allocator (invalid entry address)');
620 if (e
<> @mEntries
[idx
]) then raise Exception
.Create('internal error in hash entry allocator (wtf?!)');
622 HashObjT
.freekey(e
.key
);
623 if assigned(freevalfn
) then freevalfn(e
.value
) else e
.value
:= Default(ValueT
);
624 {$IFDEF RBHASH_SANITY_CHECKS}
627 e
.key
:= Default(KeyT
);
628 e
.value
:= Default(ValueT
);
630 e
.nextFree
:= mFreeEntryHead
;
632 // fix mFirstEntry and mLastEntry
633 {$IFDEF RBHASH_SANITY_CHECKS}
634 if (mFirstEntry
< 0) or (mLastEntry
< 0) then raise Exception
.Create('internal error in hash entry allocator (invalid first/last range; 0)');
636 if (mFirstEntry
= mLastEntry
) then
638 {$IFDEF RBHASH_SANITY_CHECKS}
639 if (mEntriesUsed
<> 0) then raise Exception
.Create('internal error in hash entry allocator (invalid first/last range; 1)');
641 mFreeEntryHead
:= nil;
647 {$IFDEF RBHASH_SANITY_CHECKS}
648 if (mEntriesUsed
= 0) then raise Exception
.Create('internal error in hash entry allocator (invalid first/last range; 2)');
650 // fix first entry index
651 if (idx
= mFirstEntry
) then
654 while (mEntries
[cidx
].empty
) do Inc(cidx
);
655 {$IFDEF RBHASH_SANITY_CHECKS}
656 if (cidx
> High(mEntries
)) then raise Exception
.Create('internal error in hash entry allocator (invalid first/last range; 3)');
660 // fix last entry index
661 if (idx
= mLastEntry
) then
664 while (mEntries
[cidx
].empty
) do Dec(cidx
);
665 {$IFDEF RBHASH_SANITY_CHECKS}
666 if (cidx
< 0) then raise Exception
.Create('internal error in hash entry allocator (invalid first/last range; 3)');
674 function THashBase
.distToStIdx (idx
: LongWord): LongWord; inline;
676 {$IFDEF RBHASH_SANITY_CHECKS}
677 assert(idx
< Length(mBuckets
));
678 assert(mBuckets
[idx
] <> nil);
680 result
:= (mBuckets
[idx
].hash
xor mSeed
) and High(mBuckets
);
681 if (result
<= idx
) then result
:= idx
-result
else result
:= idx
+(Length(mBuckets
)-result
);
685 function THashBase
.has (constref akey
: KeyT
; keyhashin
: PLongWord=nil): Boolean;
687 khash
, idx
: LongWord;
688 dist
, pdist
: LongWord;
689 bhigh
, xseed
: LongWord;
692 if (mBucketsUsed
= 0) then exit
;
694 bhigh
:= High(mBuckets
);
697 if (keyhashin
<> nil) then
700 if (khash
= 0) then khash
:= HashObjT
.hash(akey
);
704 khash
:= HashObjT
.hash(akey
);
706 if (khash
= 0) then khash
:= $29a;
708 idx
:= (khash
xor xseed
) and bhigh
;
709 if (mBuckets
[idx
] = nil) then exit
;
711 for dist
:= 0 to bhigh
do
713 if (mBuckets
[idx
] = nil) then break
;
714 pdist
:= distToStIdx(idx
);
715 if (dist
> pdist
) then break
;
716 result
:= (mBuckets
[idx
].hash
= khash
) and HashObjT
.equ(mBuckets
[idx
].key
, akey
);
717 if result
then break
;
718 idx
:= (idx
+1) and bhigh
;
723 function THashBase
.get (constref akey
: KeyT
; out rval
: ValueT
; keyhashin
: PLongWord=nil): Boolean;
725 khash
, idx
: LongWord;
726 dist
, pdist
: LongWord;
727 bhigh
, xseed
: LongWord;
730 if (mBucketsUsed
= 0) then begin rval
:= Default(ValueT
); exit
; end;
732 bhigh
:= High(mBuckets
);
735 if (keyhashin
<> nil) then
738 if (khash
= 0) then khash
:= HashObjT
.hash(akey
);
742 khash
:= HashObjT
.hash(akey
);
744 if (khash
= 0) then khash
:= $29a;
746 idx
:= (khash
xor xseed
) and bhigh
;
748 for dist
:= 0 to bhigh
do
750 if (mBuckets
[idx
] = nil) then break
;
751 pdist
:= distToStIdx(idx
);
752 if (dist
> pdist
) then break
;
753 result
:= (mBuckets
[idx
].hash
= khash
) and HashObjT
.equ(mBuckets
[idx
].key
, akey
);
754 if result
then begin rval
:= mBuckets
[idx
].value
; break
; end;
755 idx
:= (idx
+1) and bhigh
;
758 if not result
then rval
:= Default(ValueT
); // just in case
762 procedure THashBase
.putEntryInternal (swpe
: PEntry
);
764 idx
, dist
, pcur
, pdist
: LongWord;
766 bhigh
, xseed
: LongWord;
768 bhigh
:= High(mBuckets
);
770 idx
:= (swpe
.hash
xor xseed
) and bhigh
;
771 {$IFDEF RBHASH_DEBUG_INSERT}writeln('inserting key ', swpe
.key
, '; value=', swpe
.value
, '; wantidx=', idx
, '; bhigh=', bhigh
);{$ENDIF}
773 for dist
:= 0 to bhigh
do
775 if (mBuckets
[idx
] = nil) then
778 {$IFDEF RBHASH_DEBUG_INSERT}writeln(' inserted to ', idx
);{$ENDIF}
779 mBuckets
[idx
] := swpe
;
783 pdist
:= distToStIdx(idx
);
784 if (pcur
> pdist
) then
786 // swapping the current bucket with the one to insert
787 tmpe
:= mBuckets
[idx
];
788 mBuckets
[idx
] := swpe
;
792 idx
:= (idx
+1) and bhigh
;
798 function THashBase
.put (constref akey
: KeyT
; constref aval
: ValueT
; keyhashout
: PLongWord=nil): Boolean;
800 khash
, idx
, dist
, pdist
: LongWord;
801 swpe
: PEntry
= nil; // current entry to swap (or nothing)
802 bhigh
, xseed
: LongWord;
803 newsz
, eidx
: Integer;
807 bhigh
:= High(mBuckets
);
809 khash
:= HashObjT
.hash(akey
);
810 if (khash
= 0) then khash
:= $29a;
811 if (keyhashout
<> nil) then keyhashout
^ := khash
;
812 idx
:= (khash
xor xseed
) and bhigh
;
814 // check if we already have this key
815 if (mBucketsUsed
<> 0) and (mBuckets
[idx
] <> nil) then
817 for dist
:= 0 to bhigh
do
819 if (mBuckets
[idx
] = nil) then break
;
820 pdist
:= distToStIdx(idx
);
821 if (dist
> pdist
) then break
;
822 result
:= (mBuckets
[idx
].hash
= khash
) and HashObjT
.equ(mBuckets
[idx
].key
, akey
);
826 HashObjT
.freekey(mBuckets
[idx
].key
);
827 if assigned(freevalfn
) then freevalfn(mBuckets
[idx
].value
) else mBuckets
[idx
].value
:= Default(ValueT
);
828 mBuckets
[idx
].key
:= akey
;
829 mBuckets
[idx
].value
:= aval
;
832 idx
:= (idx
+1) and bhigh
;
836 // need to resize hash?
837 if (mBucketsUsed
>= (bhigh
+1)*LoadFactorPrc
div 100) then
839 newsz
:= Length(mBuckets
);
840 if (Length(mEntries
) <> newsz
) then raise Exception
.Create('internal error in hash table (resize)');
841 if (newsz
<= 1024*1024*1024) then newsz
*= 2 else raise Exception
.Create('hash table too big');
842 {$IFDEF RBHASH_DEBUG_RESIZE}
843 writeln('resizing hash; used=', mBucketsUsed
, '; total=', (bhigh
+1), '; maxload=', (bhigh
+1)*LoadFactorPrc
div 100, '; newsz=', newsz
);
845 SetLength(mBuckets
, newsz
);
846 // resize entries array
847 eidx
:= Length(mEntries
);
848 SetLength(mEntries
, newsz
);
849 while (eidx
< Length(mEntries
)) do begin mEntries
[eidx
].hash
:= 0; Inc(eidx
); end;
850 // mFreeEntryHead will be fixed in `rehash()`
856 swpe
:= allocEntry();
861 putEntryInternal(swpe
);
865 // see http://codecapsule.com/2013/11/17/robin-hood-hashing-backward-shift-deletion/
866 function THashBase
.del (constref akey
: KeyT
; keyhashin
: PLongWord=nil): Boolean;
868 khash
, idx
, idxnext
, pdist
, dist
: LongWord;
869 bhigh
, xseed
: LongWord;
872 if (mBucketsUsed
= 0) then exit
;
874 bhigh
:= High(mBuckets
);
877 if (keyhashin
<> nil) then
880 if (khash
= 0) then khash
:= HashObjT
.hash(akey
);
884 khash
:= HashObjT
.hash(akey
);
886 if (khash
= 0) then khash
:= $29a;
888 idx
:= (khash
xor xseed
) and bhigh
;
891 if (mBuckets
[idx
] = nil) then exit
; // no key
892 for dist
:= 0 to bhigh
do
894 if (mBuckets
[idx
] = nil) then break
;
895 pdist
:= distToStIdx(idx
);
896 if (dist
> pdist
) then break
;
897 result
:= (mBuckets
[idx
].hash
= khash
) and HashObjT
.equ(mBuckets
[idx
].key
, akey
);
898 if result
then break
;
899 idx
:= (idx
+1) and bhigh
;
905 {$IFDEF RBHASH_DEBUG_DELETE}
906 writeln('del: key ', akey
, ': not found');
911 {$IFDEF RBHASH_DEBUG_DELETE}
912 writeln('del: key ', akey
, ': found at ', idx
, '; ek=', mBuckets
[idx
].key
, '; ev=', mBuckets
[idx
].value
);
914 releaseEntry(mBuckets
[idx
]);
916 idxnext
:= (idx
+1) and bhigh
;
917 for dist
:= 0 to bhigh
do
919 {$IFDEF RBHASH_DEBUG_DELETE}
920 writeln(' dist=', dist
, '; idx=', idx
, '; idxnext=', idxnext
, '; ce=', (mBuckets
[idx
] <> nil), '; ne=', (mBuckets
[idxnext
] <> nil));
922 if (mBuckets
[idxnext
] = nil) then begin {$IFDEF RBHASH_DEBUG_DELETE}writeln(' idxnext nil');{$ENDIF} mBuckets
[idx
] := nil; break
; end;
923 pdist
:= distToStIdx(idxnext
);
924 if (pdist
= 0) then begin {$IFDEF RBHASH_DEBUG_DELETE}writeln(' pdist is zero');{$ENDIF} mBuckets
[idx
] := nil; break
; end;
925 {$IFDEF RBHASH_DEBUG_DELETE}writeln(' pdist=', pdist
);{$ENDIF}
926 mBuckets
[idx
] := mBuckets
[idxnext
];
927 idx
:= (idx
+1) and bhigh
;
928 idxnext
:= (idxnext
+1) and bhigh
;
935 procedure THashBase
.rehash ();
939 e
: PEntry
= nil; // shut up, fpc!
940 {$IFDEF RBHASH_SANITY_CHECKS}
944 // change seed, to minimize pathological cases
945 //TODO: use prng to generate new hash
946 if (mSeed
= 0) then mSeed
:= $29a;
947 mSeed
:= u32Hash(mSeed
);
949 //for idx := 0 to High(mBuckets) do mBuckets[idx] := nil;
950 FillChar(mBuckets
[0], Length(mBuckets
)*sizeof(mBuckets
[0]), 0);
953 mFreeEntryHead
:= nil;
955 for idx
:= 0 to High(mEntries
) do
958 if (not e
.empty
) then
960 {$IFDEF RBHASH_SANITY_CHECKS}
961 if (e
.nextFree
<> nil) then raise Exception
.Create('internal error in rehash: inconsistent');
962 if (cnt
= 0) and (idx
<> mFirstEntry
) then raise Exception
.Create('internal error in rehash: inconsistent (1)');
964 if (cnt
= mBucketsUsed
) and (idx
<> mLastEntry
) then raise Exception
.Create('internal error in rehash: inconsistent (2)');
966 // no need to recalculate hash
971 if (lastfree
<> nil) then lastfree
.nextFree
:= e
else mFreeEntryHead
:= e
;
975 if (lastfree
<> nil) then lastfree
.nextFree
:= nil;
976 {$IFDEF RBHASH_SANITY_CHECKS}
977 if (cnt
<> mBucketsUsed
) then raise Exception
.Create('internal error in hash table resize (invalid first/last range; 0)');
978 if (cnt
<> mEntriesUsed
) then raise Exception
.Create('internal error in hash table resize (invalid first/last range; 1)');
983 procedure THashBase
.compact ();
985 newsz
, didx
, f
: Integer;
986 {$IFDEF RBHASH_SANITY_CHECKS}
990 newsz
:= nextPOTU32(LongWord(mBucketsUsed
));
991 if (newsz
>= 1024*1024*1024) then exit
;
992 if (newsz
*2 >= Length(mBuckets
)) then exit
;
993 if (newsz
*2 < 128) then exit
;
994 {$IFDEF RBHASH_DEBUG_COMPACT}writeln('compacting; used=', mBucketsUsed
, '; oldsizePOT=', newsz
, '; newsize=', newsz
*2);{$ENDIF}
996 // move all entries to top
997 if (mFirstEntry
>= 0) then
999 {$IFDEF RBHASH_SANITY_CHECKS}
1000 if (mBucketsUsed
< 1) then raise Exception
.Create('internal error in hash table (invalid bucket count; 0)');
1003 while (didx
< Length(mEntries
)) do if (not mEntries
[didx
].empty
) then Inc(didx
) else break
;
1008 if (not mEntries
[f
].empty
) then
1010 {$IFDEF RBHASH_SANITY_CHECKS}
1011 if (didx
>= f
) then raise Exception
.Create('internal error in hash: inconsistent');
1013 mEntries
[didx
] := mEntries
[f
];
1014 mEntries
[f
].hash
:= 0;
1016 if (f
= mLastEntry
) then break
;
1017 while (didx
< Length(mEntries
)) do if (not mEntries
[didx
].empty
) then Inc(didx
) else break
;
1021 {$IFDEF RBHASH_SANITY_CHECKS}
1022 if (didx
<> mBucketsUsed
) then raise Exception
.Create('internal error in hash table (invalid first/last range; 1)');
1025 mLastEntry
:= mBucketsUsed
-1;
1026 {$IFDEF RBHASH_SANITY_CHECKS}
1028 for f
:= mFirstEntry
to mLastEntry
do
1030 if (mEntries
[f
].empty
) then raise Exception
.Create('internal error in hash table (invalid first/last range; 2)');
1033 if (cnt
<> mBucketsUsed
) then raise Exception
.Create('internal error in hash table (invalid first/last range; 3)');
1034 if (cnt
<> mEntriesUsed
) then raise Exception
.Create('internal error in hash table (invalid first/last range; 4)');
1035 for f
:= mLastEntry
+1 to High(mEntries
) do
1037 if (not mEntries
[f
].empty
) then raise Exception
.Create('internal error in hash table (invalid first/last range; 5)');
1043 {$IFDEF RBHASH_SANITY_CHECKS}
1044 if (mBucketsUsed
<> 0) then raise Exception
.Create('internal error in hash table (invalid bucket count; 1)');
1048 SetLength(mBuckets
, newsz
);
1049 SetLength(mEntries
, newsz
);
1050 // mFreeEntryHead will be fixed in `rehash()`
1056 function THashBase
.forEach (it
: TIteratorFn
): Boolean; overload
;
1061 if not assigned(it
) or (mFirstEntry
< 0) then exit
;
1062 for f
:= mFirstEntry
to mLastEntry
do
1064 if (not mEntries
[f
].empty
) then
1066 result
:= it(mEntries
[f
].key
, mEntries
[f
].value
);
1067 if result
then exit
;
1072 function THashBase
.forEach (it
: TIteratorExFn
): Boolean; overload
;
1077 if not assigned(it
) or (mFirstEntry
< 0) then exit
;
1078 for f
:= mFirstEntry
to mLastEntry
do
1080 if (not mEntries
[f
].empty
) then
1082 result
:= it(mEntries
[f
].key
, mEntries
[f
].value
, mEntries
[f
].hash
);
1083 if result
then exit
;
1090 function THashBase
.GetEnumerator (): TValEnumerator
;
1092 if (Length(mEntries
) > 0) then result
:= TValEnumerator
.Create(mEntries
, mFirstEntry
, mLastEntry
)
1093 else result
:= TValEnumerator
.Create(nil, -1, -1);
1096 function THashBase
.byKey (): TKeyEnumerator
;
1098 if (Length(mEntries
) > 0) then result
:= TKeyEnumerator
.Create(mEntries
, mFirstEntry
, mLastEntry
)
1099 else result
:= TKeyEnumerator
.Create(nil, -1, -1);
1102 function THashBase
.byValue (): TValEnumerator
;
1104 if (Length(mEntries
) > 0) then result
:= TValEnumerator
.Create(mEntries
, mFirstEntry
, mLastEntry
)
1105 else result
:= TValEnumerator
.Create(nil, -1, -1);
1108 function THashBase
.byKeyValue (): TKeyValEnumerator
; // PEntry
1110 if (Length(mEntries
) > 0) then result
:= TKeyValEnumerator
.Create(mEntries
, mFirstEntry
, mLastEntry
)
1111 else result
:= TKeyValEnumerator
.Create(nil, -1, -1);
1115 function THashBase
.TValEnumerator
.GetEnumerator (): TValEnumerator
; inline; begin result
.mEntries
:= self
.mEntries
; result
.mFirstEntry
:= self
.mFirstEntry
; result
.mLastEntry
:= self
.mLastEntry
; result
.cur
:= self
.cur
; end;
1116 function THashBase
.TKeyEnumerator
.GetEnumerator (): TKeyEnumerator
; inline; begin result
.mEntries
:= self
.mEntries
; result
.mFirstEntry
:= self
.mFirstEntry
; result
.mLastEntry
:= self
.mLastEntry
; result
.cur
:= self
.cur
; end;
1117 function THashBase
.TKeyValEnumerator
.GetEnumerator (): TKeyValEnumerator
; inline; begin result
.mEntries
:= self
.mEntries
; result
.mFirstEntry
:= self
.mFirstEntry
; result
.mLastEntry
:= self
.mLastEntry
; result
.cur
:= self
.cur
; end;
1120 // ////////////////////////////////////////////////////////////////////////// //
1121 constructor THashBase
.TValEnumerator
.Create (const aents
: TEntryArray
; afirst
, alast
: Integer);
1124 mFirstEntry
:= afirst
;
1125 mLastEntry
:= alast
;
1126 cur
:= mFirstEntry
-1;
1129 function THashBase
.TValEnumerator
.MoveNext (): Boolean; inline;
1132 while (cur
<= mLastEntry
) do
1134 if (not mEntries
[cur
].empty
) then begin result
:= true; exit
; end;
1139 function THashBase
.TValEnumerator
.getCurrent (): ValueT
; inline;
1141 result
:= mEntries
[cur
].value
;
1145 // ////////////////////////////////////////////////////////////////////////// //
1146 constructor THashBase
.TKeyEnumerator
.Create (const aents
: TEntryArray
; afirst
, alast
: Integer);
1149 mFirstEntry
:= afirst
;
1150 mLastEntry
:= alast
;
1151 cur
:= mFirstEntry
-1;
1154 function THashBase
.TKeyEnumerator
.MoveNext (): Boolean; inline;
1157 while (cur
<= mLastEntry
) do
1159 if (not mEntries
[cur
].empty
) then begin result
:= true; exit
; end;
1164 function THashBase
.TKeyEnumerator
.getCurrent (): KeyT
; inline;
1166 result
:= mEntries
[cur
].key
;
1170 // ////////////////////////////////////////////////////////////////////////// //
1171 constructor THashBase
.TKeyValEnumerator
.Create (const aents
: TEntryArray
; afirst
, alast
: Integer);
1174 mFirstEntry
:= afirst
;
1175 mLastEntry
:= alast
;
1176 cur
:= mFirstEntry
-1;
1179 function THashBase
.TKeyValEnumerator
.MoveNext (): Boolean; inline;
1182 while (cur
<= mLastEntry
) do
1184 if (not mEntries
[cur
].empty
) then begin result
:= true; exit
; end;
1189 function THashBase
.TKeyValEnumerator
.getCurrent (): PEntry
; inline;
1191 result
:= @mEntries
[cur
];