Skip to content

Instantly share code, notes, and snippets.

@CynicRus
Last active February 14, 2026 14:20
Show Gist options
  • Select an option

  • Save CynicRus/aeb3ffb8c880bbe465b334c32f5e7467 to your computer and use it in GitHub Desktop.

Select an option

Save CynicRus/aeb3ffb8c880bbe465b334c32f5e7467 to your computer and use it in GitHub Desktop.
Hashmap for FPC/Lazarus
{******************************************************************************
THashMap - Generic hashmap implementation for Free Pascal
Copyright (c) 2026 Aleksandr Vorobev aka CynicRus, CynicRus@gmail.com
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
}
unit HashMap;
{$mode objfpc}{$H+}
{$modeswitch advancedrecords}
interface
uses
SysUtils;
type
{ Generic hashmap class }
{ THashMap }
generic THashMap<TKey, TValue> = class
public
type
TIterationCallback = function(const Key: TKey; const Value: TValue;
Context: Pointer): boolean;
THashFunction = function(const Key: TKey): longword;
TCompareFunction = function(const Key1, Key2: TKey): integer;
TUpdateProc = procedure(const Key: TKey; var Value: TValue; Context: Pointer);
PListNode = ^TListNode;
TListNode = record
Next: PListNode;
Key: TKey;
Value: TValue;
end;
private
const
DefaultCapacity = 8;
GrowthFactor = 2;
private
FBuckets: array of PListNode;
FSize: SizeUInt;
FCapacity: SizeUInt;
FBucketsFilled: SizeUInt;
FHashFunction: THashFunction;
FCompareFunction: TCompareFunction;
{ Node Pool }
FFreeList: PListNode;
{ Internal functions }
function AllocNode: PListNode; inline;
procedure FreeNode(Node: PListNode); inline;
function ListNew(Next: PListNode; const Key: TKey; const Value: TValue): PListNode;
function ListInsert(var Head: PListNode; const Key: TKey;
const Value: TValue): boolean;
function ListFind(Head: PListNode; const Key: TKey; out Value: TValue): boolean;
function ListRemove(var List: PListNode; const Key: TKey;
out Value: TValue): boolean;
function ListIterate(Head: PListNode; Callback: TIterationCallback;
Context: Pointer): boolean;
procedure ListFree(Head: PListNode);
function ListDuplicate(Head: PListNode): PListNode;
function HashIndex(const Key: TKey): SizeUInt;
function CompareKeys(const Key1, Key2: TKey): integer;
function DefaultHash(const Key: TKey): longword;
function DefaultCompare(const Key1, Key2: TKey): integer;
public
constructor Create; overload;
constructor Create(HashFunc: THashFunction; CompareFunc: TCompareFunction); overload;
destructor Destroy; override;
procedure Reserve(MinCapacity: SizeUInt);
{ API functions }
procedure Init;
procedure Grow;
function Insert(const Key: TKey; const Value: TValue): boolean;
// Returns True if overwritten
function Remove(const Key: TKey; out Value: TValue): boolean; overload;
function Remove(const Key: TKey): boolean; overload;
function Get(const Key: TKey; out Value: TValue): boolean;
function Has(const Key: TKey): boolean;
function GetSize: SizeUInt;
procedure Clear;
procedure Iterate(Callback: TIterationCallback; Context: Pointer);
procedure Duplicate(Dest: THashMap);
function Update(const Key: TKey; Proc: TUpdateProc; Context: Pointer): boolean;
property Size: SizeUInt read FSize;
property Capacity: SizeUInt read FCapacity;
end;
{ String-specialized hashmap }
generic TStringHashMap<TValue> = class(specialize THashMap<string, TValue>)
public
constructor Create;
end;
function StringHash(const Key: string): longword; inline;
function StringCompare(const Key1, Key2: string): integer; inline;
function IsPowerOfTwo(x: SizeUInt): boolean; inline;
function NextPowerOfTwo(x: SizeUInt): SizeUInt; inline;
function MaxLoadSize(Cap: SizeUInt): SizeUInt; inline;
implementation
function IsPowerOfTwo(x: SizeUInt): boolean; inline;
begin
Result := (x <> 0) and ((x and (x - 1)) = 0);
end;
function NextPowerOfTwo(x: SizeUInt): SizeUInt; inline;
begin
if x <= 1 then Exit(1);
Dec(x);
x := x or (x shr 1);
x := x or (x shr 2);
x := x or (x shr 4);
x := x or (x shr 8);
x := x or (x shr 16);
{$IFDEF CPU64}
x := x or (x shr 32);
{$ENDIF}
Result := x + 1;
end;
function MaxLoadSize(Cap: SizeUInt): SizeUInt; inline;
begin
Result := (Cap * 3) div 4;
end;
{ THashMap }
constructor THashMap.Create;
begin
inherited Create;
FHashFunction := nil;
FCompareFunction := nil;
FSize := 0;
FCapacity := 0;
FBucketsFilled := 0;
FFreeList := nil; // Init pool
SetLength(FBuckets, 0);
end;
constructor THashMap.Create(HashFunc: THashFunction; CompareFunc: TCompareFunction);
begin
Create;
FHashFunction := HashFunc;
FCompareFunction := CompareFunc;
end;
destructor THashMap.Destroy;
var
i: SizeUInt;
Node: PListNode;
begin
if FCapacity > 0 then
begin
for i := 0 to FCapacity - 1 do
ListFree(FBuckets[i]);
end;
SetLength(FBuckets, 0);
// Free remaining nodes in the pool
while FFreeList <> nil do
begin
Node := FFreeList;
FFreeList := FFreeList^.Next;
Dispose(Node);
end;
inherited Destroy;
end;
procedure THashMap.Reserve(MinCapacity: SizeUInt);
var
TargetCap: SizeUInt;
begin
TargetCap := NextPowerOfTwo((MinCapacity * 4) div 3);
if TargetCap > FCapacity then
begin
if FCapacity = 0 then
begin
FCapacity := TargetCap;
FSize := 0;
FBucketsFilled := 0;
SetLength(FBuckets, FCapacity);
FillChar(FBuckets[0], FCapacity * SizeOf(Pointer), 0);
end
else
begin
FCapacity := TargetCap div GrowthFactor;
Grow;
end;
end;
end;
procedure THashMap.Init;
var
i: SizeUInt;
begin
FCapacity := DefaultCapacity;
FSize := 0;
FBucketsFilled := 0;
SetLength(FBuckets, FCapacity);
for i := 0 to FCapacity - 1 do
FBuckets[i] := nil;
end;
{ Node Pool Implementation }
function THashMap.AllocNode: PListNode; inline;
begin
if FFreeList <> nil then
begin
Result := FFreeList;
FFreeList := FFreeList^.Next;
end
else
New(Result);
end;
procedure THashMap.FreeNode(Node: PListNode); inline;
begin
Node^.Next := FFreeList;
FFreeList := Node;
end;
function THashMap.ListNew(Next: PListNode; const Key: TKey;
const Value: TValue): PListNode;
begin
Result := AllocNode;
Result^.Next := Next;
Result^.Key := Key;
Result^.Value := Value;
end;
function THashMap.CompareKeys(const Key1, Key2: TKey): integer;
begin
if Assigned(FCompareFunction) then
Result := FCompareFunction(Key1, Key2)
else
Result := DefaultCompare(Key1, Key2);
end;
function THashMap.DefaultHash(const Key: TKey): longword;
begin
raise Exception.Create('DefaultHash is not safe for generic TKey. Provide a HashFunc.');
end;
function THashMap.ListInsert(var Head: PListNode; const Key: TKey;
const Value: TValue): boolean;
var
Cur, Prev: PListNode;
begin
Cur := Head;
Prev := nil;
while Cur <> nil do
begin
if CompareKeys(Key, Cur^.Key) = 0 then
begin
Cur^.Value := Value;
Exit(True); // overwritten
end;
Prev := Cur;
Cur := Cur^.Next;
end;
if Prev = nil then
Head := ListNew(nil, Key, Value)
else
Prev^.Next := ListNew(nil, Key, Value);
Result := False;
end;
function THashMap.ListFind(Head: PListNode; const Key: TKey; out Value: TValue): boolean;
begin
while Head <> nil do
begin
if CompareKeys(Head^.Key, Key) = 0 then
begin
Value := Head^.Value;
Exit(True);
end;
Head := Head^.Next;
end;
Result := False;
end;
function THashMap.ListRemove(var List: PListNode; const Key: TKey;
out Value: TValue): boolean;
var
Head, Prev: PListNode;
begin
Result := False;
if List = nil then
Exit;
Head := List;
Prev := nil;
while Head <> nil do
begin
if CompareKeys(Head^.Key, Key) = 0 then
begin
Value := Head^.Value;
if Prev = nil then
List := Head^.Next
else
Prev^.Next := Head^.Next;
FreeNode(Head); // Return to pool
Exit(True);
end;
Prev := Head;
Head := Head^.Next;
end;
end;
function THashMap.ListIterate(Head: PListNode; Callback: TIterationCallback;
Context: Pointer): boolean;
begin
if not Assigned(Callback) then
Exit(True);
Result := True;
while Head <> nil do
begin
if not Callback(Head^.Key, Head^.Value, Context) then
Exit(False);
Head := Head^.Next;
end;
end;
procedure THashMap.ListFree(Head: PListNode);
var
Next: PListNode;
begin
while Head <> nil do
begin
Next := Head^.Next;
FreeNode(Head); // Return to pool
Head := Next;
end;
end;
function THashMap.ListDuplicate(Head: PListNode): PListNode;
var
NewHead, NewNext: PListNode;
begin
if Head = nil then
Exit(nil);
NewHead := ListNew(nil, Head^.Key, Head^.Value);
NewNext := NewHead;
while Head^.Next <> nil do
begin
Head := Head^.Next;
NewNext^.Next := ListNew(nil, Head^.Key, Head^.Value);
NewNext := NewNext^.Next;
end;
Result := NewHead;
end;
function THashMap.DefaultCompare(const Key1, Key2: TKey): integer;
begin
raise Exception.Create(
'DefaultCompare is not safe for generic TKey. Provide a CompareFunc.');
end;
function THashMap.HashIndex(const Key: TKey): SizeUInt;
var
Hash: longword;
begin
if not IsPowerOfTwo(FCapacity) then
raise Exception.Create('Capacity must be power of two');
if Assigned(FHashFunction) then
Hash := FHashFunction(Key)
else
Hash := DefaultHash(Key);
Result := Hash and (FCapacity - 1);
end;
procedure THashMap.Grow;
var
NewCapacity, Target: SizeUInt;
OldBuckets: array of PListNode;
OldCapacity: SizeUInt;
i: SizeUInt;
Node, Next: PListNode;
NewIdx: SizeUInt;
Hash: longword;
begin
if FCapacity = 0 then
begin
Init;
Exit;
end;
// Target capacity using GrowthFactor
Target := FCapacity * GrowthFactor;
if Target <= FCapacity then
begin
// overflow or GrowthFactor <= 1 scenario
Target := FCapacity + 1;
if Target <= FCapacity then Exit;
end;
NewCapacity := NextPowerOfTwo(Target);
if NewCapacity <= FCapacity then
Exit;
OldBuckets := FBuckets;
OldCapacity := FCapacity;
FCapacity := NewCapacity;
SetLength(FBuckets, FCapacity);
for i := 0 to FCapacity - 1 do
FBuckets[i] := nil;
FBucketsFilled := 0;
for i := 0 to OldCapacity - 1 do
begin
Node := OldBuckets[i];
while Node <> nil do
begin
Next := Node^.Next;
if Assigned(FHashFunction) then
Hash := FHashFunction(Node^.Key)
else
Hash := DefaultHash(Node^.Key);
NewIdx := Hash and (FCapacity - 1);
Node^.Next := FBuckets[NewIdx];
if FBuckets[NewIdx] = nil then
Inc(FBucketsFilled);
FBuckets[NewIdx] := Node;
Node := Next;
end;
end;
FSize := FSize;
SetLength(OldBuckets, 0);
end;
function THashMap.Insert(const Key: TKey; const Value: TValue): boolean;
var
Idx: SizeUInt;
WasEmpty: boolean;
begin
if FCapacity = 0 then
Init;
if (FSize + 1) > MaxLoadSize(FCapacity) then
Grow;
Idx := HashIndex(Key);
WasEmpty := (FBuckets[Idx] = nil);
Result := ListInsert(FBuckets[Idx], Key, Value);
if not Result then // new key
begin
Inc(FSize);
if WasEmpty then
Inc(FBucketsFilled);
end;
end;
function THashMap.Remove(const Key: TKey; out Value: TValue): boolean;
var
Idx: SizeUInt;
begin
if FCapacity = 0 then
Exit(False);
Idx := HashIndex(Key);
Result := ListRemove(FBuckets[Idx], Key, Value);
if Result then
begin
if FBuckets[Idx] = nil then
Dec(FBucketsFilled);
Dec(FSize);
end;
end;
function THashMap.Remove(const Key: TKey): boolean;
var
Dummy: TValue;
begin
Result := Remove(Key, Dummy);
end;
function THashMap.Get(const Key: TKey; out Value: TValue): boolean;
var
Idx: SizeUInt;
begin
if FCapacity = 0 then
Exit(False);
Idx := HashIndex(Key);
Result := ListFind(FBuckets[Idx], Key, Value);
end;
function THashMap.Has(const Key: TKey): boolean;
var
Dummy: TValue;
begin
Result := Get(Key, Dummy);
end;
function THashMap.GetSize: SizeUInt;
begin
Result := FSize;
end;
procedure THashMap.Clear;
var
i: SizeUInt;
begin
if FCapacity = 0 then Exit;
for i := 0 to FCapacity - 1 do
begin
ListFree(FBuckets[i]);
FBuckets[i] := nil;
end;
FSize := 0;
FBucketsFilled := 0;
end;
procedure THashMap.Iterate(Callback: TIterationCallback; Context: Pointer);
var
i: SizeUInt;
begin
if not Assigned(Callback) then
Exit;
for i := 0 to FCapacity - 1 do
begin
if not ListIterate(FBuckets[i], Callback, Context) then
Break;
end;
end;
procedure THashMap.Duplicate(Dest: THashMap);
var
i: SizeUInt;
begin
if Dest = nil then
raise Exception.Create('Destination hashmap is nil');
if FCapacity = 0 then
begin
Dest.FSize := 0;
Dest.FCapacity := 0;
Dest.FBucketsFilled := 0;
SetLength(Dest.FBuckets, 0);
Exit;
end;
Dest.FCapacity := FCapacity;
Dest.FSize := FSize;
Dest.FBucketsFilled := FBucketsFilled;
Dest.FHashFunction := FHashFunction;
Dest.FCompareFunction := FCompareFunction;
SetLength(Dest.FBuckets, Dest.FCapacity);
for i := 0 to FCapacity - 1 do
Dest.FBuckets[i] := ListDuplicate(FBuckets[i]);
end;
function THashMap.Update(const Key: TKey; Proc: TUpdateProc; Context: Pointer): boolean;
var
Idx: SizeUInt;
Node: PListNode;
begin
if not Assigned(Proc) then Exit(False);
if FCapacity = 0 then Exit(False);
Idx := HashIndex(Key);
Node := FBuckets[Idx];
while Node <> nil do
begin
if CompareKeys(Node^.Key, Key) = 0 then
begin
Proc(Node^.Key, Node^.Value, Context);
Exit(True);
end;
Node := Node^.Next;
end;
Result := False;
end;
{ TStringHashMap }
constructor TStringHashMap.Create;
begin
inherited Create(@StringHash, @StringCompare);
end;
function StringHash(const Key: string): longword; inline;
var
P: pbyte;
Len: SizeInt;
begin
Result := $811c9dc5;
Len := Length(Key);
if Len = 0 then Exit;
P := pbyte(Pointer(Key));
while Len > 0 do
begin
Result := (Result xor P^) * $01000193;
Inc(P);
Dec(Len);
end;
end;
function StringCompare(const Key1, Key2: string): integer; inline;
var
L1, L2: SizeInt;
begin
if Pointer(Key1) = Pointer(Key2) then
Exit(0);
if Pointer(Key1) = nil then Exit(-1);
if Pointer(Key2) = nil then Exit(1);
L1 := Length(Key1);
L2 := Length(Key2);
if L1 <> L2 then
begin
if L1 < L2 then Exit(-1)
else
Exit(1);
end;
Result := CompareByte(pbyte(Key1)^, pbyte(Key2)^, L1);
end;
end.
program bench_hashmaps;
{$mode objfpc}{$H+}
{$modeswitch advancedrecords}
uses
SysUtils, Windows,
fgl, // TFPGMap
Generics.Collections, // TDictionary
HashMap; // THashMap
type
TIntFPGMap = specialize TFPGMap<Integer, Integer>;
TIntHashMap = specialize THashMap<Integer, Integer>;
{ Context for THashMap Update }
PIncCtx = ^TIncCtx;
TIncCtx = record
Delta: Integer;
end;
var
QpcFreq: Int64 = 0;
{ Hash/compare for Integer keys for THashMap<Integer, Integer> }
function Hash(const Key: Integer): LongWord; inline;
begin
Result := LongWord(Key);
Result := Result xor (Result shr 16);
Result := Result * $7feb352d;
Result := Result xor (Result shr 15);
Result := Result * $846ca68b;
Result := Result xor (Result shr 16);
end;
function Cmp(const A, B: Integer): Integer;
begin
if A < B then Exit(-1);
if A > B then Exit(1);
Result := 0;
end;
{ Callback for THashMap.Update }
procedure IncValueProc(const Key: Integer; var Value: Integer; Context: Pointer);
begin
Value := Value + PIncCtx(Context)^.Delta;
end;
function NowTicks: Int64; inline;
begin
QueryPerformanceCounter(Result);
end;
function TicksToUS(const dt: Int64): Int64; inline;
begin
Result := (dt * 1000000) div QpcFreq;
end;
procedure FillKeys(var Keys: array of Integer; Seed: LongWord);
var
i: SizeInt;
x: LongWord;
begin
x := Seed;
for i := 0 to High(Keys) do
begin
// xorshift32
x := x xor (x shl 13);
x := x xor (x shr 17);
x := x xor (x shl 5);
Keys[i] := Integer(x);
end;
end;
procedure Bench_Dictionary(const Keys: array of Integer; out Sum: Int64;
out usIns, usGet, usUpd, usRem: Int64);
var
D: specialize TDictionary<Integer, Integer>;
i: SizeInt;
t0: Int64;
v: Integer;
begin
Sum := 0;
D := specialize TDictionary<Integer, Integer>.Create(Length(Keys));
try
t0 := NowTicks;
for i := 0 to High(Keys) do
D.AddOrSetValue(Keys[i], i);
usIns := TicksToUS(NowTicks - t0);
// UPDATE: read-modify-write
t0 := NowTicks;
for i := 0 to High(Keys) do
if D.TryGetValue(Keys[i], v) then
D.AddOrSetValue(Keys[i], v + 1);
usUpd := TicksToUS(NowTicks - t0);
t0 := NowTicks;
for i := 0 to High(Keys) do
if D.TryGetValue(Keys[i], v) then
Inc(Sum, v);
usGet := TicksToUS(NowTicks - t0);
t0 := NowTicks;
for i := 0 to High(Keys) do
D.Remove(Keys[i]);
usRem := TicksToUS(NowTicks - t0);
finally
D.Free;
end;
end;
procedure Bench_FPGMap(const Keys: array of Integer; out Sum: Int64;
out usIns, usGet, usUpd, usRem: Int64);
var
M: TIntFPGMap;
i: SizeInt;
t0: Int64;
idx: Integer;
begin
Sum := 0;
M := TIntFPGMap.Create;
try
M.Sorted := True;
t0 := NowTicks;
for i := 0 to High(Keys) do
M[Keys[i]] := i;
usIns := TicksToUS(NowTicks - t0);
// UPDATE
t0 := NowTicks;
for i := 0 to High(Keys) do
begin
idx := M.IndexOf(Keys[i]);
if idx >= 0 then
M.Data[idx] := M.Data[idx] + 1;
end;
usUpd := TicksToUS(NowTicks - t0);
t0 := NowTicks;
for i := 0 to High(Keys) do
begin
idx := M.IndexOf(Keys[i]);
if idx >= 0 then
Inc(Sum, M.Data[idx]);
end;
usGet := TicksToUS(NowTicks - t0);
t0 := NowTicks;
for i := 0 to High(Keys) do
begin
idx := M.IndexOf(Keys[i]);
if idx >= 0 then
M.Delete(idx);
end;
usRem := TicksToUS(NowTicks - t0);
finally
M.Free;
end;
end;
procedure Bench_THashMap(const Keys: array of Integer; out Sum: Int64;
out usIns, usGet, usUpd, usRem: Int64);
var
H: TIntHashMap;
i: SizeInt;
t0: Int64;
v: Integer;
Ctx: TIncCtx;
begin
Sum := 0;
H := TIntHashMap.Create(@Hash, @Cmp);
H.Reserve(Length(Keys));
try
t0 := NowTicks;
for i := 0 to High(Keys) do
H.Insert(Keys[i], i);
usIns := TicksToUS(NowTicks - t0);
// UPDATE
Ctx.Delta := 1;
t0 := NowTicks;
for i := 0 to High(Keys) do
H.Update(Keys[i], @IncValueProc, @Ctx);
usUpd := TicksToUS(NowTicks - t0);
t0 := NowTicks;
for i := 0 to High(Keys) do
if H.Get(Keys[i], v) then
Inc(Sum, v);
usGet := TicksToUS(NowTicks - t0);
t0 := NowTicks;
for i := 0 to High(Keys) do
H.Remove(Keys[i]);
usRem := TicksToUS(NowTicks - t0);
finally
H.Free;
end;
end;
procedure RunMultiRound(const Keys: array of Integer; Rounds: Integer);
type
TResult = record
Name: string;
InsMin, GetMin, UpdMin, RemMin: Int64;
InsSum, GetSum, UpdSum, RemSum: Int64;
SumXor: Int64;
end;
procedure InitResult(var R: TResult; const AName: string);
begin
R.Name := AName;
R.InsMin := High(Int64);
R.GetMin := High(Int64);
R.UpdMin := High(Int64);
R.RemMin := High(Int64);
R.InsSum := 0;
R.GetSum := 0;
R.UpdSum := 0;
R.RemSum := 0;
R.SumXor := 0;
end;
procedure Accumulate(var R: TResult; const tIns, tGet, tUpd, tRem, Sum: Int64);
begin
if tIns < R.InsMin then R.InsMin := tIns;
if tGet < R.GetMin then R.GetMin := tGet;
if tUpd < R.UpdMin then R.UpdMin := tUpd;
if tRem < R.RemMin then R.RemMin := tRem;
Inc(R.InsSum, tIns);
Inc(R.GetSum, tGet);
Inc(R.UpdSum, tUpd);
Inc(R.RemSum, tRem);
R.SumXor := R.SumXor xor Sum;
end;
procedure PrintResult(const R: TResult; N: SizeInt; RoundsCount: Integer);
begin
WriteLn(Format('%-32s N=%-7d ins=%8d us upd=%8d us get=%8d us rem=%8d us (min over %d rounds)',
[R.Name, N, R.InsMin, R.UpdMin, R.GetMin, R.RemMin, RoundsCount]));
WriteLn(Format('%-32s %8s ins=%8.0f us upd=%8.0f us get=%8.0f us rem=%8.0f us (avg)',
['', '', R.InsSum / RoundsCount, R.UpdSum / RoundsCount, R.GetSum / RoundsCount, R.RemSum / RoundsCount]));
end;
var
r: Integer;
sum1, sum2, sum3: Int64;
tIns, tGet, tUpd, tRem: Int64;
Dict, FPG, HMap: TResult;
begin
InitResult(Dict, 'Generics.Collections TDictionary');
InitResult(FPG, 'FGL TFPGMap (Sorted=True)');
InitResult(HMap, 'THashMap (chaining)');
for r := 1 to Rounds do
begin
Bench_Dictionary(Keys, sum1, tIns, tGet, tUpd, tRem);
Accumulate(Dict, tIns, tGet, tUpd, tRem, sum1);
Bench_FPGMap(Keys, sum2, tIns, tGet, tUpd, tRem);
Accumulate(FPG, tIns, tGet, tUpd, tRem, sum2);
Bench_THashMap(Keys, sum3, tIns, tGet, tUpd, tRem);
Accumulate(HMap, tIns, tGet, tUpd, tRem, sum3);
if (sum1 <> sum2) or (sum1 <> sum3) then
begin
WriteLn(Format('WARNING (round %d): sums differ (Dict=%d, FPG=%d, HMap=%d). ' +
'Likely duplicates / semantics mismatch.',
[r, sum1, sum2, sum3]));
end;
end;
WriteLn('Benchmark: Insert + Update + Get + Remove (deterministic keyset, QPC timing)');
PrintResult(Dict, Length(Keys), Rounds);
WriteLn;
PrintResult(FPG, Length(Keys), Rounds);
WriteLn;
PrintResult(HMap, Length(Keys), Rounds);
if (Dict.SumXor <> FPG.SumXor) or (Dict.SumXor <> HMap.SumXor) then
WriteLn('NOTE: per-round sum patterns differ (SumXor mismatch). Check duplicate-key handling.');
end;
var
N, Rounds: SizeInt;
Keys: array of Integer;
warmSum: Int64;
warmIns, warmGet, warmUpd, warmRem: Int64;
WarmKeys: array of Integer;
begin
if not QueryPerformanceFrequency(QpcFreq) or (QpcFreq = 0) then
begin
WriteLn('QueryPerformanceFrequency failed or not supported!');
Halt(1);
end;
if ParamCount >= 1 then
N := StrToIntDef(ParamStr(1), 100000)
else
N := 100000;
if ParamCount >= 2 then
Rounds := StrToIntDef(ParamStr(2), 5)
else
Rounds := 5;
SetLength(Keys, N);
FillKeys(Keys, $12345678);
if N > 10000 then
begin
WriteLn('Warming up...');
SetLength(WarmKeys, 10000);
Move(Keys[0], WarmKeys[0], Length(WarmKeys) * SizeOf(WarmKeys[0]));
Bench_Dictionary(WarmKeys, warmSum, warmIns, warmGet, warmUpd, warmRem);
Bench_FPGMap (WarmKeys, warmSum, warmIns, warmGet, warmUpd, warmRem);
Bench_THashMap (WarmKeys, warmSum, warmIns, warmGet, warmUpd, warmRem);
WriteLn;
SetLength(WarmKeys, 0);
end;
RunMultiRound(Keys, Rounds);
end.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment