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_DELETE}
19 {$IF DEFINED(D2F_DEBUG)}
20 {$DEFINE RBHASH_SANITY_CHECKS}
22 // hash table (robin hood)
29 // WARNING! don't put structures into hash, use ponters or ids!
30 generic THashBase
<KeyT
, ValueT
> = class(TObject
)
32 const InitSize
= {$IF DEFINED(D2F_DEBUG)}16{$ELSE}512{$ENDIF};
33 const LoadFactorPrc
= 90; // it is ok for robin hood hashes
36 type THashFn
= function (constref o
: KeyT
): LongWord;
37 type TEquFn
= function (constref a
, b
: KeyT
): Boolean;
45 hash
: LongWord; // key hash or 0
52 mBuckets
: array of PEntry
; // entries, points to mEntries elements
53 mBucketsUsed
: Integer;
54 mEntries
: array of TEntry
;
55 mEntriesUsed
: Integer;
56 mFreeEntryHead
: Integer;
59 function allocEntry (): PEntry
;
60 procedure releaseEntry (e
: PEntry
);
62 function distToStIdx (idx
: LongWord): LongWord; inline;
64 procedure putEntryInternal (swpe
: PEntry
);
67 constructor Create (ahashfn
: THashFn
; aequfn
: TEquFn
);
68 destructor Destroy (); override;
74 function get (constref akey
: KeyT
; out rval
: ValueT
): Boolean; // `true`: found
75 function put (constref akey
: KeyT
; constref aval
: ValueT
): Boolean; // `true`: replaced
76 function has (constref akey
: KeyT
): Boolean; // `true`: found
77 function del (constref akey
: KeyT
): Boolean; // `true`: deleted
79 property count
: Integer read mBucketsUsed
;
89 // ////////////////////////////////////////////////////////////////////////// //
90 constructor THashBase
.Create (ahashfn
: THashFn
; aequfn
: TEquFn
);
92 if not assigned(ahashfn
) then raise Exception
.Create('cannot create hash without hash function');
93 if not assigned(aequfn
) then raise Exception
.Create('cannot create hash without equality function');
102 destructor THashBase
.Destroy ();
110 function THashBase
.allocEntry (): PEntry
;
112 {$IFDEF RBHASH_SANITY_CHECKS}
113 if (mFreeEntryHead
= -1) then raise Exception
.Create('internal error in hash entry allocator (0)');
114 if (mEntries
[mFreeEntryHead
].hash
<> 0) then raise Exception
.Create('internal error in hash entry allocator (1)');
116 result
:= @mEntries
[mFreeEntryHead
];
117 mFreeEntryHead
:= result
.nextFree
;
119 result
.nextFree
:= -1;
123 procedure THashBase
.releaseEntry (e
: PEntry
);
127 {$IFDEF RBHASH_SANITY_CHECKS}
128 if (mEntriesUsed
= 0) then raise Exception
.Create('internal error in hash entry allocator');
129 if (e
= nil) then raise Exception
.Create('internal error in hash entry allocator (trying to release nil entry)');
130 if (e
.nextFree
<> -1) or (e
.hash
= 0) then raise Exception
.Create('internal error in hash entry allocator (trying to release unallocated entry)');
132 idx
:= LongWord((PtrUInt(e
)-PtrUInt(@mEntries
[0])) div sizeof(mEntries
[0]));
133 {$IFDEF RBHASH_SANITY_CHECKS}
134 if (idx
>= Length(mEntries
)) then raise Exception
.Create('internal error in hash entry allocator (invalid calculated index)');
137 e
.nextFree
:= mFreeEntryHead
;
138 mFreeEntryHead
:= idx
;
143 function THashBase
.distToStIdx (idx
: LongWord): LongWord; inline;
147 {$IFDEF RBHASH_SANITY_CHECKS}
148 assert(idx
< Length(mBuckets
));
149 assert(mBuckets
[idx
] <> nil);
151 stidx
:= mBuckets
[idx
].hash
mod Length(mBuckets
);
152 if (stidx
<= idx
) then result
:= idx
-stidx
else result
:= idx
+(Length(mBuckets
)-stidx
);
156 function THashBase
.has (constref akey
: KeyT
): Boolean;
158 khash
, idx
: LongWord;
159 dist
, pdist
: LongWord;
163 if (mBucketsUsed
= 0) then exit
;
165 blen
:= Length(mBuckets
);
166 khash
:= hashfn(akey
); if (khash
= 0) then khash
:= $29a;
167 idx
:= khash
mod blen
;
168 if (mBuckets
[idx
] = nil) then exit
;
170 for dist
:= 0 to blen
-1 do
172 if (mBuckets
[idx
] = nil) then break
;
173 pdist
:= distToStIdx(idx
);
174 if (dist
> pdist
) then break
;
175 result
:= (mBuckets
[idx
].hash
= khash
) and equfn(mBuckets
[idx
].key
, akey
);
176 if result
then break
;
177 idx
:= (idx
+1) mod blen
;
182 function THashBase
.get (constref akey
: KeyT
; out rval
: ValueT
): Boolean;
184 khash
, idx
: LongWord;
185 dist
, pdist
: LongWord;
189 if (mBucketsUsed
= 0) then begin rval
:= Default(ValueT
); exit
; end;
191 blen
:= Length(mBuckets
);
192 khash
:= hashfn(akey
); if (khash
= 0) then khash
:= $29a;
193 idx
:= khash
mod blen
;
194 if (mBuckets
[idx
] = nil) then begin rval
:= Default(ValueT
); exit
; end;
196 for dist
:= 0 to blen
-1 do
198 if (mBuckets
[idx
] = nil) then break
;
199 pdist
:= distToStIdx(idx
);
200 if (dist
> pdist
) then break
;
201 result
:= (mBuckets
[idx
].hash
= khash
) and equfn(mBuckets
[idx
].key
, akey
);
204 rval
:= mBuckets
[idx
].value
;
207 idx
:= (idx
+1) mod blen
;
210 if not result
then rval
:= Default(ValueT
); // just in case
214 procedure THashBase
.putEntryInternal (swpe
: PEntry
);
216 idx
, dist
, pcur
, pdist
: LongWord;
217 tmpe
: PEntry
; // current entry to swap (or nothing)
220 blen
:= Length(mBuckets
);
221 idx
:= swpe
.hash
mod blen
;
223 for dist
:= 0 to blen
-1 do
225 if (mBuckets
[idx
] = nil) then
228 mBuckets
[idx
] := swpe
;
232 pdist
:= distToStIdx(idx
);
233 if (pcur
> pdist
) then
235 // swapping the current bucket with the one to insert
236 tmpe
:= mBuckets
[idx
];
237 mBuckets
[idx
] := swpe
;
241 idx
:= (idx
+1) mod blen
;
247 function THashBase
.put (constref akey
: KeyT
; constref aval
: ValueT
): Boolean;
249 khash
, stidx
, idx
, dist
, pdist
: LongWord;
250 swpe
: PEntry
= nil; // current entry to swap (or nothing)
252 newsz
, eidx
: Integer;
256 blen
:= Length(mBuckets
);
257 khash
:= hashfn(akey
); if (khash
= 0) then khash
:= $29a;
258 stidx
:= khash
mod blen
;
260 // check if we already have this key
262 if (mBucketsUsed
<> 0) and (mBuckets
[idx
] <> nil) then
264 for dist
:= 0 to blen
-1 do
266 if (mBuckets
[idx
] = nil) then break
;
267 pdist
:= distToStIdx(idx
);
268 if (dist
> pdist
) then break
;
269 result
:= (mBuckets
[idx
].hash
= khash
) and equfn(mBuckets
[idx
].key
, akey
);
273 //mBuckets[idx].key := akey;
274 mBuckets
[idx
].value
:= aval
;
277 idx
:= (idx
+1) mod blen
;
281 // need to resize hash?
282 if (mBucketsUsed
>= blen
*LoadFactorPrc
div 100) then
284 newsz
:= Length(mBuckets
);
285 if (Length(mEntries
) <> newsz
) then raise Exception
.Create('internal error in hash table (resize)');
286 if (newsz
<= 1024*1024*1024) then newsz
*= 2 else raise Exception
.Create('hash table too big');
287 {$IFDEF RBHASH_DEBUG_RESIZE}
288 writeln('resizing hash; used=', mBucketsUsed
, '; total=', blen
, '; maxload=', blen
*LoadFactorPrc
div 100, '; newsz=', newsz
);
290 SetLength(mBuckets
, newsz
);
291 // resize entries array
293 SetLength(mEntries
, newsz
);
294 while (eidx
< Length(mEntries
)) do begin mEntries
[eidx
].hash
:= 0; Inc(eidx
); end;
295 // mFreeEntryHead will be fixed in `rehash()`
301 swpe
:= allocEntry();
306 putEntryInternal(swpe
);
310 // see http://codecapsule.com/2013/11/17/robin-hood-hashing-backward-shift-deletion/
311 function THashBase
.del (constref akey
: KeyT
): Boolean;
313 khash
, stidx
, idxcur
, idxnext
, pdist
, dist
: LongWord;
317 if (mBucketsUsed
= 0) then exit
;
319 blen
:= Length(mBuckets
);
320 khash
:= hashfn(akey
); if (khash
= 0) then khash
:= $29a;
321 stidx
:= khash
mod blen
;
324 if (mBuckets
[stidx
] = nil) then exit
; // no key
327 for dist
:= 0 to blen
-1 do
329 if (mBuckets
[idxcur
] = nil) then break
;
330 pdist
:= distToStIdx(idxcur
);
331 if (dist
> pdist
) then break
;
332 result
:= (mBuckets
[idxcur
].hash
= khash
) and equfn(mBuckets
[idxcur
].key
, akey
);
333 if result
then break
;
334 idxcur
:= (idxcur
+1) mod blen
;
340 {$IFDEF RBHASH_DEBUG_DELETE}
341 writeln('del: key ', akey
, ': not found');
346 {$IFDEF RBHASH_DEBUG_DELETE}
347 writeln('del: key ', akey
, ': found at ', idxcur
, '(', stidx
, '); ek=', mBuckets
[idxcur
].key
, '; ev=', mBuckets
[idxcur
].value
);
349 releaseEntry(mBuckets
[idxcur
]);
351 idxnext
:= (idxcur
+1) mod blen
;
352 for dist
:= 0 to blen
-1 do
354 {$IFDEF RBHASH_DEBUG_DELETE}
355 writeln(' dist=', dist
, '; idxcur=', idxcur
, '; idxnext=', idxnext
, '; ce=', (mBuckets
[idxcur
] <> nil), '; ne=', (mBuckets
[idxnext
] <> nil));
357 if (mBuckets
[idxnext
] = nil) then begin {$IFDEF RBHASH_DEBUG_DELETE}writeln(' idxnext nil');{$ENDIF} mBuckets
[idxcur
] := nil; break
; end;
358 pdist
:= distToStIdx(idxnext
);
359 if (pdist
= 0) then begin {$IFDEF RBHASH_DEBUG_DELETE}writeln(' pdist is zero');{$ENDIF} mBuckets
[idxcur
] := nil; break
; end;
360 {$IFDEF RBHASH_DEBUG_DELETE}writeln(' pdist=', pdist
);{$ENDIF}
361 mBuckets
[idxcur
] := mBuckets
[idxnext
];
362 idxcur
:= (idxcur
+1) mod blen
;
363 idxnext
:= (idxnext
+1) mod blen
;
370 procedure THashBase
.clear ();
374 SetLength(mBuckets
, InitSize
);
375 for idx
:= 0 to High(mBuckets
) do mBuckets
[idx
] := nil;
377 SetLength(mEntries
, Length(mBuckets
));
378 for idx
:= 0 to High(mEntries
) do
380 mEntries
[idx
].hash
:= 0;
381 mEntries
[idx
].nextFree
:= idx
+1;
383 mEntries
[High(mEntries
)].nextFree
:= -1;
391 procedure THashBase
.rehash ();
393 idx
, lastfree
: Integer;
397 for idx
:= 0 to High(mBuckets
) do mBuckets
[idx
] := nil;
400 mFreeEntryHead
:= -1;
402 for idx
:= 0 to High(mEntries
) do
405 if (e
.hash
<> 0) then
411 if (lastfree
<> -1) then mEntries
[lastfree
].nextFree
:= idx
else mFreeEntryHead
:= idx
;