Recent

Author Topic: Fast and simple hash table for static data  (Read 1458 times)

ALLIGATOR

  • Sr. Member
  • ****
  • Posts: 456
  • I use FPC [main] 💪🐯💪
Fast and simple hash table for static data
« on: September 05, 2025, 07:58:33 am »
I was practicing creating hash tables and accidentally managed to create a fairly fast hash table for static data (which is added only once)

Compared to various containers such as:
TDictionary<string, string>
ghashmap.THashmap<string, string, mormot>
Contnrs.TFPStringHashTable

I managed to achieve a multiple acceleration relative to them, ranging from 2-3 to 4-6 times (depending on many factors, sometimes even 10 times, if you set a larger number of test runs)

So I am sharing the results with the community, maybe someone will find it useful

(although this hash table is designed so that data can only be added once, its speed is not due to this, i.e., if you redesign it to be more universal with the ability to add data, the reading speed will still be high, since the reading code will not change)

This is a link to godbolt https://godbolt.org/z/EqzhxGnxK
And in the attached archive, there is a normal Lazarus project source code (with separate unit files)

Oh, yes, I completely forgot to mention... only  O:-) FPC [main] O:-) is supported, as it uses the new version of RTTI.TValue, which has the .AsType<> method

UPD: When this patch is merged, this code will no longer make sense, so there is no need to analyze it too closely, as I myself will abandon it in favor of the standard TDictionary from FPC [main]
https://gitlab.com/freepascal.org/fpc/source/-/merge_requests/1142
« Last Edit: September 07, 2025, 09:43:03 am by ALLIGATOR »
I may seem rude - please don't take it personally

bytebites

  • Hero Member
  • *****
  • Posts: 794
Re: Fast and simple hash table for static data
« Reply #1 on: September 07, 2025, 08:47:04 am »
It is fast.
Code: Pascal  [Select][+][-]
  1.   PerfHash:=TPerfHash.Create;
  2.   PerfHash.Generate([
  3.     ['abfcmbk',    'abfcmbk'],
  4.     ['abfcmbk',    'abfcmbk2']
  5.   ]);
  6.   WriteLn(PerfHash.GetValue('abfcmbk')^);
  7.   WriteLn(PerfHash.GetValue('baabaaa')^);

Causes infinite loop.

Thaddy

  • Hero Member
  • *****
  • Posts: 19455
  • Glad to be alive.
Re: Fast and simple hash table for static data
« Reply #2 on: September 07, 2025, 09:36:04 am »
Not here...
Maybe you need to update your trunk?
Maybe you are on 32 bit ? (Although N is well within range)

You can make N a type, btw:
Code: Pascal  [Select][+][-]
  1. type N = 0..32*1024*1024;// instead of const
  2. // and replace for i := 0 to with
  3. for i in N do
Such loops will always finish.
« Last Edit: September 07, 2025, 09:40:24 am by Thaddy »
Any "programmer" that knows only one programming language is not a programmer

ALLIGATOR

  • Sr. Member
  • ****
  • Posts: 456
  • I use FPC [main] 💪🐯💪
Re: Fast and simple hash table for static data
« Reply #3 on: September 07, 2025, 09:38:11 am »
Actually, it doesn't really matter. In FPC [main], a patch is coming out any day now that brings the standard TDictionary to comparable speed. So there's no particular point to this code

As for what causes the infinite loop, it's interesting, of course, because the same code runs on godbolt and doesn't cause an infinite loop 🤷‍♂️

https://gitlab.com/freepascal.org/fpc/source/-/merge_requests/1142
« Last Edit: September 07, 2025, 09:40:07 am by ALLIGATOR »
I may seem rude - please don't take it personally

Thaddy

  • Hero Member
  • *****
  • Posts: 19455
  • Glad to be alive.
Re: Fast and simple hash table for static data
« Reply #4 on: September 07, 2025, 09:41:10 am »
I tested both Windows and Linux: no infinite loops.
Any "programmer" that knows only one programming language is not a programmer

bytebites

  • Hero Member
  • *****
  • Posts: 794
Re: Fast and simple hash table for static data
« Reply #5 on: September 07, 2025, 09:52:33 am »
The mentioned infinite loop is not related to N.

avk

  • Hero Member
  • *****
  • Posts: 833
Re: Fast and simple hash table for static data
« Reply #6 on: September 07, 2025, 09:59:27 am »
It is fast.
...

Yeah, but the benchmark is too primitive.
I changed it a bit and added a dictionary from LGenerics.
Code: Pascal  [Select][+][-]
  1. program app;
  2. {$mode objfpc}{$h+}
  3. {.$optimization on}
  4.  
  5. uses
  6.   SysUtils, DateUtils,
  7.   Generics.Hashes,
  8.   Generics.Collections,
  9.   ghashmap,
  10.   Contnrs,
  11.   uPerfectHash,
  12.   LgArrayHelpers, LgHash, LgHashMap;
  13.  
  14. type
  15.   TPerfHash = specialize TPerfectHash<string, string>;
  16.  
  17.   TDict = specialize TDictionary<string, string>;
  18.  
  19.   THMHash = class
  20.     class function Hash(const Data: String; n: SizeInt): UInt32; static; inline;
  21.   end;
  22.  
  23.   TLgHash = class
  24.     class function HashCode(const s: string): SizeInt; static; inline;
  25.     class function Equal(const L, R: string): Boolean; static; inline;
  26.   end;
  27.  
  28.  
  29. class function THMHash.Hash(const Data: String; n: SizeInt): UInt32;
  30. begin
  31.   Result:=mORMotHasher(0, @Data[1], Length(Data)) and (n-1);
  32. end;
  33.  
  34. class function TLgHash.HashCode(const s: string): SizeInt;
  35. begin
  36.   Result := FNV1A_JesteressM(Pointer(s), Length(s));
  37. end;
  38.  
  39. class function TLgHash.Equal(const L, R: string): Boolean;
  40. begin
  41.   Result := L = R;
  42. end;
  43.  
  44. type
  45.   TGHashMap = ghashmap.specialize THashmap<string, string, THMHash>;
  46.   TMapType  = specialize TGLiteChainHashMap<string, string, TLgHash>;
  47.  
  48. const
  49.   //N = 32*1024*1024;
  50.   N = 3*1024*1024;
  51.  
  52. var
  53.   PerfHash: TPerfHash;
  54.   Dict: TDict;
  55.   GHM: TGHashMap;
  56.   ContnrsSHT: Contnrs.TFPStringHashTable;
  57.   LgMap: TMapType.TMap;
  58.   i: UInt32;
  59.   j: Int32;
  60.   t: TDateTime;
  61.   s: string;
  62.   StrArr: array of string = (
  63.     'type','path','lines','line_number','submatches','start','end','elapsed','human',
  64.     'matched_lines','matches','searches','searches_with_match'
  65.   );
  66.   Test: array of string;
  67.  
  68. begin
  69.  
  70.   Dict:=TDict.Create;
  71.   for s in StrArr do
  72.     Dict.Add(s, s);
  73.  
  74.   GHM:=TGHashMap.Create;
  75.   for s in StrArr do
  76.     GHM.insert(s, s);
  77.  
  78.   ContnrsSHT:=Contnrs.TFPStringHashTable.Create;
  79.   for s in StrArr do
  80.     ContnrsSHT.Add(s, s);
  81.  
  82.   for s in StrArr do
  83.     LgMap.Add(s, s);
  84.  
  85.   PerfHash:=TPerfHash.Create;
  86.   PerfHash.Generate([
  87.     ['type',    'type'],
  88.     ['path',    'path'],
  89.     ['lines',    'lines'],
  90.     ['line_number',    'line_number'],
  91.     ['submatches',    'submatches'],
  92.     ['start',    'start'],
  93.     ['end',    'end'],
  94.     ['elapsed',    'elapsed'],
  95.     ['human',    'human'],
  96.     ['matched_lines',    'matched_lines'],
  97.     ['matches',    'matches'],
  98.     ['searches',    'searches'],
  99.     ['searches_with_match',     'searches_with_match']
  100.   ]);
  101.  
  102.  
  103.   Randomize;
  104.   Test := specialize TGArrayHelpUtil<string>.CreateRandomShuffle(StrArr);
  105.  
  106.   t:=Now;
  107.   for i:=0 to N do
  108.     for j := 0 to High(Test) do
  109.       Dict.TryGetValue(Test[j], s);
  110.   WriteLn('Dict: ', MilliSecondsBetween(Now, t), 'ms');
  111.  
  112.   t:=Now;
  113.   for i:=0 to N do
  114.     for j := 0 to High(Test) do
  115.       GHM.GetValue(Test[j], s);
  116.   WriteLn('GHM: ', MilliSecondsBetween(Now, t), 'ms');
  117.  
  118.   t:=Now;
  119.   for i:=0 to N do
  120.     for j := 0 to High(Test) do
  121.       s:=ContnrsSHT.Items[Test[j]];
  122.   WriteLn('ContnrsSHT: ', MilliSecondsBetween(Now, t), 'ms');
  123.  
  124.   t:=Now;
  125.   for i:=0 to N do
  126.     for j := 0 to High(Test) do
  127.       LgMap.TryGetValue(Test[j], s);
  128.   WriteLn('LgMap: ', MilliSecondsBetween(Now, t), 'ms');
  129.  
  130.   t:=Now;
  131.   for i:=0 to N do
  132.     for j := 0 to High(Test) do
  133.       s:=PerfHash.GetValue(Test[j])^;
  134.   WriteLn('PerfHash: ', MilliSecondsBetween(Now, t), 'ms');
  135.  
  136.   ReadLn;
  137. end.
  138.  

On my Win64 machine it prints
Code: [Select]
Dict: 4420ms
GHM: 2430ms
ContnrsSHT: 1600ms
LgMap: 730ms
PerfHash: 770ms
It was compiled with optimization level O2.

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 12559
  • Debugger - SynEdit - and more
    • wiki
Re: Fast and simple hash table for static data
« Reply #7 on: September 07, 2025, 10:12:42 am »
From the MR
Quote
I assume the massive speed penalty was from essentially copying AItems[Result] into a temporary structure.

Does "with ... do" guarantee that a pointer is used (or copying is avoided)?  It seems to me that this is relaying on undocumented optimization? Maybe it should explicitly use a pointer?



If you are interested, there are some (waiting) improvements to TDictionary  https://gitlab.com/freepascal.org/fpc/source/-/merge_requests/988

ALLIGATOR

  • Sr. Member
  • ****
  • Posts: 456
  • I use FPC [main] 💪🐯💪
Re: Fast and simple hash table for static data
« Reply #8 on: September 07, 2025, 10:54:07 am »
...
Does "with ... do" guarantee that a pointer is used (or copying is avoided)?
...
Maybe it should explicitly use a pointer?
...

1. Good question—but I don't know the answer 100%, only from my limited experience can I say that it has always been this way
2. Perhaps the pointer will indeed be 100% more reliable



Off topic question: do you (Martin and other users) have any tips on how to quickly test changes in RTL?
For example, to test a patch with TDictionary, I had to make a copy of it and rename all units to UnitNameOriginal_2.pas, and then I could quickly check and test changes in the TDictionary code.

Another way is to recompile the entire FPC, which takes a very long time.

The third way, which I haven't tried, is to recompile only a separate package, for example, rtl-generics. Something like make rtl-generics?
I may seem rude - please don't take it personally

ALLIGATOR

  • Sr. Member
  • ****
  • Posts: 456
  • I use FPC [main] 💪🐯💪
Re: Fast and simple hash table for static data
« Reply #9 on: September 07, 2025, 11:01:19 am »
I changed it a bit and added a dictionary from LGenerics.
Code: [Select]
LgMap: 730ms
PerfHash: 770ms

Cool! I need to study the solutions you applied
I may seem rude - please don't take it personally

ALLIGATOR

  • Sr. Member
  • ****
  • Posts: 456
  • I use FPC [main] 💪🐯💪
Re: Fast and simple hash table for static data
« Reply #10 on: September 07, 2025, 11:02:57 am »
If you are interested, there are some (waiting) improvements to TDictionary  https://gitlab.com/freepascal.org/fpc/source/-/merge_requests/988

I already saw it and even gave it a thumbs up as soon as I saw this merge request  :)
I may seem rude - please don't take it personally

ALLIGATOR

  • Sr. Member
  • ****
  • Posts: 456
  • I use FPC [main] 💪🐯💪
Re: Fast and simple hash table for static data
« Reply #11 on: September 07, 2025, 11:19:43 am »
On my Win64 machine it prints
Code: [Select]
LgMap: 730ms
PerfHash: 770ms
It was compiled with optimization level O2.

Hmm, I get this result on my machine (i7-8750H laptop):
Code: [Select]
Dict: 3882ms
GHM: 1956ms
ContnrsSHT: 1551ms
LgMap: 622ms
PerfHash: 604ms

Apparently, there is no room for further optimization; everything will depend on the speed of the hash function, the amount of data, and the capacity of the hash map. I have heard a little about the currently popular SwissTable, but as far as I understand, they offer greater memory savings and more collisions, as they can check 8 keys at once


UPD: But when I changed the method to TryGetMutValue, the results got way better!

Code: [Select]
LgMap: 478ms
PerfHash: 638ms
« Last Edit: September 07, 2025, 11:22:28 am by ALLIGATOR »
I may seem rude - please don't take it personally

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 12559
  • Debugger - SynEdit - and more
    • wiki
Re: Fast and simple hash table for static data
« Reply #12 on: September 07, 2025, 11:41:19 am »
I managed to achieve a multiple acceleration relative to them, ranging from 2-3 to 4-6 times (depending on many factors, sometimes even 10 times, if you set a larger number of test runs)

If you run benchmarks, then introduce some variances, like
Code: Pascal  [Select][+][-]
  1. {$CODEALIGN PROC=32}
  2. {$CODEALIGN LOOP=32}
  3. {$CODEALIGN JUMP=32}

You may also test with value like 2, 8, 16

Some code (especially very small loops) run a lot faster if aligned at 32bytes (intel) => but the alignment also costs, so it only gains if you get enough savings.
Random changes elsewhere in your code can change the alignment of code that may be affected. So speed changes may sometimes be side effects.

Other ways of testing alignment (but also jump/call distance) are re-ordering of procedures (or even units in uses).

Different alignment of test data may also have an impact, and if data consists of strings, then the way there content (on the heap) is distributed (across a potentially fragmented heap) may impact. As well as allocations by the dictionary may depend on the state of the heap. So changing data (allocation order, padding, size) may have side effects too.

Most side effects wont commonly show, but if you are unlucky, your results could be affected by any one of them. And they may in some cases add changes of a 2 digit percentage.

Compile and bench for diff architectures (e.g. 64 and 32 bits). Maybe even compare the impact at different -O levels.
« Last Edit: September 07, 2025, 12:03:18 pm by Martin_fr »

avk

  • Hero Member
  • *****
  • Posts: 833
Re: Fast and simple hash table for static data
« Reply #13 on: September 07, 2025, 12:12:29 pm »
...
UPD: But when I changed the method to TryGetMutValue, the results got way better!
...

Yeah, refcounting in strings isn't free.

bytebites

  • Hero Member
  • *****
  • Posts: 794
Re: Fast and simple hash table for static data
« Reply #14 on: September 07, 2025, 12:23:27 pm »
Quote
diff --git a/packages/rtl-generics/src/inc/generics.dictionaries.inc b/packages/rtl-generics/src/inc/generics.dictionaries.inc
index f7ecdf36df..25a1c9a605 100644
--- a/packages/rtl-generics/src/inc/generics.dictionaries.inc
+++ b/packages/rtl-generics/src/inc/generics.dictionaries.inc
@@ -800,7 +800,7 @@ function TOpenAddressingLP<OPEN_ADDRESSING_CONSTRAINTS>.DoRemove(AIndex: SizeInt
 function TOpenAddressingLP<OPEN_ADDRESSING_CONSTRAINTS>.FindBucketIndex(const AItems: TArray<TItem>;
   const AKey: TKey; out AHash: UInt32): SizeInt;
 var
-  LItem: {TOpenAddressing<OPEN_ADDRESSING_CONSTRAINTS>.}_TItem; // for workaround Lazarus bug #25613
+  LItem:^ {TOpenAddressing<OPEN_ADDRESSING_CONSTRAINTS>.}_TItem; // for workaround Lazarus bug #25613
   LLengthMask: SizeInt;
   i, m: SizeInt;
   LHash: UInt32;
@@ -820,15 +820,15 @@ function TOpenAddressingLP<OPEN_ADDRESSING_CONSTRAINTS>.FindBucketIndex(const AI
   Result := LHash and LLengthMask;
 
   repeat
-    LItem := _TItem(AItems[Result]);
+    LItem := @_TItem(AItems[Result]);
 
     // Empty position
-    if (LItem.Hash and UInt32.GetSignMask) = 0 then
+    if (LItem^.Hash and UInt32.GetSignMask) = 0 then
       Exit(not Result); // insert!
 
     // Same position?
-    if LItem.Hash = LHash then
-      if FEqualityComparer.Equals(AKey, LItem.Pair.Key) then
+    if LItem^.Hash = LHash then
+      if FEqualityComparer.Equals(AKey, LItem^.Pair.Key) then
         Exit;
 
     Inc(i);

II made quoted changes and the test run is 5x faster. Maybe it is just pure luck. :D

 

TinyPortal © 2005-2018