(* Copyright (C) DooM 2D:Forever Developers
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*)
{$INCLUDE a_modes.inc}
{.$DEFINE RBHASH_DEBUG_RESIZE}
{.$DEFINE RBHASH_DEBUG_DELETE}
{$IF DEFINED(D2F_DEBUG)}
{$DEFINE RBHASH_SANITY_CHECKS}
{$ENDIF}
// hash table (robin hood)
unit hashtable;
interface
type
// WARNING! don't put structures into hash, use ponters or ids!
generic THashBase = class(TObject)
private
const InitSize = {$IF DEFINED(D2F_DEBUG)}16{$ELSE}512{$ENDIF};
const LoadFactorPrc = 90; // it is ok for robin hood hashes
public
type THashFn = function (constref o: KeyT): LongWord;
type TEquFn = function (constref a, b: KeyT): Boolean;
private
type
PEntry = ^TEntry;
TEntry = record
key: KeyT;
value: ValueT;
hash: LongWord; // key hash or 0
nextFree: Integer;
end;
private
hashfn: THashFn;
equfn: TEquFn;
mBuckets: array of PEntry; // entries, points to mEntries elements
mBucketsUsed: Integer;
mEntries: array of TEntry;
mEntriesUsed: Integer;
mFreeEntryHead: Integer;
private
function allocEntry (): PEntry;
procedure releaseEntry (e: PEntry);
function distToStIdx (idx: LongWord): LongWord; inline;
procedure putEntryInternal (swpe: PEntry);
public
constructor Create (ahashfn: THashFn; aequfn: TEquFn);
destructor Destroy (); override;
procedure clear ();
procedure rehash ();
function get (constref akey: KeyT; out rval: ValueT): Boolean; // `true`: found
function put (constref akey: KeyT; constref aval: ValueT): Boolean; // `true`: replaced
function has (constref akey: KeyT): Boolean; // `true`: found
function del (constref akey: KeyT): Boolean; // `true`: deleted
property count: Integer read mBucketsUsed;
end;
implementation
uses
SysUtils;
// ////////////////////////////////////////////////////////////////////////// //
constructor THashBase.Create (ahashfn: THashFn; aequfn: TEquFn);
begin
if not assigned(ahashfn) then raise Exception.Create('cannot create hash without hash function');
if not assigned(aequfn) then raise Exception.Create('cannot create hash without equality function');
hashfn := ahashfn;
equfn := aequfn;
clear();
end;
destructor THashBase.Destroy ();
begin
mBuckets := nil;
mEntries := nil;
inherited;
end;
function THashBase.allocEntry (): PEntry;
begin
{$IFDEF RBHASH_SANITY_CHECKS}
if (mFreeEntryHead = -1) then raise Exception.Create('internal error in hash entry allocator (0)');
if (mEntries[mFreeEntryHead].hash <> 0) then raise Exception.Create('internal error in hash entry allocator (1)');
{$ENDIF}
result := @mEntries[mFreeEntryHead];
mFreeEntryHead := result.nextFree;
Inc(mEntriesUsed);
result.nextFree := -1;
end;
procedure THashBase.releaseEntry (e: PEntry);
var
idx: LongWord;
begin
{$IFDEF RBHASH_SANITY_CHECKS}
if (mEntriesUsed = 0) then raise Exception.Create('internal error in hash entry allocator');
if (e = nil) then raise Exception.Create('internal error in hash entry allocator (trying to release nil entry)');
if (e.nextFree <> -1) or (e.hash = 0) then raise Exception.Create('internal error in hash entry allocator (trying to release unallocated entry)');
{$ENDIF}
idx := LongWord((PtrUInt(e)-PtrUInt(@mEntries[0])) div sizeof(mEntries[0]));
{$IFDEF RBHASH_SANITY_CHECKS}
if (idx >= Length(mEntries)) then raise Exception.Create('internal error in hash entry allocator (invalid calculated index)');
{$ENDIF}
e.hash := 0;
e.nextFree := mFreeEntryHead;
mFreeEntryHead := idx;
Dec(mEntriesUsed);
end;
function THashBase.distToStIdx (idx: LongWord): LongWord; inline;
var
stidx: LongWord;
begin
{$IFDEF RBHASH_SANITY_CHECKS}
assert(idx < Length(mBuckets));
assert(mBuckets[idx] <> nil);
{$ENDIF}
stidx := mBuckets[idx].hash mod Length(mBuckets);
if (stidx <= idx) then result := idx-stidx else result := idx+(Length(mBuckets)-stidx);
end;
function THashBase.has (constref akey: KeyT): Boolean;
var
khash, idx: LongWord;
dist, pdist: LongWord;
blen: LongWord;
begin
result := false;
if (mBucketsUsed = 0) then exit;
blen := Length(mBuckets);
khash := hashfn(akey); if (khash = 0) then khash := $29a;
idx := khash mod blen;
if (mBuckets[idx] = nil) then exit;
for dist := 0 to blen-1 do
begin
if (mBuckets[idx] = nil) then break;
pdist := distToStIdx(idx);
if (dist > pdist) then break;
result := (mBuckets[idx].hash = khash) and equfn(mBuckets[idx].key, akey);
if result then break;
idx := (idx+1) mod blen;
end;
end;
function THashBase.get (constref akey: KeyT; out rval: ValueT): Boolean;
var
khash, idx: LongWord;
dist, pdist: LongWord;
blen: LongWord;
begin
result := false;
if (mBucketsUsed = 0) then begin rval := Default(ValueT); exit; end;
blen := Length(mBuckets);
khash := hashfn(akey); if (khash = 0) then khash := $29a;
idx := khash mod blen;
if (mBuckets[idx] = nil) then begin rval := Default(ValueT); exit; end;
for dist := 0 to blen-1 do
begin
if (mBuckets[idx] = nil) then break;
pdist := distToStIdx(idx);
if (dist > pdist) then break;
result := (mBuckets[idx].hash = khash) and equfn(mBuckets[idx].key, akey);
if result then
begin
rval := mBuckets[idx].value;
break;
end;
idx := (idx+1) mod blen;
end;
if not result then rval := Default(ValueT); // just in case
end;
procedure THashBase.putEntryInternal (swpe: PEntry);
var
idx, dist, pcur, pdist: LongWord;
tmpe: PEntry; // current entry to swap (or nothing)
blen: LongWord;
begin
blen := Length(mBuckets);
idx := swpe.hash mod blen;
pcur := 0;
for dist := 0 to blen-1 do
begin
if (mBuckets[idx] = nil) then
begin
// put entry
mBuckets[idx] := swpe;
Inc(mBucketsUsed);
break;
end;
pdist := distToStIdx(idx);
if (pcur > pdist) then
begin
// swapping the current bucket with the one to insert
tmpe := mBuckets[idx];
mBuckets[idx] := swpe;
swpe := tmpe;
pcur := pdist;
end;
idx := (idx+1) mod blen;
Inc(pcur);
end;
end;
function THashBase.put (constref akey: KeyT; constref aval: ValueT): Boolean;
var
khash, stidx, idx, dist, pdist: LongWord;
swpe: PEntry = nil; // current entry to swap (or nothing)
blen: LongWord;
newsz, eidx: Integer;
begin
result := false;
blen := Length(mBuckets);
khash := hashfn(akey); if (khash = 0) then khash := $29a;
stidx := khash mod blen;
// check if we already have this key
idx := stidx;
if (mBucketsUsed <> 0) and (mBuckets[idx] <> nil) then
begin
for dist := 0 to blen-1 do
begin
if (mBuckets[idx] = nil) then break;
pdist := distToStIdx(idx);
if (dist > pdist) then break;
result := (mBuckets[idx].hash = khash) and equfn(mBuckets[idx].key, akey);
if result then
begin
// replace element
//mBuckets[idx].key := akey;
mBuckets[idx].value := aval;
exit;
end;
idx := (idx+1) mod blen;
end;
end;
// need to resize hash?
if (mBucketsUsed >= blen*LoadFactorPrc div 100) then
begin
newsz := Length(mBuckets);
if (Length(mEntries) <> newsz) then raise Exception.Create('internal error in hash table (resize)');
if (newsz <= 1024*1024*1024) then newsz *= 2 else raise Exception.Create('hash table too big');
{$IFDEF RBHASH_DEBUG_RESIZE}
writeln('resizing hash; used=', mBucketsUsed, '; total=', blen, '; maxload=', blen*LoadFactorPrc div 100, '; newsz=', newsz);
{$ENDIF}
SetLength(mBuckets, newsz);
// resize entries array
eidx := newsz;
SetLength(mEntries, newsz);
while (eidx < Length(mEntries)) do begin mEntries[eidx].hash := 0; Inc(eidx); end;
// mFreeEntryHead will be fixed in `rehash()`
// reinsert entries
rehash();
end;
// create new entry
swpe := allocEntry();
swpe.key := akey;
swpe.value := aval;
swpe.hash := khash;
putEntryInternal(swpe);
end;
// see http://codecapsule.com/2013/11/17/robin-hood-hashing-backward-shift-deletion/
function THashBase.del (constref akey: KeyT): Boolean;
var
khash, stidx, idxcur, idxnext, pdist, dist: LongWord;
blen: LongWord;
begin
result := false;
if (mBucketsUsed = 0) then exit;
blen := Length(mBuckets);
khash := hashfn(akey); if (khash = 0) then khash := $29a;
stidx := khash mod blen;
// find key
if (mBuckets[stidx] = nil) then exit; // no key
idxcur := stidx;
for dist := 0 to blen-1 do
begin
if (mBuckets[idxcur] = nil) then break;
pdist := distToStIdx(idxcur);
if (dist > pdist) then break;
result := (mBuckets[idxcur].hash = khash) and equfn(mBuckets[idxcur].key, akey);
if result then break;
idxcur := (idxcur+1) mod blen;
end;
if not result then
begin
// key not found
{$IFDEF RBHASH_DEBUG_DELETE}
writeln('del: key ', akey, ': not found');
{$ENDIF}
exit;
end;
{$IFDEF RBHASH_DEBUG_DELETE}
writeln('del: key ', akey, ': found at ', idxcur, '(', stidx, '); ek=', mBuckets[idxcur].key, '; ev=', mBuckets[idxcur].value);
{$ENDIF}
releaseEntry(mBuckets[idxcur]);
idxnext := (idxcur+1) mod blen;
for dist := 0 to blen-1 do
begin
{$IFDEF RBHASH_DEBUG_DELETE}
writeln(' dist=', dist, '; idxcur=', idxcur, '; idxnext=', idxnext, '; ce=', (mBuckets[idxcur] <> nil), '; ne=', (mBuckets[idxnext] <> nil));
{$ENDIF}
if (mBuckets[idxnext] = nil) then begin {$IFDEF RBHASH_DEBUG_DELETE}writeln(' idxnext nil');{$ENDIF} mBuckets[idxcur] := nil; break; end;
pdist := distToStIdx(idxnext);
if (pdist = 0) then begin {$IFDEF RBHASH_DEBUG_DELETE}writeln(' pdist is zero');{$ENDIF} mBuckets[idxcur] := nil; break; end;
{$IFDEF RBHASH_DEBUG_DELETE}writeln(' pdist=', pdist);{$ENDIF}
mBuckets[idxcur] := mBuckets[idxnext];
idxcur := (idxcur+1) mod blen;
idxnext := (idxnext+1) mod blen;
end;
Dec(mBucketsUsed);
end;
procedure THashBase.clear ();
var
idx: Integer;
begin
SetLength(mBuckets, InitSize);
for idx := 0 to High(mBuckets) do mBuckets[idx] := nil;
SetLength(mEntries, Length(mBuckets));
for idx := 0 to High(mEntries) do
begin
mEntries[idx].hash := 0;
mEntries[idx].nextFree := idx+1;
end;
mEntries[High(mEntries)].nextFree := -1;
mBucketsUsed := 0;
mEntriesUsed := 0;
mFreeEntryHead := 0;
end;
procedure THashBase.rehash ();
var
idx, lastfree: Integer;
e: PEntry;
begin
// clear buckets
for idx := 0 to High(mBuckets) do mBuckets[idx] := nil;
mBucketsUsed := 0;
// reinsert entries
mFreeEntryHead := -1;
lastfree := -1;
for idx := 0 to High(mEntries) do
begin
e := @mEntries[idx];
if (e.hash <> 0) then
begin
putEntryInternal(e);
end
else
begin
if (lastfree <> -1) then mEntries[lastfree].nextFree := idx else mFreeEntryHead := idx;
lastfree := idx;
end;
end;
end;
end.