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, either version 3 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 {$INCLUDE a_modes.inc}
17 {.$DEFINE RBHASH_DEBUG_RESIZE}
18 {.$DEFINE RBHASH_DEBUG_INSERT}
19 {.$DEFINE RBHASH_DEBUG_DELETE}
20 {.$DEFINE RBHASH_DEBUG_COMPACT}
21 {$IF DEFINED(D2F_DEBUG)}
22 {.$DEFINE RBHASH_SANITY_CHECKS}
24 // hash table (robin hood)
31 // WARNING! don't put structures into hash, use ponters or ids!
32 generic THashBase
<KeyT
, ValueT
> = class(TObject
)
34 const InitSize
= {$IF DEFINED(RBHASH_SANITY_CHECKS)}16{$ELSE}256{$ENDIF}; // *MUST* be power of two
35 const LoadFactorPrc
= 90; // it is ok for robin hood hashes
38 type THashFn
= function (constref o
: KeyT
): LongWord;
39 type TEquFn
= function (constref a
, b
: KeyT
): Boolean;
40 type TIteratorFn
= function (constref k
: KeyT
; constref v
: ValueT
): Boolean is nested
; // return `true` to stop
41 type TIteratorExFn
= function (constref k
: KeyT
; constref v
: ValueT
; keyhash
: LongWord): Boolean is nested
; // return `true` to stop
50 hash
: LongWord; // key hash or 0
51 nextFree
: PEntry
; // next free entry
53 property keyhash
: LongWord read hash
;
58 TEntryArray
= array of TEntry
;
62 TValEnumerator
= record
64 mEntries
: TEntryArray
;
65 mFirstEntry
, mLastEntry
, cur
: Integer;
67 constructor Create (const aents
: TEntryArray
; afirst
, alast
: Integer);
68 function MoveNext (): Boolean; inline;
69 function getCurrent (): ValueT
; inline;
70 function GetEnumerator (): TValEnumerator
; inline;
71 property Current
: ValueT read getCurrent
;
74 TKeyEnumerator
= record
76 mEntries
: TEntryArray
;
77 mFirstEntry
, mLastEntry
, cur
: Integer;
79 constructor Create (const aents
: TEntryArray
; afirst
, alast
: Integer);
80 function MoveNext (): Boolean; inline;
81 function getCurrent (): KeyT
; inline;
82 function GetEnumerator (): TKeyEnumerator
; inline;
83 property Current
: KeyT read getCurrent
;
86 TKeyValEnumerator
= record
88 mEntries
: TEntryArray
;
89 mFirstEntry
, mLastEntry
, cur
: Integer;
91 constructor Create (const aents
: TEntryArray
; afirst
, alast
: Integer);
92 function MoveNext (): Boolean; inline;
93 function getCurrent (): PEntry
; inline;
94 function GetEnumerator (): TKeyValEnumerator
; inline;
95 property Current
: PEntry read getCurrent
;
101 mBuckets
: array of PEntry
; // entries, points to mEntries elements
102 mBucketsUsed
: Integer;
103 mEntries
: TEntryArray
;
104 {$IFDEF RBHASH_SANITY_CHECKS}
105 mEntriesUsed
: Integer;
107 mFreeEntryHead
: PEntry
;
108 mFirstEntry
, mLastEntry
: Integer;
112 function allocEntry (): PEntry
;
113 procedure releaseEntry (e
: PEntry
);
115 //function distToStIdx (idx: LongWord): LongWord; inline;
117 procedure putEntryInternal (swpe
: PEntry
);
119 function getCapacity (): Integer; inline;
122 constructor Create (ahashfn
: THashFn
; aequfn
: TEquFn
);
123 destructor Destroy (); override;
126 procedure reset (); // don't shrink buckets
129 procedure compact (); // call this instead of `rehash()` after alot of deletions
131 // you may pass `keyhash` to bypass hash calculation
132 function get (constref akey
: KeyT
; out rval
: ValueT
; keyhashin
: PLongWord=nil): Boolean; // `true`: found
133 // the function may return calculated value hash in `keyhash`
134 function put (constref akey
: KeyT
; constref aval
: ValueT
; keyhashout
: PLongWord=nil): Boolean; // `true`: replaced
135 // you may pass `keyhash` to bypass hash calculation
136 function has (constref akey
: KeyT
; keyhashin
: PLongWord=nil): Boolean; // `true`: found
137 // you may pass `keyhash` to bypass hash calculation
138 function del (constref akey
: KeyT
; keyhashin
: PLongWord=nil): Boolean; // `true`: deleted
140 //WARNING! don't modify table in iterator (queries are ok, though)
141 function forEach (it
: TIteratorFn
): Boolean; overload
;
142 function forEach (it
: TIteratorExFn
): Boolean; overload
;
144 // default `for ... in` enums values
145 function GetEnumerator (): TValEnumerator
;
146 function byKey (): TKeyEnumerator
;
147 function byValue (): TValEnumerator
;
148 function byKeyValue (): TKeyValEnumerator
; // PEntry
150 property count
: Integer read mBucketsUsed
;
151 property capacity
: Integer read getCapacity
;
155 TJoaatHasher
= record
157 seed
: LongWord; // initial seed value; MUST BE FIRST
158 hash
: LongWord; // current value
161 constructor Create (aseed
: LongWord);
163 procedure reset (); inline; overload
;
164 procedure reset (aseed
: LongWord); inline; overload
;
166 procedure put (constref buf
; len
: LongWord);
168 // current hash value
169 // you can continue putting data, as this is not destructive
170 function value
: LongWord; inline;
175 THashIntInt
= specialize THashBase
<Integer, Integer>;
176 THashStrInt
= specialize THashBase
<AnsiString, Integer>;
177 THashStrStr
= specialize THashBase
<AnsiString, AnsiString>;
179 function hashNewIntInt (): THashIntInt
;
180 function hashNewStrInt (): THashStrInt
;
181 function hashNewStrStr (): THashStrStr
;
184 function u32Hash (a
: LongWord): LongWord; inline;
185 function fnvHash (constref buf
; len
: LongWord): LongWord;
186 function joaatHash (constref buf
; len
: LongWord): LongWord;
188 function nextPOT (x
: LongWord): LongWord; inline;
192 function hiiequ (constref a
, b
: Integer): Boolean;
193 function hiihash (constref k
: Integer): LongWord;
194 function hsiequ (constref a
, b
: AnsiString): Boolean;
195 function hsihash (constref k
: AnsiString): LongWord;
204 // ////////////////////////////////////////////////////////////////////////// //
207 function nextPOT (x
: LongWord): LongWord; inline;
210 result
:= result
or (result
shr 1);
211 result
:= result
or (result
shr 2);
212 result
:= result
or (result
shr 4);
213 result
:= result
or (result
shr 8);
214 result
:= result
or (result
shr 16);
216 if (x
<> 0) and ((x
and (x
-1)) = 0) then result
:= result
and (not (result
shr 1)) else result
+= 1;
221 // ////////////////////////////////////////////////////////////////////////// //
222 function hiiequ (constref a
, b
: Integer): Boolean; begin result
:= (a
= b
); end;
223 function hsiequ (constref a
, b
: AnsiString): Boolean; begin result
:= (a
= b
); end;
227 function hiihash (constref k
: Integer): LongWord;
229 result
:= LongWord(k
);
230 result
-= (result
shl 6);
231 result
:= result
xor (result
shr 17);
232 result
-= (result
shl 9);
233 result
:= result
xor (result
shl 4);
234 result
-= (result
shl 3);
235 result
:= result
xor (result
shl 10);
236 result
:= result
xor (result
shr 15);
239 function hsihash (constref k
: AnsiString): LongWord;
241 if (Length(k
) > 0) then result
:= fnvHash(PAnsiChar(k
)^, Length(k
)) else result
:= 0;
246 function hashNewIntInt (): THashIntInt
;
248 result
:= THashIntInt
.Create(hiihash
, hiiequ
);
252 function hashNewStrInt (): THashStrInt
;
254 result
:= THashStrInt
.Create(hsihash
, hsiequ
);
258 function hashNewStrStr (): THashStrStr
;
260 result
:= THashStrStr
.Create(hsihash
, hsiequ
);
264 // ////////////////////////////////////////////////////////////////////////// //
267 constructor TJoaatHasher
.Create (aseed
: LongWord);
273 procedure TJoaatHasher
.reset (); inline; overload
;
279 procedure TJoaatHasher
.reset (aseed
: LongWord); inline; overload
;
286 procedure TJoaatHasher
.put (constref buf
; len
: LongWord);
291 if (len
< 1) then exit
;
292 bytes
:= PByte(@buf
);
298 h
:= h
xor (h
shr 6);
306 function TJoaatHasher
.value
: LongWord; inline;
309 result
+= (result
shl 3);
310 result
:= result
xor (result
shr 11);
311 result
+= (result
shl 15);
316 function joaatHash (constref buf
; len
: LongWord): LongWord;
320 h
:= TJoaatHasher
.Create(0);
321 h
.put(PByte(@buf
)^, len
);
326 // ////////////////////////////////////////////////////////////////////////// //
329 // fnv-1a: http://www.isthe.com/chongo/tech/comp/fnv/
330 function fnvHash (constref buf
; len
: LongWord): LongWord;
335 result
:= 2166136261; // fnv offset basis
338 result
:= result
xor b
^;
339 result
:= result
*16777619; // 32-bit fnv prime
349 function u32Hash (a
: LongWord): LongWord; inline;
352 result
-= (result
shl 6);
353 result
:= result
xor (result
shr 17);
354 result
-= (result
shl 9);
355 result
:= result
xor (result
shl 4);
356 result
-= (result
shl 3);
357 result
:= result
xor (result
shl 10);
358 result
:= result
xor (result
shr 15);
363 // ////////////////////////////////////////////////////////////////////////// //
364 constructor THashBase
.Create (ahashfn
: THashFn
; aequfn
: TEquFn
);
366 if not assigned(ahashfn
) then raise Exception
.Create('cannot create hash without hash function');
367 if not assigned(aequfn
) then raise Exception
.Create('cannot create hash without equality function');
371 mSeed
:= u32Hash($29a);
377 destructor THashBase
.Destroy ();
385 procedure THashBase
.clear ();
389 SetLength(mBuckets
, InitSize
);
390 for idx
:= 0 to High(mBuckets
) do mBuckets
[idx
] := nil;
391 SetLength(mEntries
, Length(mBuckets
));
393 {$IFDEF RBHASH_SANITY_CHECKS}
396 mFreeEntryHead
:= nil; //@mEntries[0];
402 procedure THashBase
.reset ();
406 if (mBucketsUsed
> 0) then
408 for idx
:= 0 to High(mBuckets
) do mBuckets
[idx
] := nil;
410 {$IFDEF RBHASH_SANITY_CHECKS}
413 mFreeEntryHead
:= nil; //@mEntries[0];
420 function THashBase
.getCapacity (): Integer; inline; begin result
:= Length(mBuckets
); end;
423 function THashBase
.allocEntry (): PEntry
;
427 if (mFreeEntryHead
= nil) then
429 if (mLastEntry
= High(mEntries
)) then raise Exception
.Create('internal error in hash entry allocator (0.0)');
431 if (mFirstEntry
= -1) then
433 if (mLastEntry
<> 0) then raise Exception
.Create('internal error in hash entry allocator (0.1)');
436 result
:= @mEntries
[mLastEntry
];
437 result
.nextFree
:= nil; // just in case
438 {$IFDEF RBHASH_SANITY_CHECKS}
443 {$IFDEF RBHASH_SANITY_CHECKS}
444 if (mFreeEntryHead
= nil) then raise Exception
.Create('internal error in hash entry allocator (0)');
445 if (mFreeEntryHead
.hash
<> 0) then raise Exception
.Create('internal error in hash entry allocator (1)');
447 result
:= mFreeEntryHead
;
448 mFreeEntryHead
:= result
.nextFree
;
449 {$IFDEF RBHASH_SANITY_CHECKS}
452 result
.nextFree
:= nil; // just in case
453 // fix mFirstEntry and mLastEntry
454 idx
:= Integer((PtrUInt(result
)-PtrUInt(@mEntries
[0])) div sizeof(mEntries
[0]));
455 {$IFDEF RBHASH_SANITY_CHECKS}
456 if (idx
< 0) or (idx
> High(mEntries
)) then raise Exception
.Create('internal error in hash entry allocator (invalid entry address)');
457 if (result
<> @mEntries
[idx
]) then raise Exception
.Create('internal error in hash entry allocator (wtf?!)');
459 if (mFirstEntry
< 0) or (idx
< mFirstEntry
) then mFirstEntry
:= idx
;
460 if (idx
> mLastEntry
) then mLastEntry
:= idx
;
464 procedure THashBase
.releaseEntry (e
: PEntry
);
468 {$IFDEF RBHASH_SANITY_CHECKS}
469 if (mEntriesUsed
= 0) then raise Exception
.Create('internal error in hash entry allocator');
470 if (mEntriesUsed
<> mBucketsUsed
) then raise Exception
.Create('internal error in hash entry allocator (entry/bucket count mismatch)');
471 if (e
= nil) then raise Exception
.Create('internal error in hash entry allocator (trying to release nil entry)');
472 if (e
.hash
= 0) then raise Exception
.Create('internal error in hash entry allocator (trying to release unallocated entry)');
474 idx
:= Integer((PtrUInt(e
)-PtrUInt(@mEntries
[0])) div sizeof(mEntries
[0]));
475 {$IFDEF RBHASH_SANITY_CHECKS}
476 if (idx
< 0) or (idx
> High(mEntries
)) then raise Exception
.Create('internal error in hash entry allocator (invalid entry address)');
477 if (e
<> @mEntries
[idx
]) then raise Exception
.Create('internal error in hash entry allocator (wtf?!)');
479 {$IFDEF RBHASH_SANITY_CHECKS}
483 e
.nextFree
:= mFreeEntryHead
;
484 mFreeEntryHead
:= e
; //idx;
485 // fix mFirstEntry and mLastEntry
486 {$IFDEF RBHASH_SANITY_CHECKS}
487 if (mFirstEntry
< 0) or (mLastEntry
< 0) then raise Exception
.Create('internal error in hash entry allocator (invalid first/last range; 0)');
489 if (mFirstEntry
= mLastEntry
) then
491 {$IFDEF RBHASH_SANITY_CHECKS}
492 if (mEntriesUsed
<> 0) then raise Exception
.Create('internal error in hash entry allocator (invalid first/last range; 1)');
499 {$IFDEF RBHASH_SANITY_CHECKS}
500 if (mEntriesUsed
= 0) then raise Exception
.Create('internal error in hash entry allocator (invalid first/last range; 2)');
502 // fix first entry index
503 if (idx
= mFirstEntry
) then
506 while (mEntries
[cidx
].hash
= 0) do Inc(cidx
);
507 {$IFDEF RBHASH_SANITY_CHECKS}
508 if (cidx
> High(mEntries
)) then raise Exception
.Create('internal error in hash entry allocator (invalid first/last range; 3)');
512 // fix last entry index
513 if (idx
= mLastEntry
) then
516 while (mEntries
[cidx
].hash
= 0) do Dec(cidx
);
517 {$IFDEF RBHASH_SANITY_CHECKS}
518 if (cidx
< 0) then raise Exception
.Create('internal error in hash entry allocator (invalid first/last range; 3)');
527 function THashBase.distToStIdx (idx: LongWord): LongWord; inline;
529 {$IFDEF RBHASH_SANITY_CHECKS}
530 assert(idx < Length(mBuckets));
531 assert(mBuckets[idx] <> nil);
533 result := (mBuckets[idx].hash xor mSeed) and High(mBuckets);
534 if (result <= idx) then result := idx-result else result := idx+(Length(mBuckets)-result);
539 function THashBase
.has (constref akey
: KeyT
; keyhashin
: PLongWord=nil): Boolean;
541 khash
, idx
: LongWord;
542 dist
, pdist
: LongWord;
543 bhigh
, xseed
: LongWord;
546 if (mBucketsUsed
= 0) then exit
;
548 bhigh
:= High(mBuckets
);
551 if (keyhashin
<> nil) then
554 if (khash
= 0) then khash
:= hashfn(akey
);
558 khash
:= hashfn(akey
);
560 if (khash
= 0) then khash
:= $29a;
562 idx
:= (khash
xor xseed
) and bhigh
;
563 if (mBuckets
[idx
] = nil) then exit
;
565 for dist
:= 0 to bhigh
do
567 if (mBuckets
[idx
] = nil) then break
;
568 //pdist := distToStIdx(idx);
569 pdist
:= (mBuckets
[idx
].hash
xor xseed
) and bhigh
;
570 if (pdist
<= idx
) then pdist
:= idx
-pdist
else pdist
:= idx
+((bhigh
+1)-pdist
);
572 if (dist
> pdist
) then break
;
573 result
:= (mBuckets
[idx
].hash
= khash
) and equfn(mBuckets
[idx
].key
, akey
);
574 if result
then break
;
575 idx
:= (idx
+1) and bhigh
;
580 function THashBase
.get (constref akey
: KeyT
; out rval
: ValueT
; keyhashin
: PLongWord=nil): Boolean;
582 khash
, idx
: LongWord;
583 dist
, pdist
: LongWord;
584 bhigh
, xseed
: LongWord;
587 if (mBucketsUsed
= 0) then begin rval
:= Default(ValueT
); exit
; end;
589 bhigh
:= High(mBuckets
);
592 if (keyhashin
<> nil) then
595 if (khash
= 0) then khash
:= hashfn(akey
);
599 khash
:= hashfn(akey
);
601 if (khash
= 0) then khash
:= $29a;
603 idx
:= (khash
xor xseed
) and bhigh
;
605 for dist
:= 0 to bhigh
do
607 if (mBuckets
[idx
] = nil) then break
;
608 //pdist := distToStIdx(idx);
609 pdist
:= (mBuckets
[idx
].hash
xor xseed
) and bhigh
;
610 if (pdist
<= idx
) then pdist
:= idx
-pdist
else pdist
:= idx
+((bhigh
+1)-pdist
);
612 if (dist
> pdist
) then break
;
613 result
:= (mBuckets
[idx
].hash
= khash
) and equfn(mBuckets
[idx
].key
, akey
);
614 if result
then begin rval
:= mBuckets
[idx
].value
; break
; end;
615 idx
:= (idx
+1) and bhigh
;
618 if not result
then rval
:= Default(ValueT
); // just in case
622 procedure THashBase
.putEntryInternal (swpe
: PEntry
);
624 idx
, dist
, pcur
, pdist
: LongWord;
625 tmpe
: PEntry
; // current entry to swap (or nothing)
626 bhigh
, xseed
: LongWord;
628 bhigh
:= High(mBuckets
);
630 idx
:= (swpe
.hash
xor xseed
) and bhigh
;
631 {$IFDEF RBHASH_DEBUG_INSERT}writeln('inserting key ', swpe
.key
, '; value=', swpe
.value
, '; wantidx=', idx
, '; bhigh=', bhigh
);{$ENDIF}
633 for dist
:= 0 to bhigh
do
635 if (mBuckets
[idx
] = nil) then
638 {$IFDEF RBHASH_DEBUG_INSERT}writeln(' inserted to ', idx
);{$ENDIF}
639 mBuckets
[idx
] := swpe
;
643 //pdist := distToStIdx(idx);
644 pdist
:= (mBuckets
[idx
].hash
xor xseed
) and bhigh
;
645 if (pdist
<= idx
) then pdist
:= idx
-pdist
else pdist
:= idx
+((bhigh
+1)-pdist
);
647 if (pcur
> pdist
) then
649 // swapping the current bucket with the one to insert
650 tmpe
:= mBuckets
[idx
];
651 mBuckets
[idx
] := swpe
;
655 idx
:= (idx
+1) and bhigh
;
661 function THashBase
.put (constref akey
: KeyT
; constref aval
: ValueT
; keyhashout
: PLongWord=nil): Boolean;
663 khash
, idx
, dist
, pdist
: LongWord;
664 swpe
: PEntry
= nil; // current entry to swap (or nothing)
665 bhigh
, xseed
: LongWord;
666 newsz
, eidx
: Integer;
670 bhigh
:= High(mBuckets
);
672 khash
:= hashfn(akey
);
673 if (khash
= 0) then khash
:= $29a;
674 if (keyhashout
<> nil) then keyhashout
^ := khash
;
675 idx
:= (khash
xor xseed
) and bhigh
;
677 // check if we already have this key
678 if (mBucketsUsed
<> 0) and (mBuckets
[idx
] <> nil) then
680 for dist
:= 0 to bhigh
do
682 if (mBuckets
[idx
] = nil) then break
;
683 //pdist := distToStIdx(idx);
684 pdist
:= (mBuckets
[idx
].hash
xor xseed
) and bhigh
;
685 if (pdist
<= idx
) then pdist
:= idx
-pdist
else pdist
:= idx
+((bhigh
+1)-pdist
);
687 if (dist
> pdist
) then break
;
688 result
:= (mBuckets
[idx
].hash
= khash
) and equfn(mBuckets
[idx
].key
, akey
);
692 //mBuckets[idx].key := akey;
693 mBuckets
[idx
].value
:= aval
;
696 idx
:= (idx
+1) and bhigh
;
700 // need to resize hash?
701 if (mBucketsUsed
>= (bhigh
+1)*LoadFactorPrc
div 100) then
703 newsz
:= Length(mBuckets
);
704 if (Length(mEntries
) <> newsz
) then raise Exception
.Create('internal error in hash table (resize)');
705 if (newsz
<= 1024*1024*1024) then newsz
*= 2 else raise Exception
.Create('hash table too big');
706 {$IFDEF RBHASH_DEBUG_RESIZE}
707 writeln('resizing hash; used=', mBucketsUsed
, '; total=', (bhigh
+1), '; maxload=', (bhigh
+1)*LoadFactorPrc
div 100, '; newsz=', newsz
);
709 SetLength(mBuckets
, newsz
);
710 // resize entries array
711 eidx
:= Length(mEntries
);
712 SetLength(mEntries
, newsz
);
713 while (eidx
< Length(mEntries
)) do begin mEntries
[eidx
].hash
:= 0; Inc(eidx
); end;
714 // mFreeEntryHead will be fixed in `rehash()`
720 swpe
:= allocEntry();
725 putEntryInternal(swpe
);
729 // see http://codecapsule.com/2013/11/17/robin-hood-hashing-backward-shift-deletion/
730 function THashBase
.del (constref akey
: KeyT
; keyhashin
: PLongWord=nil): Boolean;
732 khash
, idx
, idxnext
, pdist
, dist
: LongWord;
733 bhigh
, xseed
: LongWord;
736 if (mBucketsUsed
= 0) then exit
;
738 bhigh
:= High(mBuckets
);
741 if (keyhashin
<> nil) then
744 if (khash
= 0) then khash
:= hashfn(akey
);
748 khash
:= hashfn(akey
);
750 if (khash
= 0) then khash
:= $29a;
752 idx
:= (khash
xor xseed
) and bhigh
;
755 if (mBuckets
[idx
] = nil) then exit
; // no key
756 for dist
:= 0 to bhigh
do
758 if (mBuckets
[idx
] = nil) then break
;
759 //pdist := distToStIdx(idxcur);
760 pdist
:= (mBuckets
[idx
].hash
xor xseed
) and bhigh
;
761 if (pdist
<= idx
) then pdist
:= idx
-pdist
else pdist
:= idx
+((bhigh
+1)-pdist
);
763 if (dist
> pdist
) then break
;
764 result
:= (mBuckets
[idx
].hash
= khash
) and equfn(mBuckets
[idx
].key
, akey
);
765 if result
then break
;
766 idx
:= (idx
+1) and bhigh
;
772 {$IFDEF RBHASH_DEBUG_DELETE}
773 writeln('del: key ', akey
, ': not found');
778 {$IFDEF RBHASH_DEBUG_DELETE}
779 writeln('del: key ', akey
, ': found at ', idx
, '; ek=', mBuckets
[idx
].key
, '; ev=', mBuckets
[idx
].value
);
781 releaseEntry(mBuckets
[idx
]);
783 idxnext
:= (idx
+1) and bhigh
;
784 for dist
:= 0 to bhigh
do
786 {$IFDEF RBHASH_DEBUG_DELETE}
787 writeln(' dist=', dist
, '; idx=', idx
, '; idxnext=', idxnext
, '; ce=', (mBuckets
[idx
] <> nil), '; ne=', (mBuckets
[idxnext
] <> nil));
789 if (mBuckets
[idxnext
] = nil) then begin {$IFDEF RBHASH_DEBUG_DELETE}writeln(' idxnext nil');{$ENDIF} mBuckets
[idx
] := nil; break
; end;
790 //pdist := distToStIdx(idxnext);
791 pdist
:= (mBuckets
[idxnext
].hash
xor xseed
) and bhigh
;
792 if (pdist
<= idxnext
) then pdist
:= idxnext
-pdist
else pdist
:= idxnext
+((bhigh
+1)-pdist
);
794 if (pdist
= 0) then begin {$IFDEF RBHASH_DEBUG_DELETE}writeln(' pdist is zero');{$ENDIF} mBuckets
[idx
] := nil; break
; end;
795 {$IFDEF RBHASH_DEBUG_DELETE}writeln(' pdist=', pdist
);{$ENDIF}
796 mBuckets
[idx
] := mBuckets
[idxnext
];
797 idx
:= (idx
+1) and bhigh
;
798 idxnext
:= (idxnext
+1) and bhigh
;
805 procedure THashBase
.rehash ();
809 e
: PEntry
= nil; // shut up, fpc!
810 {$IFDEF RBHASH_SANITY_CHECKS}
814 // change seed, to minimize pathological cases
815 //TODO: use prng to generate new hash
816 if (mSeed
= 0) then mSeed
:= $29a;
817 mSeed
:= u32Hash(mSeed
);
819 for idx
:= 0 to High(mBuckets
) do mBuckets
[idx
] := nil;
822 mFreeEntryHead
:= nil;
824 for idx
:= 0 to High(mEntries
) do
827 if (e
.hash
<> 0) then
829 {$IFDEF RBHASH_SANITY_CHECKS}
830 if (e
.nextFree
<> nil) then raise Exception
.Create('internal error in rehash: inconsistent');
831 if (cnt
= 0) and (idx
<> mFirstEntry
) then raise Exception
.Create('internal error in rehash: inconsistent (1)');
833 if (cnt
= mBucketsUsed
) and (idx
<> mLastEntry
) then raise Exception
.Create('internal error in rehash: inconsistent (2)');
835 // no need to recalculate hash
836 //e.hash := hashfn(e.key) xor mSeed; if (e.hash = 0) then e.hash := $29a;
841 if (lastfree
<> nil) then lastfree
.nextFree
:= e
else mFreeEntryHead
:= e
;
845 if (lastfree
<> nil) then e
.nextFree
:= nil;
846 {$IFDEF RBHASH_SANITY_CHECKS}
847 if (cnt
<> mBucketsUsed
) then raise Exception
.Create('internal error in hash table resize (invalid first/last range; 0)');
848 if (cnt
<> mEntriesUsed
) then raise Exception
.Create('internal error in hash table resize (invalid first/last range; 1)');
853 procedure THashBase
.compact ();
855 newsz
, didx
, f
: Integer;
856 {$IFDEF RBHASH_SANITY_CHECKS}
860 newsz
:= nextPOT(LongWord(mBucketsUsed
));
861 if (newsz
>= 1024*1024*1024) then exit
;
862 if (newsz
*2 >= Length(mBuckets
)) then exit
;
863 if (newsz
*2 < 128) then exit
;
864 {$IFDEF RBHASH_DEBUG_COMPACT}writeln('compacting; used=', mBucketsUsed
, '; oldsizePOT=', newsz
, '; newsize=', newsz
*2);{$ENDIF}
866 // move all entries to top
867 if (mFirstEntry
>= 0) then
869 {$IFDEF RBHASH_SANITY_CHECKS}
870 if (mBucketsUsed
< 1) then raise Exception
.Create('internal error in hash table (invalid bucket count; 0)');
873 while (didx
< Length(mEntries
)) do if (mEntries
[didx
].hash
<> 0) then Inc(didx
) else break
;
878 if (mEntries
[f
].hash
<> 0) then
880 {$IFDEF RBHASH_SANITY_CHECKS}
881 if (didx
>= f
) then raise Exception
.Create('internal error in hash: inconsistent');
883 mEntries
[didx
] := mEntries
[f
];
884 mEntries
[f
].hash
:= 0;
886 if (f
= mLastEntry
) then break
;
887 while (didx
< Length(mEntries
)) do if (mEntries
[didx
].hash
<> 0) then Inc(didx
) else break
;
891 {$IFDEF RBHASH_SANITY_CHECKS}
892 if (didx
<> mBucketsUsed
) then raise Exception
.Create('internal error in hash table (invalid first/last range; 1)');
895 mLastEntry
:= mBucketsUsed
-1;
896 {$IFDEF RBHASH_SANITY_CHECKS}
898 for f
:= mFirstEntry
to mLastEntry
do
900 if (mEntries
[f
].hash
= 0) then raise Exception
.Create('internal error in hash table (invalid first/last range; 2)');
903 if (cnt
<> mBucketsUsed
) then raise Exception
.Create('internal error in hash table (invalid first/last range; 3)');
904 if (cnt
<> mEntriesUsed
) then raise Exception
.Create('internal error in hash table (invalid first/last range; 4)');
905 for f
:= mLastEntry
+1 to High(mEntries
) do
907 if (mEntries
[f
].hash
<> 0) then raise Exception
.Create('internal error in hash table (invalid first/last range; 5)');
913 {$IFDEF RBHASH_SANITY_CHECKS}
914 if (mBucketsUsed
<> 0) then raise Exception
.Create('internal error in hash table (invalid bucket count; 1)');
918 SetLength(mBuckets
, newsz
);
919 SetLength(mEntries
, newsz
);
920 // mFreeEntryHead will be fixed in `rehash()`
926 function THashBase
.forEach (it
: TIteratorFn
): Boolean; overload
;
931 if not assigned(it
) then exit
;
933 if (i
< 0) then exit
;
934 while (i
<= mLastEntry
) do
936 if (mEntries
[i
].hash
<> 0) then
938 result
:= it(mEntries
[i
].key
, mEntries
[i
].value
);
945 function THashBase
.forEach (it
: TIteratorExFn
): Boolean; overload
;
950 if not assigned(it
) then exit
;
952 if (i
< 0) then exit
;
953 while (i
<= mLastEntry
) do
955 if (mEntries
[i
].hash
<> 0) then
957 result
:= it(mEntries
[i
].key
, mEntries
[i
].value
, mEntries
[i
].hash
);
966 function THashBase
.GetEnumerator (): TValEnumerator
;
968 if (Length(mEntries
) > 0) then result
:= TValEnumerator
.Create(mEntries
, mFirstEntry
, mLastEntry
)
969 else result
:= TValEnumerator
.Create(nil, -1, -1);
972 function THashBase
.byKey (): TKeyEnumerator
;
974 if (Length(mEntries
) > 0) then result
:= TKeyEnumerator
.Create(mEntries
, mFirstEntry
, mLastEntry
)
975 else result
:= TKeyEnumerator
.Create(nil, -1, -1);
978 function THashBase
.byValue (): TValEnumerator
;
980 if (Length(mEntries
) > 0) then result
:= TValEnumerator
.Create(mEntries
, mFirstEntry
, mLastEntry
)
981 else result
:= TValEnumerator
.Create(nil, -1, -1);
984 function THashBase
.byKeyValue (): TKeyValEnumerator
; // PEntry
986 if (Length(mEntries
) > 0) then result
:= TKeyValEnumerator
.Create(mEntries
, mFirstEntry
, mLastEntry
)
987 else result
:= TKeyValEnumerator
.Create(nil, -1, -1);
991 function THashBase
.TValEnumerator
.GetEnumerator (): TValEnumerator
; inline; begin result
.mEntries
:= self
.mEntries
; result
.mFirstEntry
:= self
.mFirstEntry
; result
.mLastEntry
:= self
.mLastEntry
; result
.cur
:= self
.cur
; end;
992 function THashBase
.TKeyEnumerator
.GetEnumerator (): TKeyEnumerator
; inline; begin result
.mEntries
:= self
.mEntries
; result
.mFirstEntry
:= self
.mFirstEntry
; result
.mLastEntry
:= self
.mLastEntry
; result
.cur
:= self
.cur
; end;
993 function THashBase
.TKeyValEnumerator
.GetEnumerator (): TKeyValEnumerator
; inline; begin result
.mEntries
:= self
.mEntries
; result
.mFirstEntry
:= self
.mFirstEntry
; result
.mLastEntry
:= self
.mLastEntry
; result
.cur
:= self
.cur
; end;
996 // ////////////////////////////////////////////////////////////////////////// //
997 constructor THashBase
.TValEnumerator
.Create (const aents
: TEntryArray
; afirst
, alast
: Integer);
1000 mFirstEntry
:= afirst
;
1001 mLastEntry
:= alast
;
1002 cur
:= mFirstEntry
-1;
1005 function THashBase
.TValEnumerator
.MoveNext (): Boolean; inline;
1008 while (cur
<= mLastEntry
) do
1010 if (mEntries
[cur
].hash
<> 0) then begin result
:= true; exit
; end;
1015 function THashBase
.TValEnumerator
.getCurrent (): ValueT
; inline;
1017 result
:= mEntries
[cur
].value
;
1021 // ////////////////////////////////////////////////////////////////////////// //
1022 constructor THashBase
.TKeyEnumerator
.Create (const aents
: TEntryArray
; afirst
, alast
: Integer);
1025 mFirstEntry
:= afirst
;
1026 mLastEntry
:= alast
;
1027 cur
:= mFirstEntry
-1;
1030 function THashBase
.TKeyEnumerator
.MoveNext (): Boolean; inline;
1033 while (cur
<= mLastEntry
) do
1035 if (mEntries
[cur
].hash
<> 0) then begin result
:= true; exit
; end;
1040 function THashBase
.TKeyEnumerator
.getCurrent (): KeyT
; inline;
1042 result
:= mEntries
[cur
].key
;
1046 // ////////////////////////////////////////////////////////////////////////// //
1047 constructor THashBase
.TKeyValEnumerator
.Create (const aents
: TEntryArray
; afirst
, alast
: Integer);
1050 mFirstEntry
:= afirst
;
1051 mLastEntry
:= alast
;
1052 cur
:= mFirstEntry
-1;
1055 function THashBase
.TKeyValEnumerator
.MoveNext (): Boolean; inline;
1058 while (cur
<= mLastEntry
) do
1060 if (mEntries
[cur
].hash
<> 0) then begin result
:= true; exit
; end;
1065 function THashBase
.TKeyValEnumerator
.getCurrent (): PEntry
; inline;
1067 result
:= @mEntries
[cur
];