Diverse fragen zu Hash Maps in C

#7512089
Lesenswert?

Ich bin gerade dabei, eine utility library in C zu erstellen, in der ich nützlich Funktionen für andere Projekte zusammenfasse (https://github.com/Daniel-Abrecht/dpa-utils). Ist aber alles noch unfertig.

Eine Sache, die ich einbaue, ist eine Art Interning für Buffer Objekte, wie z.B. Strings. Wenn die Daten kleiner sind, als mein Buffer Objekt, speichere ich die direkt dort drin. Andernfalls enthält das Buffer Objekt (momentan) einen Pointer auf einen Eintrag in einem Hash Map. Der Sinn der Sache ist es, Vergleiche von häufig verwendeten Strings schnell & trivial zu machen, und die eventuell dann auch in anderen Hash Maps oder Sets usw. einsetzen zu können.

Dieses Hash Map, das ist der Punkt, an dem ich gerade bin.

Es gibt da einige erschwerende Faktoren. Das HashMap soll Thread Safe sein. Momentan habe ich dafür einfach ein paar Mutex Locks. Ich nutze den Hash, um einen Lock zu wählen, um eine Gruppe an Buckets zu locken, bevor ich sie modifiziere. (mtx_t ist auf meinem System 40 Bytes gross! Darum spendiere ich nicht jedem Bucket seinen eigenen Lock).

Und die Einträge des Hash Map haben einen Reference Counter. Das momentane Design sieht vor, dass der Eintrag und dessen Reference Counter mit einem Pointer referenziert werden können. Das vereinheitlicht das Referencecounting und das Freigeben der Objekte, aber bedeutet auch, das der Eintrag nicht verschoben werden kann.

Bei Hash Maps gibt es ja den Open Addressing und den Closed Addressing Ansatz. Momentan tendiere ich zu letzterem Ansatz, denn wenn die Einträge separat mit malloc allociiert sind, ist es trivial diese nicht zu verschieben.

Was mich nun aber wunder nimmt, man hört ja immer, linked lists (und damit Closed Addressing) seien so schlimm für die CPU Caches, das sei wahnsinnig ineffizient. Und die locks sind ja auch nicht so toll für Performance.

Und damit zur eigentlichen Frage. Wie "schlimm" ist dieser Ansatz tatsächlich? Gibt es da bessere Ansätze?

#7512172
Lesenswert?

Daniel A. schrieb:

....

Zu kompliziert, solange du nicht weißt ob du es wirklich brauchst.

Do the simplest thing that could possibly work. Not the most stupid thing.

Und damit zur eigentlichen Frage. Wie "schlimm" ist dieser Ansatz tatsächlich?

Kann man nicht sagen. Du musst das für deine Daten, in deiner Umgebung und für deine Anwendung messen wenn du es optimieren willst. Aber, dass heißt dann noch nicht dass deine hochgezüchtete Implementierung im Einsatz robust gegenüber kleinen Veränderungen in deinen Eingangsdaten ist.

Gibt es da bessere Ansätze?

Einfache, robuste Standardimplementierungen nehmen. Erst dann hochzüchten wenn es für deine Anwendung wirklich nötig ist. Das heißt wenn du gemessen hast, nicht Bauchi-Bauchi-Gefühl dass dich deine Hashmap zu viel Resourcen kostet.

So würde ich zum Beispiel für das Multithreading am Anfang einen einzigen Lock nehmen und erst mal brutal die ganze Hashmap locken. Vielleicht reicht das in der Anwendung. Wenn's nicht reicht fängst du an zu optimieren. Es gibt auch lock-free Algorithmen für Hashmaps auf die man mal einen Blick werfen könnte.

#7512192
Lesenswert?

Daniel A. schrieb:

Was mich nun aber wunder nimmt, man hört ja immer, linked lists (und damit Closed Addressing) seien so schlimm für die CPU Caches, das sei wahnsinnig ineffizient. Und die locks sind ja auch nicht so toll für Performance.

Generell stimmt das, jedoch hängt das wieder vom System (CPU+Cache+RAM) ab.

Teste doch einfach mal std::vector<> gegen std::list<>, oder schau Dir die entsprechenden Tests an.

Mutex-Locking ist tatsächlich langsam: schau die lockfree-Datenstrukturen an.

Persönliche Seite #7512196
Lesenswert?

Daniel A. schrieb:

Das HashMap soll Thread Safe sein.

Threadsafe Datenstruktren / Container sind relativ unüblich, weil sie kompliziert, ineffizient und Deadlock-anfällig sind. Nebenläufigkeit / Multithreading implementiert man am Besten auf der obersten Anwendungsebene, die Ebenen darunter (inkl. normalen Datenstrukturen) wissen nichts davon. Klar gibt es so etwas wie thread-safe queues, die sind aber genau für diese Anwendungsebene gedacht (und werden bei Single-Thread-Anwendungen überhaupt nicht genutzt).

Diese ganze Funktionalität "in einem" zu implementieren klingt nach "The Blob"-Antipattern und ziemlich unflexibel. Die meisten Sprachen (außer C) bieten die einzelnen Elemente, die du umsetzen willst, als Bausteine fertig an (Hash-Maps, Strings mit SSO, Speicherverwaltung mit Referenzzählung) sodass man sie nach Bedarf kombinieren oder einzeln nutzen kann. Vielleicht wäre das für dich sinnvoller?

Daniel A. schrieb:

Der Sinn der Sache ist es, Vergleiche von häufig verwendeten Strings schnell & trivial zu machen

Strings, welche nicht allzu lang sind, zu vergleichen, ist gar nicht so langsam. Hashes zu berechnen, eine Hash-Map entlangzuhangeln, Mutexe zu locken hingegen schon - insbesondere eben auch wegen der Cache-Misses.

Strings als Key in einer Hashmap zu nutzen passiert auch nicht zu oft; meistens beim Einlesen von ASCII-Formaten, wo Bezeichner einer Bedeutung zugewiesen werden (z.B. "red" nach 0xFF0000 o.ä.). Das bedeutet aber auch, dass man diesen String als Hash-Key auch nur an genau dieser einen Stelle braucht und dann nie wieder. Da macht es dann sehr wenig Sinn dieses "red" irgendwie zu cachen, weil man intern dann doch nur mit 0xFF0000 rechnet; sollte das nicht der Fall sein, stimmt wahrscheinlich etwas an der Architektur nicht.

Ja, in dynamischen Sprachen (ruby, Python, JS...) ist praktisch "alles" eine HashMap und Zugriffe auf Objekt-Attribute sind oft Hash-Zugriffe. In kompilierten Sprachen aber eben nicht.

Daniel A. schrieb:

Vergleiche von häufig verwendeten Strings

Meistens ist ja eine Seite des Vergleichs fest / bekannt. Die braucht man also nicht in eine flexible / threadsichere HashMap stecken, sondern kann den Hash "direkt" nutzen. Oder die HashMap readonly nutzen, dann entfällt das Locking.

#7512376
Lesenswert?

Hannes J. schrieb:

Einfache, robuste Standardimplementierungen nehmen. Erst dann hochzüchten wenn es für deine Anwendung wirklich nötig ist. Das heißt wenn du gemessen hast, nicht Bauchi-Bauchi-Gefühl dass dich deine Hashmap zu viel Resourcen kostet.

Naja, bei C gibt es ja nicht "die" Standardimplementierungen. Und nicht jede ist für meine Zwecke geeignet.

Eine bekannte, die ich gesehen habe, ist z.B. uthash. Sieht ebenfalls sehr nach doubly linked list aus. Und so wie es verwendet wird, kann der Eintrag, der dran hängt, ebenfalls nicht verschoben werden. Soweit ähnlich wie bei mir.

Aber UT_hash_handle enthält 6 Pointer und 2 integer, auf einem 64bit PC sind das Mindestend 56 bytes, das ist schon recht viel.

Thread Safe scheint es auch nicht zu sein.

Und der hash ist immer ein unsigned integer, auf meinem PC ist das 4 bytes. Bei einem hash map sollte der load factor ja nicht zu hoch werden, sind da maximal 2^32 buckets auf einem System, das bis zu 2^48 Addressen hat, nicht etwas wenig?

Andere Implementierungen kenne ich noch nicht. Diese zu evaluieren, ob sie geeignet sind, was Vor / Nachteile sind, ist auch Aufwand. Und ich hole mir damit eine weitere Abhängigkeit rein, um die ich mich dann kümmern muss.

Wilhelm M. schrieb:

Mutex-Locking ist tatsächlich langsam: schau die lockfree-Datenstrukturen an.

Diese reizen mich schon. Sind aber sehr schwierig & aufwendig richtig hin zu kriegen. Vielleicht versuche ich das später nochmal.

Niklas G. schrieb:

Diese ganze Funktionalität "in einem" zu implementieren klingt nach "The Blob"-Antipattern und ziemlich unflexibel. Die meisten Sprachen (außer C) bieten die einzelnen Elemente, die du umsetzen willst, als Bausteine fertig an (Hash-Maps, Strings mit SSO, Speicherverwaltung mit Referenzzählung) sodass man sie nach Bedarf kombinieren oder einzeln nutzen kann. Vielleicht wäre das für dich sinnvoller?

Was ich hier mache ist genau so eine Library, die solche Bausteine anbietet, eine Utility library halt, für C17. Und nein, ich bleibe bei C, das ist nicht verhandelbar.

Herbert B. schrieb:

not invented here-syndron. das muss ich alles selber machen.

Bei C ist das durchaus üblich. So ziemlich jedes grosse Projekt implementiert seine eigenen Memory Management Primitive und Datenstrukturen, mit seinen eigenen Versionen von allerhand üblichen Sachen.

#7512749
Lesenswert?

Ich realisierte gerade noch etwas.

Momentan nutze ich size_t für meine hashs. Die Überlegung war, der load factor der hash map sollte gering sein, man hat also effektiv etwa gleich viele buckets wie Elemente vielleicht auch halb oder doppelt so viele, aber nichts dramatischeres. Also sollte mein hash gleich viele indexe adressieren können wie das System. (ok, mit der logik sollte ich eigentlich intptr_t nehmen, kommt aber normalerweise mehr oder weniger auf selbe raus).

Bei meinen Refcountern nutze ich 64bit. Der Grund ist recht simpel, angenommen, ich würde es darauf anlegen, den überlaufen zu lassen mit "while(++refcount);", effektiv dauert das länger, als der PC halten wird. Nehmen wir an ein increment dauert 1us, also 1/1'000'000s, dan bräuchte es 2**64 / 1000000 / (3562460*60) = ~599'730 Jahre bis zum überlauf. (Wobei, eigentlich nutze ich 4 bits noch für was anderes, also hab ich "nur" 37'483 Jahre bis zum überlauf).

Jetzt ist auf meinem PC ein size_t auch 64bit gross. Und damit meine hashs, die ja effektive indeces in ein Array sind auch. Aber aus dem selben Grund, wie bei den Refcountern, dürfte es ja gar zeitlich nicht möglich sein, die alle auszunutzen, und ein malloc ist ja nochmal um einiges langsamer, als einen Pointer zu inkrementieren.

Ich frage mich jetzt also, rein zeitlich betrachtet, was ist eigentlich die Limite an kleinen Allokationen, die hier überhaupt realistisch sind? Angenommen eine Allokation dauert 25us, und ich mache nichts anderes 1 Jahr lang, dann hätte ich ja (10356246060) * (1000000 / 25) = ~2^43.5 Einträge. Bei 50us ~2^42.5, 100us ~2^41.5. 100us und 1 Jahr wären ~2^38.

Passt das so ungefär? Wie lange braucht malloc heutzutage normalerweise? Und zusammen mit Locking & Hashing usw., wie schnell könnte mein hash map "schlimmstenfalls" überhaupt werden?

Ab 1 Jahr, mit 7.2ms pro Eintag, bräuchte ich höchstens noch 32bit für meinen Hash. Macht es überhaupt Sinn mehr als 2^32 Einträge zu erlaubten, oder nutzt das sowieso nie jemand aus?

Gast #7512924
Lesenswert?

Ich habe mich vor einiger Zeit mit verschiedenen Suchmethoden beschäftig, und einige Antworten gefunden. Siehe hier: Beitrag "Benchmark: C/C++ Effiziente Methoden zum Suchen"

Heutige Prozessoren sind abartig schnell, und im Vergleich dazu ist der DRAM so langsam, dass die Grösse deiner Datenstrukturen die maximale Geschwindigkeit deiner Suche bestimmt. Wenn du also alles in den L1/L2/L3 Cache reinbekommst ist deine Suche um ein Vielfaches schneller. Wenn du zufällig rumhüpfst, und die Daten gross sind, dann ist die Geschwindigkeit deiner Suche durch das RAM limitiert, der Cache ist dann ja praktisch ausgeschaltet. Das gilt natürlich nur, wenn deine Hash Funktion oder deine Vergleichs-Funktion nicht der Bottleneck sind (wie es in der Praxis oft der Fall seiin dürfte). Wenn du eine brauchbare Hash-Map ausprogrammiert hast, dann kann ich eventuell den Benchmark mit deiner Hash-Map noch mal durchlaufen lassen.

Gruss, Udo

(Firma: Torrox.de) #7513436
Lesenswert?

Daniel A. schrieb:

Und damit zur eigentlichen Frage. Wie "schlimm" ist dieser Ansatz tatsächlich? Gibt es da bessere Ansätze?

Du möchtest reference counting strings, die um noch einmal Duplikate zu vermeiden, alle strings im system in einer hash map halten? Und das in C?

Ja, Du kannst eine hash map haben, die die Strings und ihre counter enthält. Die string Typen müssten dann nur noch einen Zeiger auf die Strings selbst enthalten (evtl. als Optimierung den hash). Eine zweite hash map könnte dann Mutexe enthalten. Könnte auch eine sehr triviale hash map sein, die eine fixe Größe hätte und bei der sich der bucket z.B. einfach durch Modulo aus dem string hash ermitteln läßt. Dann brauchst Du keinen mutex für die string hash map und auch keinen mutex für die mutex hash map.

Ich würde das erst einmal vom Interface her designen. Wie sieht das aus der Sicht der Nutzer dieser Strings aus. Dann würde ich das trivialste implementieren, dass dieses Design erfüllt.

Die ersten C++ string Klassen hatten auch reference counting / copy on write implementiert. Die Praxis hat aber später gezeigt, dass sich der Aufwand nicht lohnt. Daher der Ansatz, das Interface zu erst zu gestalten, dann könnte man diese Implementierung auch später noch einführen, wenn Messungen zeigen, dass dort Handlungsbedarf besteht.

Und ja, in C++ ist das sicher 10mal einfacher umzusetzen ;-)

Viel Erfolg!

#7518297
Lesenswert?

Mittlerweile habe ich meine Hashmap schon fast fertig, ein paar Bugs muss ich noch beheben. Ich melde mich dann nochmal, sobald sie voll funktionsfähig ist.

Mein erster Versuch hatte ich erst getestet, als er schon praktisch fertig war, und da hatte ich noch einen ziemlich groben Fehler drin, beim Vergrössern der Hashmap https://github.com/Daniel-Abrecht/dpa-utils/blob/f7c64c1db30488bc8bf175624531e4a3482cacd7/src/bo-unique.c#L110

Ich habe bei Kollisionen eine simple Linked List der Einträge. Diese Sortiere ich nach dem Hash Wert.

Ich habe ein Array von Pointern auf Arrays von Buckets, die Buckets beinhalten einen Pointer auf den ersten Eintrag. Die Arrays von Buckets nenne ich Bucket Listen. Jedes Bucket Liste ist 2^N gross, und die nächste Bucket Liste ist doppelt doppelt do gross wie die vorherige. Ausser bei den ersten 2, die sind gleich gross.

Beim Einfügen der Einträge verwendete ich einfach die hintersten N bits des Hashs. Mittels log2 bekam ich die Bucket Liste heraus, und die restlichen Bits waren einfach der Index, des Buckets in der Bucket Liste.

Beim Vergrössern hatte ich einfach die neue Bucket Liste mit calloc alloziiert, und dann bei allen bestehenden Buckets das n-te Bit bei den Hashs der Einträge geprüft. Sobald ich den ersten gefunden katte, habe ich diesen, mit dem Rest der Linked List der Einträge, einfach in die neue Bucket List in den passenden Bucket umgehängt. Reihenfolge bei beiden Bucket Lists gleich, die hintersten N bits des Hash.

Eventuell realisiert ihr schon, was ich da falsch gemacht hatte. Das Problem war, welche Bits des Hashs sind bei der Sortierung wichtiger? Mit einem simplen > und < Vergleich, natürlich die oberen / linken Bits.Aber der Index meiner Buckets nutzte die unteren Bits...

Ich habe 2 Möglichkeiten, das zu lösen.

  1. Entweder, Ich nehme die Oberen statt die Unteren Bits als Index in die Buckets und Bucket Listen (die Variante habe ich mittlerweile fast fertig, statt log2/__builtin_clzll kann ich __builtin_ctzll nehmen (die 0 bits von rechts statt links zählen>). Nachteil ist, dass die Reihenfolge sich bei den Grössenänderungen zwischen den Bucket Lists verändert, weil von rechts statt links ein Bit zum Index dazu kommt. Die Berechnung ist dadurch auch etwas komplexer. Ich glaube das macht noch recht viel aus.

  2. Die andere Möglichkeit ist, beim Sortieren, bei den Hashs, vor dem vergleich, alle Bits der Hashs umzukehren. Gibt es einen einfachen & schnellen Weg, so einen verkehrten Vergleich durchzuführen? Bits umdrehen ist ja nicht unbedingt eine schnelle Aktion...

Ich muss noch schauen, was in der Praxis tatsächlich schneller ist / ob es überhaupt eine Rolle spielt.

#7518313
Lesenswert?

Torsten R. schrieb:

Du sortierst die Elemente einer verketteten Liste? Wozu?

Die Liste ist von allen Einträgen im gleichen Bucket. Also von den Einträgen, die Kollisionen haben. Beim Einfügen eines Eintrag muss ich sie sowieso vergleichen. Wenn ich es dann gleich sortiert einfüge, muss ich nicht die ganze Liste vergleichen, das spart Zeit. Und aufgrund meiner Vergrösserungsstrategie (nennt man wohl normalerweise "rehashing"), muss ich dabei auch nicht jeden einzelnen Eintrag betrachten.

Gast #7518326
Lesenswert?

Daniel A. schrieb:

Die Liste ist von allen Einträgen im gleichen Bucket. Also von den Einträgen, die Kollisionen haben.

Es macht Sinn, gefundene Einträge ganz vorne in die Liste einzutragen. Wenn öfters nach dem selben Wert gesucht wird, dann findest du ihn schneller. Manchmal wird auch ein binärer Baum anstatt einer Liste verwendet. Damit entschärft man das Problem, dass die ganze Hash Map zu einer sau langsamen Liste entarten kann.

#7518334
Lesenswert?

Udo K. schrieb:

Manchmal wird auch ein binärer Baum anstatt einer Liste verwendet. Damit entschärft man das Problem, dass die ganze Hash Map zu einer sau langsamen Liste entarten kann.

Baum und verlinkte Liste sind beide mies, was Zugriffszeit ins RAM angeht. Die ist nämlich sequentiell sehr viel besser als vogelwild. Dann schon eher B-Baum statt binär.

Gast #7518350
Lesenswert?

(prx) A. K. schrieb:

Baum und verlinkte Liste sind beide mies, was Zugriffszeit ins RAM angeht. Die ist nämlich sequentiell sehr viel besser als vogelwild. Dann schon eher B-Baum statt binär.

Das Problem hat aber praktisch jede Hash Map. Da wird ja auch mehr oder minder zufällig auf die Elemente zugegriffen. Und wenn du öfters auf die selben Elemente zugreifst, sind sie hoffentlich im Cache. Darum auch der Tipp, ein gefundenes Element vorne in der Liste einzutragen. Wenn das Element dann noch einmal gesucht wird, muss man nicht die ganze Liste in den Cache holen. Andererseits soll eine Hashmap ja gar nicht so voll werden soll, das das eine Rolle spielt. Im Benchmark Link weiter oben von mir sieht man den Effekt recht schön. Bei heutigen Cache Grössen ist das aber nur für richtig aufwendige Simulationen und Serveranwendungen wichtig. Wer hat schon eine Hashmap mit > 100000 Elementen?

#7518355
Lesenswert?

Udo K. schrieb:

Das Problem hat aber praktisch jede Hash Map. Da wird ja auch mehr oder minder zufällig auf die Elemente zugegriffen.

Der erste Zugriff geht nicht anders. Wenn dann schon Ende ist, man seinen Eintrag gefunden hat, ist alles OK. Sitzt dort aber der Anfang einer Liste, dann kann sinnvoll sein, darüber nachzudenken, wie man ab dieser Stelle mindestens auf kurz sequentiell weiter macht, statt gleich Pointer hinter Pointer zu setzen.

Allerdings habe ich das Gefühl, dass hier das Rad neu erfunden werden soll. Darüber sollten eigentlich längst Horden von Informatikern nachgedacht und es experimentell untersucht haben. Aber bitte nicht Knuth - der schrieb in einer Zeit uniformen RAMs, während heute eher gilt, dass der L1-Cache das ist was damals RAM war, und das heutige DRAM mehr seinen Bändern ähnelt.

Andererseits soll eine Hashmap ja gar nicht so voll werden soll, das das eine Rolle spielt.

Yep. Ich hatte mich auch nur zur Idee von Bäumen geäussert. Nicht zu einer Hash-Tabelle mit wenig Konflikten.

#7518572
Lesenswert?

Mittlerweile funktioniert meine hash map. Ein Beispiel, wie sie verwendet wird, ist hier: https://github.com/Daniel-Abrecht/dpa-utils/blob/master/src/main/intern-example.c
Und hier: https://github.com/Daniel-Abrecht/dpa-utils/blob/master/src/main/bo-example.c

Die Funktion dpa_u_bo_intern speichert die Einträge in der Hash Map.
Naja, eigentlich speichert die Funktion dpa__u_bo_do_intern einen Eintrag im Hash Map, dpa_u_bo_intern tut das nur, wenn das Wort grösser als das Buffer Object ist, bei 64bit Systemen normalerweise ab >15 Bytes Länge.

Von den 234937 Wörtern in /usr/share/dict/words, die ich im Beispiel oben verende, sind das gerade mal 6803.

An der Library habe ich noch einiges zu tun, bis sie fertig ist. Aber den Hash Map teil könnt ihr schon mal ausprobieren, wenn ihr wollt.

Der Hash ist momentan noch ein lahmer FNV-1a Hash. Und die Schwellwerte, wann die Grösse des Hashmap verdoppelt / halbiert wird, ist vermutlich noch zu hoch (https://github.com/Daniel-Abrecht/dpa-utils/blob/master/src/bo-unique.c#L16-L17). Aber das kann ich später noch optimieren.

Im Branch "hm-test" habe ich noch versucht die unteren statt die oberen Bits vom Hash zu nehmen, um zu sehen, ob das schneller ist. In dem Branch ist noch irgendwo ein Bug, aber es scheint sowieso nicht nennenswert schneller zu sein. Vielleicht schau ich mir das später nochmal an.

#7518605
Lesenswert?

Daniel A. schrieb:

Bei C ist das durchaus üblich. So ziemlich jedes grosse Projekt implementiert seine eigenen Memory Management Primitive und Datenstrukturen, mit seinen eigenen Versionen von allerhand üblichen Sachen.

Es gibt heute noch große Projekte die C verwenden, irgendwann gemerkt haben, dass ihr Problem nicht zur Programmiersprache passt und dann lieber das Rad neu erfinden, anstatt die getroffene Entscheidung zur Programmiersprache zu korrigieren? Erstaunlich.

Gast #7519131
Lesenswert?

Ich habe mal versucht, den Code mit MS cl zu übersetzen. Das hat leider nicht funktioniert... clang-cl liefert auch seitenweise Fehlermeldungen...

Kompatibilität ist eigentlich das wichtigste Argument für "C". Die Präprozessor Orgien und gcc Spezialitäten sind doch für eine einfache Hash Tabelle nicht nötig? Und wenn ich schon am Jammern bin: Ein paar Leerzeichen, Zeilenumbrüche und lesbare Funktionsnahmen würden sicher nicht schaden.

Gruss, Udo

#7519161
Lesenswert?

Nun hab ich den Fehler im Branch "hm-test" gefunden, war nur ein off-by-one Fehler.
Ich hab dann das "intern-example" von oben mit beiden Varianten nochmal laufen lassen, mit etwas mehr Einträgen als sonst (4096000 davon, mit "pwgen -1 16 4096000" generiert). Die Variante im "hm-test" Branch war doch etwas schneller. Normalerweise um die 2.4s statt 2.8s. Ich hab das jetzt in den master branch gemerged, und die alte Version in den "hm-test2" branch getan. Diff ist relativ klein: https://github.com/Daniel-Abrecht/dpa-utils/commit/22b08b576a3e09e82aa6982acde6649917fc3be1

Udo K. schrieb:

Kompatibilität ist eigentlich das wichtigste Argument für "C". Die Präprozessor Orgien und gcc Spezialitäten sind doch für eine einfache Hash Tabelle nicht nötig?

Es verwendet ein paar Attribute:

  • attribute((always_inline)) - Weil es teil simple Funktionen die nur ein Feld zurückgaben nicht geinlined hat. Ok, das könnte ich noch weg lassen.
  • attribute((packed)) - Ohne das ist das Layout meiner Buffer Objekte nicht umsetzbar. Da gibt es keinen weg darum rum.
  • attribute((visibility("default"))) - Weile es eine library ist, und nur die Symbole exportiert werden sollen.
  • attribute((weak)) - Brauch ich für optionale setup / teardown Funktion für ein simples test utility.

Davon abgesehen ist es alles komplett Standard konformes c17. Lässt sich also mit jedem vernünftigen, c17 konformen, Compiler kompilieren. Ich hab es mit gcc und mit clang getestet. Ich kompilere das Ding bei mir mit "--std=c17 -Wall -Wextra -pedantic -Werror -Wno-missing-field-initializers -Wno-missing-braces".

Zu diesem "cl" habe ich nur das hier gefunden: https://clang.llvm.org/docs/MSVCCompatibility.html

Falls "cl" den MS Compiler meint, MSVC implementiert C nicht vollständig, das kann man vergessen. Und falls du versuchst, die Header mit C++ zu kompilieren, das kann man auch vergessen. Das ist eine C Library, keine C++ Library, und das soll es auch nicht werden.
Die Macros und Generics sind teil eines Konzepts, mit der man verschiedene Arten von Buffer Objekten an Funktionen übergeben kann, und mit der Selben API auf alle zugreifen kann. Dafür muss ich an die grenzen dessen gehen, was in C möglich ist.

Auf Windows wird es auch nicht laufen. Ich glaube, dort implementiert kein Compiler die C17 Thread Library. Ausserdem brauche ich die getrandom() Funktion meiner libc (die wederum /dev/urandom ausliest), um die Hash Funktion richtig zufällig zu initialisieren (um gegen vorausberechnete hash collisions zu schützen). Windows Support ist für mich aber auch keine Priorität, weil proprietär.

Wichtig ist für mich hauptsächlich Linux. Und vermutlich geht die Library auch auf anderen unixoiden Systemen. Mehr interessiert mich ehrlich gesagt eh nicht.

#7520318
Lesenswert?

Ich habe jetzt noch eingebaut, dass wenn man es mit -DDPA_U_NO_THREADS baut, alles was mit Multithreading zu tun hat ausgeschaltet wird. Das ist nützlich für Plattformen & Programme, die das sowieso nicht unterstützen.
Ohne das locking usw. braucht es für die 4096000 Einträge nur 2.0s statt 2.4s.

#7523764
Lesenswert?

Ich hatte einen Fehler in dem Macro mit dem ich das attribute((always_inline)) gesetzt habe. Statt es nur bei gcc und clang zu setzen, hatte ich es versehentlich nur gesetzt, wenn es nicht gcc oder clang ist. Mit dem korrigiert braucht das "intern-example" Program bei meinem PC für 4096000 Einträge nur noch 2.1s statt 2.4s. Und mit -DDPA_U_NO_THREADS gesetzt sind es 1.7s statt 2.0s.

#7523810
Lesenswert?

Rein interessehalber: kannst du das mal auf deiner Hardware gegen eine C++/STL-Version testen?

der Einzeiler

1
std::set<std::string> testset(std::istream_iterator<std::string>(std::cin),std::istream_iterator<std::string>());

kommt bei mir mit dem 67MB "pwgen"-Testfile auf 6,77 Sekunden, davon entfallen laut Profiler 21,7% (≈1.5 Sekunden) auf die set, der Rest wird im std::istream vertrödelt.

Angehängte Dateien:
#7523959
Lesenswert?

Εrnst B. schrieb:

Rein interessehalber: kannst du das mal auf deiner Hardware gegen eine C++/STL-Version testen?

Ergebnisse sind im Anhang. Getestet unter Devuan Linux (vergleichbar mit Debian).

Bei mir kommen mit gcc auch etwa 6s heraus. Mit clang sogar etwa 7s (dort musste ich noch ein paar () einfügen, damit clang den Code akzeptiert hat).
In test2.cc habe ich noch eine Version die näher an dem ist, was das "intern-example" Programm macht. Das kopiert den string jeweils 1mal unnötig, ist aber trotzdem schneller (5.4s)!
Ich vermute, dass da noch irgendwo etwas kopiert wird, und std::istream_iteratorstd::string(std::cin) noch irgendwo sonstige ineffizienzen hat.

Hat mich ehrlich gesagt überrascht, ich hätte nicht erwartet, dass mein Programm schneller ist. War auch nie meine Absicht. Aber das muss auch nicht unbedingt viel heissen.

Könnte zwar alles mögliche sein. Vielleicht sind einfach die Eingangsdaten zufällig für mein Set besser geeignet. Oder vielleicht ist das Vergrössern / Rehashing, in der STL ineffizienter (falls es das macht). Mein Set fängt auch mit 512 Buckets an, da es nur 1 globales gibt, vielleicht ist die STL dafür bei kleineren Sets effizienter?

Und der Test ist ja auch nicht unbedingt besonders realistisch, normalerweise fügt man ja nicht nur Strings ein, die es noch nicht im set gibt.

Angehängte Dateien:
#7523978
Lesenswert?

Daniel A. schrieb:

Ich vermute, dass da noch irgendwo etwas kopiert wird, und std::istream_iteratorstd::string(std::cin) noch irgendwo sonstige ineffizienzen hat.

ja, das scheint die Eingabe recht unglücklich zu verarbeiten, laut valgrind/callgrind byteweise mit sehr vielen getc / ungetc aus der libc.

Daniel A. schrieb:

Könnte zwar alles mögliche sein.

die std::set benutzt kein hashing, sondern einen Binärbaum. Insofern ist das eh ein Äpfel/Birnen-Vergleich, auch wenn man am Ende die Datenstruktur für ähnliches verwenden kann.

die meiste Zeit verbringt die Set mit (AVX2-Optimierten) String-Vergleichen, etwas malloc/free usw. das Tree-Rebalance fällt mit 1% nicht weiter ins Gewicht, was aber an der Zufalls-Verteilung der Eingabedaten liegen kann.

Interessanterweise ist das Set-Test-Programm jedoch mit einem sortierten Eingabefile schneller als mit dem unsortierten. Hätte das eher andersherum erwartet.

1
> sort inputfile > inputfile.sorted
2
> time ./test  < inputfile.sorted 
3

4
SET Size 4096000
5

6
real    0m2,576s
7
user    0m2,319s
8
sys     0m0,256s
Angehängte Dateien:
#7524066
Lesenswert?

Ich habe jetzt auch mal noch so einen call graph erstellt. Das ist doch recht nützlich, um zu sehen, wo es am längsten braucht.

Wenn ich im "intern-example" Programm den Teil nach dem einfügen der Einträge auskommentiere (also das Validieren des Set und das wieder Entfernen aus dem Set jedes einzelnen Eintrags), kommt mein Programm auch auf 1.2s.

Angehängte Dateien:
#7526947
Lesenswert?

Jetzt kompiliert es sogar auch mit avr-gcc! (Bei älteren Versionen muss man im makefile -std=c17 statt -std=c11 setzen, und dass -Werror raus nehmen, aber dann geht es mit CFLAGS="-DDPA_U_CONFIG='\"$PWD/example-config/avr-gcc.h\"'" make CC=avr-gcc notest=1 clean all).

Ist zwar nicht wirklich sinnvoll auf Chips mit so wenig ram. War aber gut um noch ein paar Platformabhängige Annahmen zu finden und auszumerzen.

Gast #7527315
Lesenswert?

Daniel A. schrieb:

Jetzt kompiliert es sogar auch mit avr-gcc!

Der Code ist nicht Standard-C konform. Siehe etwa hier: https://stackoverflow.com/questions/60772586/sizeof-of-packed-structure-with-bit-fields.

Hier ein Beispiel, wie man es konform machen kann (wobei es meiner Meinung nach gescheiter ist ganz auf Bitfields zu verzichten): https://stackoverflow.com/questions/71227275/merge-bit-fields-across-anonymous-structs-in-c

Ich habe schon einiges an Code gesehen, aber deine Makro und Typedef Orgien in Kombination mit unlesbaren Funktionsnahmen und ohne Kommentare schlagen so ziemlich alles auf der Negativskala... inklusive der C++ STL Lib - ok die STL liegt noch knapp vorne.

Aus deinem Source Code werde ich also nicht schlau, das kann aber auch an mir liegen. Abgesehen davon bin ich gerne bereit anzunehmen, dass der Code genial ist... Aber wie bitte verwende ich die Hash Map in der Praxis?

Annahme: Ich habe ein Feld mit User-Strukturen MyData. Die will ich in die Hash-Map einfügen, mehrmals suchen, und später wieder rauslöschen. Der Key soll etwa ein Integer sein. Kannst du da ein funktionierendes Beipiel posten?

Hier meine Vorstellung:

1
#define N 1000
2

3
static struct MyData
4
{
5
    char m_data[32];   // Private Daten - gehen keinen was an.
6
    int  m_key;        // Der Key zum Einfügen und Suchen in die Hash-Map.
7
} MyDataArray[N];
8

9
int main()
10
{
11
    int i;
12
    HashMap MyMap;
13

14
    HashMapInit(&MyMap, offsetof(struct MyData, m_key));
15

16
    for (i = 0; i < N; i++)
17
         HashMapInsert(&MyMap, &MyDataArray[i]);
18

19
    for (i = 0; i < 1000*N; i++)
20
         assert(HashMapFind(&MyMap, &MyDataArray[i % N]) != NULL);
21

22
    for (i = 0; i < N; i++)
23
         HashMapRemove(&MyMap, &MyDataArray[i]);
24
}
#7527464
Lesenswert?

Udo K. schrieb:

Daniel A. schrieb:

Jetzt kompiliert es sogar auch mit avr-gcc!

Der Code ist nicht Standard-C konform. Siehe etwa hier: https://stackoverflow.com/questions/60772586/sizeof-of-packed-structure-with-bit-fields.

Wie ich bereits sagte:

Daniel A. schrieb:

Es verwendet ein paar Attribute:

  • attribute((always_inline)) - Weil es teil simple Funktionen die nur ein Feld zurückgaben nicht geinlined hat. Ok, das könnte ich noch weg lassen.
  • attribute((packed)) - Ohne das ist das Layout meiner Buffer Objekte nicht umsetzbar. Da gibt es keinen weg darum rum.
  • attribute((visibility("default"))) - Weile es eine library ist, und nur die Symbole exportiert werden sollen.
  • attribute((weak)) - Brauch ich für optionale setup / teardown Funktion für ein simples test utility.

Davon abgesehen ist es alles komplett Standard konformes c17.

Dass das alles vom Layout und Grösse her passt stelle ich mit einigen static_assert() sicher.

Udo K. schrieb:

Hier ein Beispiel, wie man es konform machen kann (wobei es meiner Meinung nach gescheiter ist ganz auf Bitfields zu verzichten): https://stackoverflow.com/questions/71227275/merge-bit-fields-across-anonymous-structs-in-c

Das ist ein guter Weg, sich selbst in den Fuss zu schiessen. Sobald man da auf e zugreift, sind die Einträge die von reserved geschützt hätten werden sollen eventuell trotzdem futsch. (Zudem ist die Reihenfolge der Bitfelder ja implementation defined).

Zugegeben, das attribute((packed)) in meinen buffer typen ist etwas unschön. Aber eine bessere Lösung hab ich momentan einfach nicht. Die 3 Haupt buffer struct typen sind absichtlich so gestaltet, dass es so gross wie ein size_t + ein pointer ist (und die sind meistens gleich gross). Das macht es recht Effizient, viele davon zu haben, oder sie an Funktionen als Argument zu übergeben. Im Grunde sind es eine Art fat pointer mit ein par Bits für die typ info. Darum mache ich meinen size_t typ in den meisten buffern 1 byte kleiner. Nur beim dpa_u_inline_bo_t Typ packe ich die Grösse ins erste Byte, weil es da halt noch Platz hat, und ich dann ein byte mehr darin speichern kann. Und die Bitfelder sind da, damit man das alles schön mit designated initializern initialisieren kann.
Vielleicht versuche ich das später nochmal zu überarbeiten, aber im moment bekomme ich das nicht besser hin.

Udo K. schrieb:

Aus deinem Source Code werde ich also nicht schlau, das kann aber auch an mir liegen. Abgesehen davon bin ich gerne bereit anzunehmen, dass der Code genial ist... Aber wie bitte verwende ich die Hash Map in der Praxis?

Das HashMap ist eigentlich nur ein Implementationsdetail des dpa_u_bo_unique_hashmap_t und des dpa_u_bo_unique_t typs, den man von dpa_u_bo_intern(bo) bekommt. Es ist nicht als general purpose HashMap gedacht.

Es geht da nur darum, dass das vergleichen von 2 dpa_u_bo_unique_t typen, und das Abfragen des Hash, in O(1) möglich ist.

(Das erstmalige Hashen beim Einfügen von längeren Strings in die HashMap, damit man den eindeutigen dpa_u_bo_unique_t bekommt, ist natürlich trotzdem O(n), aber die ganzen buffer typen sind speziell dafür da, unnötiges Kopieren, Hashen, oder suchen im HashMap, wann immer möglich zu vermeiden).

1
#include <dpa/utils/bo.h>
2

3
int main(){
4
  dpa_u_bo_unique_t a = dpa_u_bo_intern(((dpa_u_bo_simple_ro_t){
5
    .type = DPA_U_BO_SIMPLE,
6
    .size = sizeof("The quick brown fox jumps over the lazy dog.")-1,
7
    .data = "The quick brown fox jumps over the lazy dog.",
8
  }));
9
  dpa_u_bo_unique_t b = dpa_u_bo_intern(((dpa_u_bo_simple_ro_t){
10
    .type = DPA_U_BO_SIMPLE,
11
    .size = sizeof("The quick brown fox jumps over the lazy dog.")-1,
12
    .data = "The quick brown fox jumps over the lazy dog.",
13
  }));
14
  dpa_u_bo_unique_t c = dpa_u_bo_intern(((dpa_u_bo_simple_ro_t){
15
    .type = DPA_U_BO_SIMPLE,
16
    .size = sizeof("The quick brown fox jumps over the lazy dog!")-1,
17
    .data = "The quick brown fox jumps over the lazy dog!",
18
  }));
19
  assert(a.bo_unique_hashmap == b.bo_unique_hashmap);
20
  assert(a.bo_unique_hashmap != c.bo_unique_hashmap);
21
  puts(dpa_u_bo_compare(a, b) == 0 ? "a == b" : "a != b");
22
  puts(dpa_u_bo_compare(a, c) == 0 ? "a == c" : "a != c");
23
  dpa_u_bo_put(a);
24
  dpa_u_bo_put(b); // Removes a and b from the hash map, no references left
25
  dpa_u_bo_put(c); // Removes c
26
}
Gast #7527622
Lesenswert?

Daniel A. schrieb:

Udo K. schrieb:

Daniel A. schrieb:

Zugegeben, das attribute((packed)) in meinen buffer typen ist etwas unschön. Aber eine bessere Lösung hab ich momentan einfach nicht.

Das ist nich nur unschön, sondern nicht definiertes Verhalten. Es ist nicht garantiert, das Bitfelder, die grösser als der zugrundeliegende Integer Typ sind, zusammengefasst werden. Dein Code setzt sogar voraus, das die Bitfelder in angrenzenden structs zusammengefasst werden. Dazu trickst du wild mit packed herum, und hoffst, dass der Compiler das schon richten wird. Der gcc unter Cygwin z.B. macht es z.B. nicht, auch nicht der Windows clang oder Microsoft cl (der nebenbei bemerkt C17 und C++20 konform ist). Der Code wird wahrscheinlich nicht mal auf älteren Prozessoren laufen, die davon ausgehen, dass ein size_t 4 oder 8 Byte aligned im Speicher liegt. Schaue dir doch mal an wie es die Hash-Maps in den letzen 20 Jahren gelöst haben. Abgesehen davon kosten die Bitfelder Laufzeit und bringen nur eine marginale Ersparnis an Speicher, der oft vom grösseren Code aufgefressen wird. Ältere Compiler optimieren Bitfelder relativ schlecht.

Das HashMap ist eigentlich nur ein Implementationsdetail des dpa_u_bo_unique_hashmap_t und des dpa_u_bo_unique_t typs, den man von dpa_u_bo_intern(bo) bekommt. Es ist nicht als general purpose HashMap gedacht.

Wozu ist das Ding dann da? Irgendwie steige ich da langsam aus... Kann ich damit mein einfaches Problem von weiter oben mit dem struct MyData lösen, und deine Lib als Ersatz für std::unordered_map verwenden oder geht das nicht?

Gruss, Udo

#7527648
Lesenswert?

Udo K. schrieb:

Dazu trickst du wild mit packed herum, und hoffst, dass der Compiler das schon richten wird.

Ich hoffe nicht nur, ich prüfe es mit static_assert nach.

Udo K. schrieb:

Prozessoren laufen, die davon ausgehen, dass ein size_t 4 oder 8 Byte aligned im Speicher liegt.

Nur einige der inneren Structs sind packed, und es gibt keine Referenzen zu den packed Feldern. Und es sind auch nicht alle Felder in einem packed struct. Deshalb ist das kein Problem, und wegen dem Alignment des Haupt struct kann der Compiler trotzdem annahmen zum alignment machen, selbst bei den enthaltenen packed structs.

Schaue dir doch mal an wie es die Hash-Maps in den letzen 20 Jahren gelöst haben.

Wie schon mehrfach gesagt, das HashMap ist ein Implementations Detail, um diese buffer typen zu implementieren, und nicht umgekehrt. Dem HashMap code selbst ist das layout der Structs sogar egal.

Abgesehen davon kosten die Bitfelder Laufzeit und bringen nur eine marginale Ersparnis an Speicher,

In der Praxis macht das hier nicht viel aus. Ausserdem kann der Compiler das oft ganz wegoptimieren, da das struct oft per value übergeben wird, dessen Wert oft schon zur Compile zeit bekannt ist, und er bei einigen typen überhaupt nicht abgefragt wird. Wobei, gcc kommt damit nicht so gut klar wie clang, und macht beim dpa_u_inline_bo_t ein paar sinnlose moves: https://godbolt.org/z/bqcaejEzf Aber in der Praxis macht es nicht viel aus, da haben adere Faktoren grösseren Einfluss. (Ironischerweise sehen die movs von GCC sehr nach einem missglückten Versuch der Optimierung mit AVX aus. Auf dem Stack alignt der gcc unter amd64 die Structs normalerweise auf 16bytes, und die passen mit 16bytes auch ins 128bit grosse xmm0 register). Wobei, der Code von GCC ist trotzdem nur 2 Instruktionen länger, wenn man sich den Inhalt von main() ansieht.

der oft vom grösseren Code aufgefressen wird. Ältere Compiler optimieren Bitfelder relativ schlecht.

Compiler, die derart alt sind, unterstützen meist auch kein C11 und _Generic, und Kompilieren es sowieso erst gar nicht.

(Firma: Torrox.de) #7527655
Lesenswert?

Udo K. schrieb:

Der Code wird wahrscheinlich nicht mal auf älteren Prozessoren laufen, die davon ausgehen, dass ein size_t 4 oder 8 Byte aligned im Speicher liegt.

Wenn die Compiler Möglichkeiten haben, Strukturen ohne padding zu erzeugen, dann erzeugen die für solche Strukturen auch Code, der nicht vom natürlichen Alignment ausgeht. Das ist dann entsprechend größerer und langsamerer Code.

#7527696
Lesenswert?

Ich habe mir andere Implementierungen doch schon längst angesehen, und bin mit den Ergebnissen bisher mehr als zufrieden. Es tut was es soll, es ist doch recht schnell, und auch mit dem Platzverbrauch bin ich ganz zufrieden.
Wollt ihr einfach nicht wahr haben, das man auch heute noch gute Programme schreiben kann?

Gast #7527720
Lesenswert?

Daniel A. schrieb:

Ich habe mir andere Implementierungen doch schon längst angesehen, und bin mit den Ergebnissen bisher mehr als zufrieden. Es tut was es soll, es ist doch recht schnell, und auch mit dem Platzverbrauch bin ich ganz zufrieden. Wollt ihr einfach nicht wahr haben, das man auch heute noch gute Programme schreiben kann?

Ok, mit den Innereien kann ich leben, wenn ich das irgendwie übersetzt bekomme. Geht nur leider nicht. Solchen empfindlichen Code kenne ich nur von C++ Projekten. Und wie kann ich die Hash Map verwenden, um Datenblöcke wie in meinem Beispiel oben in einer Schleife einzufügen und zu suchen? Das muss ja wohl irgendwie einfach gehen?

#7527781
Lesenswert?

Udo K. schrieb:

Und wie kann ich die Hash Map verwenden, um Datenblöcke wie in meinem Beispiel oben in einer Schleife einzufügen und zu suchen? Das muss ja wohl irgendwie einfach gehen?

Man kann das hashmap in meiner utility library mit 2 minimalen anpassungen schon dafür missbrauchen, aber vorgesehen ist es nicht. Im "struct dpa__u_bo_unique_hashmap_entry" brauchte man einen Pointer auf die assoziierten Daten. Und dpa__u_bo_do_intern ist eine suche-und-füge-ein-falls nicht-vorhanden funktion, da müsste man den Suchen teil (hier markiert https://github.com/Daniel-Abrecht/dpa-utils/blob/0eec4fd282e2494fc86611ed864a39264dc06c2f/src/bo-unique.c#L305-L333) erst in eine andere Funktion auslagern. Der Insert Funktion könnte man dann den Key übergeben, und im zurückgegebenen struct dpa__u_bo_unique_hashmap_entry* könnte man den Pointer auf seine Daten setzen. Aber eben, vorgesehen ist das so nicht.

Vorgesehen sind da komplett andere Anwendungszwecke:
Man hat einen String oder sonstige Daten als key in einem buffer objekt, und ruft dpa__u_bo_do_intern auf. Dann bekommt man das eindeutige dpa_u_bo_unique_t Objekt zurück. Danach geht den Hash abzufragen, sowie 2 dpa_u_bo_unique_t zu vergleichen, extrem Schnell, in O(1). Ist also ideal geeignet als Key in einem weiteren HashMap, dort müsste dann kein Hash mehr berechnet werden, und keine langen Strings mehr verglichen werden.
Und so ein dpa_u_bo_unique_t ist auch gut geeignet, wenn man sich die Namen von Tags und Attributen einer XML Struktur oder so speichern will. Die kurzen Namen passen meistens direkt in die dpa_u_bo_unique_t. Die langen kommen halt ins globale HashMap, und verbrauchen nur 1mal Speicher.

Das ist, wofür es eigentlich gedacht ist. Eventuell füge ich auch irgendwann mal noch ein general purpose HashMap zu meiner Library hinzu, aber da würde ich sicher einiges anderes machen, weil man da andere Anforderungen an sowas hätte.

#7527905
Lesenswert?

Klaus schrieb:

Ich muss nochmal darauf zurückkommen. Hast du da ein Beispiel für ein größeres Projekt in C?

z.B. Apache Projekte, siehe Apache2.

Anderes Beispiel, Mesa. Die haben ihr eigenes hash map hier: https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/util/hash_table.c?ref_type=heads (Und auch einige andere hübsche Sachen sind dort im util Folder zu finden).

Die GTK programme, aber auch anderes gnome zeug wie gstreamer, haben ihre gobjects fürs memory managemant.

Es gibt sicher noch weitere Projekte mit solchen Sachen, die hier sind mir nur spontan eingefallen.

#7554276
Lesenswert?

Ich habe testweise das ganze trotz allem mal mit MSVC Kompiliert, in der WSL. Geht jetzt einfach mit make use=msvc clean all.
Das Inkludiert das Makefile unter mk/msvc.mk, in dem ein paar config Optionen gesetzt sind, und CC auf script/msvc-cc setzt, ein Wrapper Script von mir, das die Optionen für CC übersetzt und dann cl.exe aufruft.

Momentan setze ich in der msvc.mk "noshared=1", damit stelle ich das bauen einer dll in meinem Makefile ab. Es baut & nutzt nur die static library (das funktioniert).
Ohne die Option baut mein Makefile eine DLL von der static library, aber ich kriege die EXEs nicht dazu die Variablen aus der DLL zu nehmen. Mit ein paar dllimport kriege ich die EXEs dann zwar auch mit DLL gebaut, aber dann wollen sie nicht starten. Keine Ahnung, warum.
Ein anderes Problem sind die dllexport / dllimport statements selbst. Mit den dllexport Statements bekommt die exe, wenn man mit der statischen library linkt, auch ein .lib file, weil es Symbole exportiert, und dllimport .geht nur, wenn ein Symbol tatsächlich in einer DLL drin ist. Damit kann man also nicht die gleichen .obj Dateien für static und shared library verwenden. Eventuell könnte man da was mit .def Dateien statt dllimport/dllexport in den .c Files machen, aber das ist mir zu kompliziert / aufwändig.

Sachen die ich für MSVC anpassen musste waren unter anderem, 2 VM Typen, die ich ersetzen musste, und die Position 2er Funktionsdeklarationen, die msvc nicht Passten. Ein Makro switch, um die Buffer Structs ohne Packing oder Bitfields auszulegen (sind dann 24 statt 16 Bytes gross), und um MT / Atomic Sachen zu deaktivieren, hatte ich eh schon eingebaut, und da auch gebraucht.
Und mein Testframework habe ich für MSVC auch deaktiviert, weil das funktioniert mit file descriptor inheritance unter Testprogrammen um die Ergebnisse zurück zu melden, und das hat man unter Windows nicht.
Und das dpa_u_init Macro habe ich in MSVC noch nicht implementiert, das Initialisieren des Zufallsseed für das Hash Map geht darum dort noch nicht.

Das Struct Packing scheint keinen grossen Einfluss auf die Geschwindigkeit zu haben, zumindest nicht in meinen 2 Beispielprogrammen. Deaktiviert man es, sind die buffer Objekte halt grösser / verschwenden mehr platz. Dafür passen in den dpa_u_bo_inline_t Type aber mehr Daten rein. Daher musste ich beim Testen längere Datensätze nehmen, ich habe die mit "pwgen -1 24 4096000" erzeugt.

Das "intern-example" Programm, kompiliert mit MSVC cl, braucht dabei etwa 8s. Was wäre etwa 3x langsamer als unter linux mit gcc / llvm. Ich weiss aber nicht, wie viel davon effektiv die runtime war, und wie lange es einfach dauerte, bis Windows die exe gestartet hatte, vermutlich wäre es eigentlich nicht ganz so extrem. Meine utility library setzt aber extrem auf kleine Hilfsfunktionen und inlining, ich vertraue da voll drauf, dass der Optimizer alles weg optimiert, und ich glaube nicht, dass MSVC das besonders gut hinbekommt. Die inline Funktionen scheint es auch in jedes Objektfile nochmal rein zu packen, statt nur in denen objects mit den exports derselben, das ist dabei sicher auch nicht hilfreich.

Was mir noch aufgefallen ist, es gibt eine stdatomic.h, aber msvc cl scheint die nicht kompilieren zu können, weil es die _Atomic() Statements darin nicht kann, und setzt auch die Macros, um das anzuzeigen. Ich frag mich, wofür es die Datei überhaupt hat.

Insgesamt ist sicher nicht alles ideal, aber als PoC, dass es selbst mit dem MSVC cl gehen würde, reicht es erst mal.

Ich habe auch mal mit der Dokumentation angefangen: https://daniel-abrecht.github.io/dpa-utils/bo.html
Aber sowohl die Doku, als auch die Implementation, sind noch lange nicht vollständig. Und dazu, Testcases zu schreiben, bin ich auch noch nicht gekommen. Da werde ich noch länger dran sein.

Gast #7554749
Lesenswert?

Super, freut mich, dass du deinen Sourcecode mit MSVC compiliert bekommen hast :-)

Wenn es dich interessiert, wo die Zeit verbraten wird, dann kannst du mit Profiling compilieren. Starte dazu den MSVC Kommandoprompt, wo die Umgebungsvariablen PATH, INLUDE und LIB richtig gesetzt sind:

1
Compilieren geht mit:
2
cl -nologo -I<dir> -DDEBUG=0 -W3 -O2 -GL -c test1.cpp test2.cpp
3

4
und Linken mit:
5
cl -nologo -O2 -FeTest.exe test1.obj test2.obj myprivate.lib -link ltcg:pgi 
6

7
Dann lässt du Test.exe laufen, und es wird ein File mit der Info erzeugt, welche Funktionen wie oft aufgerufen werden.
8

9
pgomerge.exe -merge *.pgd 
10

11
und
12

13
pgomgr.exe -summary *.pgd
14

15
erzeugen dann ein lesbares Textfile mit der relevanten Info, wo die Zeit verbraten wird.

Wenn Dein Code 3x langsamer unter Windows läuft, zeigt das ein Problem auf. So ein Hash Map Test muss OS unabhängig sein - du willst da DEINE Routinen testen...

Wenn du Zeit hast, dann implementiere bitte mein Minimalbeispiel von weiter oben. Damit wird dein Hashcode für viele Anwendungen sinnvoll einsetzbar.

Gruss, Udo

Gast #7554765
Lesenswert?

Daniel A. schrieb:

Meine utility library setzt aber extrem auf kleine Hilfsfunktionen und inlining, ich vertraue da voll drauf, dass der Optimizer alles weg optimiert, und ich glaube nicht, dass MSVC das besonders gut hinbekommt.

Dann schau dir doch die Linker map an. Da siehst du schnell welche Funktionen in deinem Program drinnen stehen (cl.exe ... -link -map)

Die inline Funktionen scheint es auch in jedes Objektfile nochmal rein zu packen, statt nur in denen objects mit den exports derselben, das ist dabei sicher auch nicht hilfreich.

Die stehen in den Objects drinnen, damit der Linker mehrere gleiche Funktionen zusammenfassen kann. Wenn sie nicht gebraucht werden fallen sie später raus. "inline" ist mit einem modernen MSVC Compiler völlig unnötig, der macht das automatisch beim Linken wo der gesamte Code vorliegt mit LTCG.

#7637488
Lesenswert?

Ich bin gerade dabei, noch ein HashMap und ein HashSet zu implementieren.
Also diesmal kein globales für String Interning, sondern ein klassisches. Man kann dann da Nummern, die geinternten Strings, oder Pointer hinzufügen, entfernen, Checken ob sie drin sind, und darüber iterieren, das übliche eben.

Ich habe vor, Robin Hood Hashing zu verwenden: https://programming.guide/robin-hood-hashing.html

Ich verdopple / halbiere den Speicher von dem Set / Map wieder je nach bedarf. Ich nehme die oberen paar Bits des Hash zum einfügen / Sortieren der Einträge.

Die Idee dahinter ist, unter anderem, dass dadurch beim vergrössern / verkleinern, die Signifikanten Bits sich nicht ändern, und deshalb die Reihenfolge, wann auch nicht die Position, der Einträge, gleich bleibt.

Beim Vergrössern und Verkleinern hänge ich nun etwas fest. Ich würde es eigentlich gerne inplace machen, nach/vor einem realloc, und ich würde idealerweise jeden Eintrag gerne nur einmal umsortieren. Nur leider ist mir dafür noch keine gute Methode eingefallen. Eines der Schwierigkeiten ist, dass wenn ein paar Einträge auf den letzten Eintrag Hashen, dann wird der Anfang der Liste nach vorne geschoben. Es erinnert fast ein bisschen an einen Ring Buffer. Es gibt also nicht wirklich einen Anfang / Ende. Bei meinen Überlegungen laufe ich da dann immer rein, was, wenn wegen dem vorherigen Eintrag ich den etwas weiter hinten platzieren müsste? Einen Ort zum Anfangen hab ich ja nicht, das könnte an jeder Position passieren...

Wie würdet ihr das lösen?

Gast #7637501
Lesenswert?

Baue die Hash Map komplett neu auf, trage jeden Einträge aus der alten Hash Map in die neue Hash Map ein. Die Kollisionen sind ja auch von der Grösse der Hash Map abhängig. Der Overhead für den Neuaufbau ist meist klein. Aufpassen, dass du keine ungünstige Reihenfolge nimmst. Sonst kann die neue Hash Map zu einer Liste entarten (weiss jetzt nicht genau wie das geht, aber es gibt da böse Spezialfälle).

#7637583
Lesenswert?

Udo K. schrieb:

Baue die Hash Map komplett neu auf, trage jeden Einträge aus der alten Hash Map in die neue Hash Map ein.

Ok, das ist vermutlich der bessere weg. Löst aber mein Problem auch nicht so ganz. Aber Ich denke ich bin nun dahinter gekommen, wie ich das am besten mache, zumindest beim vergrössern.

Ich muss nur einen Eintrag mit PSL (Probe Sequence Length) 0 finden. So einen müsste es immer geben. Nach dem vergrössern hat der dann immer noch einen PSL von 0, das heisst, er ist effektiv an der Position, wo der hash hin zeigt. Von dort weg kann ich dann einfach jeden Eintrag der reihe nach kopieren. Bei ungeraden hashs mit PSL 0 muss ich ein Feld im Ziel überspringen, ansonsten müsste alles gleich bleiben. Für die leeren Einträge verwende ich einfach den Index-1. Das ergibt den höchsten berechenbaren PSL (Anzahl buckets - 1). Mit dem Trick kann ich nochmal etwas speicher sparen.

Gast #7637619
Lesenswert?

Das funktioniert nur, wenn der Platz in der vergrösserten HM noch frei ist, was wegen Kollisionen nicht der Fall sein muss. Wenn du das mit einer entarteten HM durchspielst, kann es sein, dass die Einträge in der oberen Hälfte wieder in die untere Hälfte wandern. Du darfst also durch Verschieben eines Eintrags in die obere Hälfte keine ungültige HM zurücklassen. Dazu kommt, dass es neben freien Einträgen auch gelöschte Einträge gibt, die nicht übernommen werden sollen. Ist ziemlich aufwändig und bringt nur einen kleinen Speichervorteil. RH Hashing funktioniert auch noch mit einer 3/4 vollen HM, und hat sowieso schon einen Speichervorteil. Ich würde den Aufwand in ein effizienteres Abspeichern der Einträge stecken, das geht direkt in einen Geschwindigkeitsvorteil über. Z.B. würde ich den Hashwert nicht mitabspeichern, der braucht 4-8 Bytes, die meist nutzlos sind und den Cache zumüllen. Der ganze HM Eintrag lässt sich auf 8 Bytes zusammenstampfen.

#7637653
Lesenswert?

Udo K. schrieb:

Das funktioniert nur, wenn der Platz in der vergrösserten HM noch frei ist, was wegen Kollisionen nicht der Fall sein muss.

Normalerweise schon, aber bei diesem Hashmap sind die Einträge sortiert, und auch nach dem verdoppeln der Grösse ist es noch gleich sortiert. Ich habe da einige Ideen, wofür ich die Eigenschaft ausnutzen kann.

Udo K. schrieb:

Z.B. würde ich den Hashwert nicht mitabspeichern, der braucht 4-8 Bytes, die meist nutzlos sind und den Cache zumüllen.

Ich mache das umgekehrt. Ich speichere /nur/ den hash. Da es sich nicht um einen kryptographischen Hash handelt, kann ich eine Funktion nehmen, die sich umkehren lässt. Ich brauche den Hash, um den PSL eines Eintrags zu berechnen, und neue Einträge einzusortieren. Daher nützt mir der Hash mehr als der Wert, ich muss den dann nie neu berechnen.

Gast #7637668
Lesenswert?

Daniel A. schrieb:

Ich mache das umgekehrt. Ich speichere nur den hash. Da es sich nicht um einen kryptographischen Hash handelt, kann ich eine Funktion nehmen, die sich umkehren lässt. Ich brauche den Hash, um den PSL eines Eintrags zu berechnen, und neue Einträge einzusortieren. Daher nützt mir der Hash

Ok, da bin ich ausgestiegen. Wozu musst du den Hash umkehren, und geht das mit allen möglichen Hash Funktionen? Für die PSL (DIB = Distance to Initial Bucket) reichen ein paar Bits aus. Normalerweise ist die DIB << 16, darüber entartet das zu einer unsortierten Liste wie in einer klassischen HM. Pass auf, das die vielen Spezialfälle nicht die Performance in den wichtigen Fällen zusammenhauen. Die HM Einträge mit Zeiger auf die Daten sollten idealerweise <= 8 Bytes sein.

#7637694
Lesenswert?

Udo K. schrieb:

Wozu musst du den Hash umkehren, und geht das mit allen möglichen Hash Funktionen?

Nein, nicht alle Hash Funktionen sind umkehrbar. Ich versuche es zunächst mal mit folgender:

1
DPA__U_SM_KEY_ENTRY_TYPE HASH(const DPA__U_SM_KEY_TYPE n){
2
  DPA__U_SM_KEY_ENTRY_TYPE num = (DPA__U_SM_KEY_ENTRY_TYPE)n;
3
#pragma GCC diagnostic push
4
#pragma GCC diagnostic ignored "-Wshift-count-overflow"
5
  switch(sizeof(num)){
6
    case 32: case 31: case 30: case 29:
7
    case 28: case 27: case 26: case 25:
8
    case 24: case 23: case 22: case 21:
9
    case 20: case 19: case 18:
10
    case 17: num ^= num << 128; /* fallthrough */
11
    case 16: case 15: case 14: case 13:
12
    case 12: case 11: case 10:
13
    case 9: num ^= num << 64; /* fallthrough */
14
    case 8: case 7: case 6:
15
    case 5: num ^= num << 32; /* fallthrough */
16
    case 4:
17
    case 3: num ^= num << 16; /* fallthrough */
18
    case 2: num ^= num << 8; /* fallthrough */
19
    case 1:
20
      num ^= num << 4;
21
      num ^= num << 2;
22
  }
23
#pragma GCC diagnostic pop
24
  return num;
25
}
26

27
DPA__U_SM_KEY_TYPE UNHASH(DPA__U_SM_KEY_ENTRY_TYPE num){
28
  return (DPA__U_SM_KEY_TYPE)(num ^ (num<<2));
29
}

Ich verwende die obersten Bits, also schaue ich einfach, dass diese von möglichst allen Bits abhängig sind. Die Funktion ist nicht ideal wenn die Bits des Keys im vornherein schon nicht unabhängig sind, aber andererseits lässt sich theoretisch immer ein Fall konstruieren, wo eine Hash Funktion nicht ideal performt. Ich muss mal schauen, ob die hier gut genug funktioniert.

Auf jeden Fall kann ich damit hash -> key und key -> hash berechnen (DPA__U_SM_KEY_TYPE und DPA__U_SM_KEY_ENTRY_TYPE sind hier gleich gross, und DPA__U_SM_KEY_ENTRY_TYPE ist ein positiver integer).

Die PSL zu berechnen geht mit dem Hash ja schnell. Ich brauche nur eine Shift operation, und dann muss ich das vom Index des Eintrags abziehen.

Gast #7637715
Lesenswert?

Kannst du deine Hash Funktion so wie die HashBernstein Funktion schreiben? Also mit einer allgemeinen Länge (gerne auch begrenzt auf 32 Bytes)?

1
// Bernstein's hash
2
UINT HashBernstein(PCSTR key, SIZE_T len)
3
{
4
    SIZE_T i;
5
    UINT hash = 5381;
6

7
    for(i = 0; i < len; i++)
8
        hash = 33 * hash + key[i];
9
    return hash ^ (hash >> 16);
10
}

Ich schicke sie dann durch meinen Kollisionschecker durch. Da sollte ein Ergebnis wie in der Tabelle unten rauskommen. Da sieht man, dass etwa die CRC32C_HW die niedrigste Kollisonsrate hat und recht schnell ist.

1
Eingabe: Eagle Netzlisten
2
N$0
3
N$1
4
...
5
N$99990
6
N$99991
7
N$99992
8
N$99993
9
N$99994
10
N$99995
11
N$99996
12
N$99997
13
N$99998
14
N$99999
15

16

17
# ------------------------------------------------------------------------------------------------
18
# Lines read: 100000
19
# Table size: 131072 (17 bits)
20
# Fill-factor: 0.76
21
# Mean chars in line: 6
22
# ------------------------------------------------------------------------------------------------
23
# Id               Hash-Function  Insert/Search-Cycles Coll-Rate Max-Coll Total-Coll  Used-RAM
24
# ------------------------------------------------------------------------------------------------
25
  1                  Paul_Larson        133        162     11.80      813    1179704   1048576
26
  2                    Bernstein        494        588     58.52     1233    5852324   1048576
27
  3   Sedgewick (Algorithm in C)         82        103      7.20      350     720440   1048576
28
  4            Kernighan&Ritchie        996       1208    138.34     4265   13834390   1048576
29
  5     x65599 (Red Dragon Book)        193        225     19.05     2013    1904545   1048576
30
  6                        FNV1A         71         94      1.52      111     151813   1048576
31
  7                   Paul_Hsieh        155        173      6.89      619     688701   1048576
32
  8        One_At_Time (Jenkins)         81        104      1.61      156     161149   1048576
33
  9            Lookup3 (Jenkins)         73         97      1.59      107     159348   1048576
34
 10                      Murmur2         71         95      1.65      117     164728   1048576
35
 11                     Murmur2A         73         95      1.58      107     158448   1048576
36
 12                  DavidHanson         66         87      1.60      111     160013   1048576
37
 13                         SBox         71         92      1.65      186     164985   1048576
38
 14    Alfalfa (Georgi Sanmayce)        318        383     34.31     3175    3431356   1048576
39
 15                    Jesteress         60         83      1.48      102     147615   1048576
40
 16                       Meiyan         66         89      1.68      230     168309   1048576
41
 17                     CRC32_SW         68         79      1.98      136     197640   1048576
42
 18                  CRC32C_HW64         34         49      0.81       35      81457   1048576
43
 19                CRC32C_HW64_B         41         56      1.34      121     134178   1048576
#7637887
Lesenswert?

Das hier müsste äquivalent sein, zumindest auf little endian Maschinen: https://godbolt.org/z/3E9YcG8d8

1
void hash(size_t n, unsigned char out[n], const unsigned char in[n]){
2
  if(!n) return;
3
  memcpy(out, in, n);
4
  unsigned char akku = 0;
5
  for(size_t j=0; j<n*4; j++){
6
    akku ^= (out[j/4] >> ((j%4)*2)) & 0x3;
7
    out[j/4] &= ~(0x3<<((j%4)*2));
8
    out[j/4] |= akku << ((j%4)*2);
9
  }
10
}
11

12
UINT hash2(PCSTR key, SIZE_T len){
13
  // Round up to multiple of the size of UINT
14
  unsigned char a[(len+sizeof(UINT)-1)/sizeof(UINT)*sizeof(UINT)];
15
  memset(a, 0, sizeof(a));
16
  memcpy(a, key, len);
17
  unsigned char b[sizeof(a)];
18
  hash(len, b, a);
19
  return *(UINT*)b+(sizeof(b)-sizeof(UINT));
20
}

Wobei das dafür gedacht ist, wenn man die oberen Bits nutzt.
Für die unteren bits müsste man es auf die andere Seite Shiften: https://godbolt.org/z/oWbzr37Yn

1
void hash(size_t n, unsigned char out[n], const unsigned char in[n]){
2
  if(!n) return;
3
  memcpy(out, in, n);
4
  unsigned char akku = 0;
5
  for(size_t j=n*4; j--;){
6
    akku ^= (out[j/4] >> ((j%4)*2)) & 0x3;
7
    out[j/4] &= ~(0x3<<((j%4)*2));
8
    out[j/4] |= akku << ((j%4)*2);
9
  }
10
}
11

12
UINT hash2(PCSTR key, SIZE_T len){
13
  // Round up to multiple of the size of UINT
14
  unsigned char a[(len+sizeof(UINT)-1)/sizeof(UINT)*sizeof(UINT)];
15
  memset(a, 0, sizeof(a));
16
  memcpy(a+(sizeof(a)-len), key, len);
17
  unsigned char b[sizeof(a)];
18
  hash(len, b, a);
19
  return *(UINT*)b;
20
}
Gast #7637918
Lesenswert?

Compiliert mit dem MS cl nicht:

1
hash_test.cc(76): error C2131: expression did not evaluate to a constant
2
hash_test.cc(91): error C2131: expression did not evaluate to a constant
3

4
76> void Daniel_hash(size_t n, unsigned char out[n], const unsigned char in[n])
5
91> unsigned char a[(len+sizeof(UINT)-1)/sizeof(UINT)*sizeof(UINT)];

Die Ergebnisse mit den Wörtern aus den Werken von Shakespeare sind nicht gut. Das memcpy wird nicht wegoptimiert, und die Kollisionen sind unterirdisch.

1
// https://www.mikrocontroller.net/topic/560335#new
2
void Daniel_hash(size_t n, unsigned char out[], const unsigned char in[])
3
{
4
  if(!n) return;
5
  memcpy(out, in, n);
6
  unsigned char akku = 0;
7
  for(size_t j=n*4; j--;){
8
    akku ^= (out[j/4] >> ((j%4)*2)) & 0x3;
9
    out[j/4] &= ~(0x3<<((j%4)*2));
10
    out[j/4] |= akku << ((j%4)*2);
11
  }
12
}
13

14
UINT Daniel_hash2(PCSTR key, SIZE_T len)
15
{
16
  // Round up to multiple of the size of UINT
17
  SIZE_T n = (len+sizeof(UINT)-1)/sizeof(UINT)*sizeof(UINT);
18
  unsigned char a[64];
19
  memset(a, 0, n);
20
  memcpy(a+(n-len), key, len);
21
  unsigned char b[64];
22
  Daniel_hash(len, b, a);
23
  return *(UINT*)b;
24
}
25

26
[0]$ ./hash_test.exe dic_Shakespeare.txt
27
# ------------------------------------------------------------------------------------------------
28
# Lines read: 3228
29
# Table size: 4096 (12 bits)
30
# Fill-factor: 0.79
31
# Mean chars in line: 6
32
# ------------------------------------------------------------------------------------------------
33
# Id               Hash-Function  Insert/Search-Cycles Coll-Rate Max-Coll Total-Coll  Used-RAM
34
# ------------------------------------------------------------------------------------------------
35
  1                  Paul Larson         57         77      1.79       67       5778     32768
36
  2                    Bernstein         59         79      1.86       69       5999     32768
37
  3   Sedgewick (Algorithm in C)         63         84      2.18      109       7052     32768
38
  4            Kernighan Ritchie         66         87      2.93      203       9448     32768
39
  5     x65599 (Red Dragon Book)         61         82      2.40      102       7755     32768
40
  6                        FNV1A         61         79      2.06       96       6647     32768
41
  7                   Paul Hsieh         61         80      1.61       39       5198     32768
42
  8          OneAtTime (Jenkins)         63         82      1.57       61       5054     32768
43
  9            Lookup3 (Jenkins)         67         87      1.92       73       6195     32768
44
 10                      Murmur2         60         79      1.77       82       5709     32768
45
 11                     Murmur2A         67         86      2.22      153       7157     32768
46
 12                 David Hanson         59         78      1.92       90       6210     32768
47
 13                         SBox         58         76      2.02      120       6532     32768
48
 14    Alfalfa (Georgi Sanmayce)         59         76      1.76       79       5689     32768
49
 15                    Jesteress         59         77      1.99      102       6416     32768
50
 16                       Meiyan         56         73      1.65       82       5323     32768
51
 17                     CRC32_SW         61         78      1.78      103       5750     32768
52
 18                  CRC32C_HW64         57         74      1.91       95       6173     32768
53
 19                CRC32C_HW64_B         55         72      1.72       65       5559     32768
54
 20                   CRC32C_ROT         56         73      1.64       72       5296     32768
55
 21                 Daniel_hash2       2125       2442    299.70     1989     967437     32768
#7637957
Lesenswert?

Udo K. schrieb:

Die Ergebnisse mit den Wörtern aus den Werken von Shakespeare sind nicht gut.

Ok, muss ich mir eine bessere Funktion überlegen. Eigentlich nicht überraschend. (Die Funktion war übrigens von der Pointer Hash Funktion in Mesa inspiriert).

Vielleicht versuche ich es als nächstes mit Knuth's integer hash Funktion. Eine simple Multiplikation, aber manche Werte sind umkehrbar (wie man hier sehen kann https://github.com/jenssegers/optimus)

Aber zuerst kümmere ich mich um den restlichen Kram. Um die Optimierungen kann ich mich kümmern, wen das Ding erst mal läuft.

Das memcpy wird nicht wegoptimiert, und die Kollisionen sind unterirdisch.

Naja, die ursprüngliche Funktion war für integer Hashes gedacht, von genau einem Integer fester Grösse. Dort brauche ich dann auch das memcpy nicht. Dass das äquivalent für variable Längen nicht effizient ist, ist wenig überraschend, dafür war es ja auch nicht designt.

PS: Vermutlich ist das immer noch besser als in Python. Dort ist der Hash eines Integer wieder der selbe integer:

1
dpa@dragonfly:~$ python3 -c 'print(hash(12345))'
2
12345
3
dpa@dragonfly:~$ python3 -c 'print(hash(12345))'

Was aber auch nicht schlimm sein muss. Es kommt immer darauf an, wofür man es verwendet, und das ist auch eine perfekte hash funktion (jeder eindeutige Input hat einen eindeutigen Output und umgekehrt).

Gast #7637976
Lesenswert?

Ich würde eine allgemeine und austauschbare Hash Funktion verwenden, sonst verbaust du dir zu viel. Es gibt nicht die beste Hash Funktion, der Hash hängt ja auch von der Anwendung ab.

Die Hash Map muss mit Zeigern auf die User-Daten zurechtkommen, und auch mit dem Fall, dass die User-Daten einen Zeiger auf den Key enthalten, oder der Key irgendwo in den User-Daten eingebettet ist. Meist sind die User-Daten in einer weiteren Datenstruktur drinnen (Array, Liste), und die HM wird nur zur schnellen Suche verwendet.

Für 64 Bit Zeiger bringt deine Funktion auch einen Haufen Kollisionen, vielleicht ist da noch ein Bug drinnen. Eine leicht modifizierte CRC32C Funktion mit einer zusätzlichen Rotation (mit x64 Hardware Instruction) ist für den Fall ziemlich unschlagbar:

1
1700 Zeiger:
2
00000000FF800000
3
00000000FF800100
4
00000000FF800200
5
00000000FF800300
6
00000000FF800400
7
00000000FF800500
8
00000000FF800600
9
00000000FF800700
10
00000000FF800800
11
00000000FF800900
12
...
13
00000000FF86A000
14
00000000FF86A100
15
00000000FF86A200
16
00000000FF86A300
17

18
[0]$ ./hash_test.exe dic_handles1700.txt -x
19
# ------------------------------------------------------------------------------------------------
20
# Lines read: 1700
21
# Table size: 2048 (11 bits)
22
# Fill-factor: 0.83
23
# Mean chars in line: 8
24
# ------------------------------------------------------------------------------------------------
25
# Id               Hash-Function  Insert/Search-Cycles Coll-Rate Max-Coll Total-Coll  Used-RAM
26
# ------------------------------------------------------------------------------------------------
27
  1                  Paul Larson         47         61      1.81      135       3074     16384
28
  2                    Bernstein         52         67      2.96       95       5033     16384
29
  3   Sedgewick (Algorithm in C)         22         30      0.30       10        513     16384
30
  4            Kernighan Ritchie         28         34      1.19        4       2022     16384
31
  5     x65599 (Red Dragon Book)         98        114      3.07      109       5221     16384
32
  6                        FNV1A         47         63      1.64       59       2792     16384
33
  7                   Paul Hsieh         49         62      2.27       91       3852     16384
34
  8          OneAtTime (Jenkins)         64         79      2.84      131       4825     16384
35
  9            Lookup3 (Jenkins)         45         60      2.17       66       3681     16384
36
 10                      Murmur2         45         60      2.03       91       3456     16384
37
 11                     Murmur2A         52         65      2.41       75       4101     16384
38
 12                 David Hanson        118        131     13.80       61      23466     16384
39
 13                         SBox         59         73      3.71      165       6312     16384
40
 14    Alfalfa (Georgi Sanmayce)         62         72      4.32      296       7338     16384
41
 15                    Jesteress         52         66      4.18       54       7106     16384
42
 16                       Meiyan         51         66      4.18       54       7106     16384
43
 17                     CRC32_SW         32         38      0.69        5       1176     16384
44
 18                  CRC32C_HW64         18         28      0.73       17       1246     16384
45
 19                CRC32C_HW64_B         13         19      0.00        0          0     16384
46
 20                   CRC32C_ROT         12         19      0.00        0          0     16384
47
 21                 Daniel_hash2       1405       1378    179.38      636     304938     16384
Gast #7637998
Lesenswert?

Daniel A. schrieb:

PS: Vermutlich ist das immer noch besser als in Python. Dort ist der Hash eines Integer wieder der selbe integer:dpa@dragonfly:$ python3 -c 'print(hash(12345))' 12345 dpa@dragonfly:$ python3 -c 'print(hash(12345))'

Die Identity Funktion kann in Spezialfällen sehr gut funktionieren, versagt aber völlig in anderen, vor allem wenn die HM eine 2'er Potenz ist.

1
UINT HashIdentity(PCSTR key, SIZE_T len)
2
{
3
    UINT hash = *(UINT*)key;
4
    return hash;
5
}
6

7
// Verschoben um 8 Bits
8
UINT HashIdentity_SHR8(PCSTR key, SIZE_T len)
9
{
10
    UINT hash = *(UINT*)key;
11
    return hash >> 8;
12
}
1
HM Grösse ist eine Primzahl (2053), 1500 Zeiger aufsteigend (+256 Inkrement) - Identity ist super:
2
0000000000800000
3
0000000000800100
4
0000000000800200
5
...
6

7
[0]$ ./hash_test.exe dic_handles1500.txt -x 
8
# ------------------------------------------------------------------------------------------------
9
# Lines read: 1500
10
# Table size: 2053 (12 bits)
11
# Fill-factor: 0.73
12
# Mean chars in line: 8
13
# ------------------------------------------------------------------------------------------------
14
# Id               Hash-Function  Insert/Search-Cycles Coll-Rate Max-Coll Total-Coll  Used-RAM
15
# ------------------------------------------------------------------------------------------------
16
  1                  Paul Larson         55         70      1.15       30       1718     16424
17
  2                    Bernstein         54         68      1.26       43       1890     16424
18
  3   Sedgewick (Algorithm in C)         31         39      0.58       11        864     16424
19
  4            Kernighan Ritchie         36         48      0.93       57       1391     16424
20
  5     x65599 (Red Dragon Book)         54         69      1.30       34       1949     16424
21
  6                        FNV1A         56         70      1.43       48       2149     16424
22
  7                   Paul Hsieh         50         63      1.17       40       1755     16424
23
  8          OneAtTime (Jenkins)         64         78      1.30       44       1943     16424
24
  9            Lookup3 (Jenkins)         46         61      1.25       33       1870     16424
25
 10                      Murmur2         51         61      1.14       29       1706     16424
26
 11                     Murmur2A         57         65      1.26       36       1883     16424
27
 12                 David Hanson        124        139      1.28       43       1925     16424
28
 13                         SBox         51         65      1.18       32       1764     16424
29
 14    Alfalfa (Georgi Sanmayce)         55         66      1.23       53       1851     16424
30
 15                    Jesteress         38         53      0.78       18       1170     16424
31
 16                       Meiyan         37         52      0.78       18       1170     16424
32
 17                     CRC32_SW         53         66      1.38       44       2066     16424
33
 18                  CRC32C_HW64         44         57      1.53       43       2294     16424
34
 19                CRC32C_HW64_B         64         77      1.26       68       1889     16424
35
 20                 HashIdentity         12         17      0.00        0          0     16424
36
 21            HashIdentity_SHR8         12         17      0.00        0          0     16424
37

38

39
HM ist eine Primzahl (131), 73 allgemeine Zeiger - Identity ist super, Identity_SHR8 versagt:
40
...
41
0000000000090728
42
00000000000C0620
43
00000000000D0620
44
00000000000E071E
45
0000000000130674
46
000000000013067A
47
00000000002605C6
48
00000000002705C6
49
00000000002D0672
50
0000000000540614
51

52
[0]$ ./hash_test.exe dic_handles.txt -x
53
# ------------------------------------------------------------------------------------------------
54
# Lines read: 73
55
# Table size: 131 (8 bits)
56
# Fill-factor: 0.56
57
# Mean chars in line: 8
58
# ------------------------------------------------------------------------------------------------
59
# Id               Hash-Function  Insert/Search-Cycles Coll-Rate Max-Coll Total-Coll  Used-RAM
60
# ------------------------------------------------------------------------------------------------
61
  1                  Paul Larson         30         40      0.55        8         40      1048
62
  2                    Bernstein         30         40      0.37        4         27      1048
63
  3   Sedgewick (Algorithm in C)         28         36      0.27        3         20      1048
64
  4            Kernighan Ritchie         29         40      0.36        4         26      1048
65
  5     x65599 (Red Dragon Book)         35         49      0.95       10         69      1048
66
  6                        FNV1A         35         45      0.71        9         52      1048
67
  7                   Paul Hsieh         29         40      0.64        7         47      1048
68
  8          OneAtTime (Jenkins)         39         53      0.52        6         38      1048
69
  9            Lookup3 (Jenkins)         36         51      1.58       17        115      1048
70
 10                      Murmur2         28         39      0.77        8         56      1048
71
 11                     Murmur2A         32         39      0.85        7         62      1048
72
 12                 David Hanson         28         42      0.67        5         49      1048
73
 13                         SBox         28         39      0.47        4         34      1048
74
 14    Alfalfa (Georgi Sanmayce)         28         39      0.37        3         27      1048
75
 15                    Jesteress         22         36      0.60        7         44      1048
76
 16                       Meiyan        570        581      0.60        7         44      1048
77
 17                     CRC32_SW       1107       1113      0.62        5         45      1048
78
 18                  CRC32C_HW64         21         32      0.41        4         30      1048
79
 19                CRC32C_HW64_B         22         31      0.51        4         37      1048
80
 20                 HashIdentity         16         22      0.32        3         23      1048
81
 21            HashIdentity_SHR8        171        179     17.53       53       1280      1048
1
HM ist eine 2-er Potenz (2048): 1500 aufsteigende Zeiger (+256 Inkrement), HashIdentity versagt:
2
[0]$ ./hash_test.exe dic_handles1500.txt -x
3
# ------------------------------------------------------------------------------------------------
4
# Lines read: 1500
5
# Table size: 2048 (11 bits)
6
# Fill-factor: 0.73
7
# Mean chars in line: 8
8
# ------------------------------------------------------------------------------------------------
9
# Id               Hash-Function  Insert/Search-Cycles Coll-Rate Max-Coll Total-Coll  Used-RAM
10
# ------------------------------------------------------------------------------------------------
11
  1                  Paul Larson         63         80      0.92       22       1385     16384
12
  2                    Bernstein         43         57      1.25       64       1882     16384
13
  3   Sedgewick (Algorithm in C)         18         25      0.00        0          0     16384
14
  4            Kernighan Ritchie         26         33      0.98        4       1466     16384
15
  5     x65599 (Red Dragon Book)         42         58      1.80       87       2707     16384
16
  6                        FNV1A         43         59      1.60       50       2393     16384
17
  7                   Paul Hsieh         85        101      1.19       27       1789     16384
18
  8          OneAtTime (Jenkins)         52         69      1.43       57       2141     16384
19
  9            Lookup3 (Jenkins)         64         81      1.38       36       2065     16384
20
 10                      Murmur2         41         55      1.76       66       2639     16384
21
 11                     Murmur2A         77         89      1.51       48       2271     16384
22
 12                 David Hanson        141        178     11.82       53      17731     16384
23
 13                         SBox         48         64      2.02       53       3032     16384
24
 14    Alfalfa (Georgi Sanmayce)         69         89      5.83      224       8738     16384
25
 15                    Jesteress         30         46      1.45       13       2178     16384
26
 16                       Meiyan         29         45      1.45       13       2178     16384
27
 17                     CRC32_SW         28         33      0.53        3        794     16384
28
 18                  CRC32C_HW64         16         24      0.54       16        805     16384
29
 19                CRC32C_HW64_B         13         18      0.00        0          0     16384
30
 20                 HashIdentity        632        781     93.25      187     139876     16384
31
 21            HashIdentity_SHR8          9         15      0.00        0          0     16384
32

33
HM ist eine 2-er Potenz (128): Allgemeine Zeiger, HashIdentity_SHR8 versagt:
34
[0]$ ./hash_test.exe dic_handles.txt -x
35
# ------------------------------------------------------------------------------------------------
36
# Lines read: 73
37
# Table size: 128 (7 bits)
38
# Fill-factor: 0.57
39
# Mean chars in line: 8
40
# ------------------------------------------------------------------------------------------------
41
# Id               Hash-Function  Insert/Search-Cycles Coll-Rate Max-Coll Total-Coll  Used-RAM
42
# ------------------------------------------------------------------------------------------------
43
  1                  Paul Larson        125        154      0.93       10         68      1024
44
  2                    Bernstein         31         39      0.75       10         55      1024
45
  3   Sedgewick (Algorithm in C)         23         32      0.45        7         33      1024
46
  4            Kernighan Ritchie         22         30      0.25        4         18      1024
47
  5     x65599 (Red Dragon Book)         26         39      0.86        9         63      1024
48
  6                        FNV1A        557        568      0.53        7         39      1024
49
  7                   Paul Hsieh         65         75      0.60        8         44      1024
50
  8          OneAtTime (Jenkins)         33         46      0.59        7         43      1024
51
  9            Lookup3 (Jenkins)         22         33      0.66        8         48      1024
52
 10                      Murmur2         20         31      0.34        5         25      1024
53
 11                     Murmur2A         24         33      0.64        9         47      1024
54
 12                 David Hanson         87        116      9.60       26        701      1024
55
 13                         SBox         25         38      0.86        8         63      1024
56
 14    Alfalfa (Georgi Sanmayce)         26         34      0.60        4         44      1024
57
 15                    Jesteress        898        912      0.30        6         22      1024
58
 16                       Meiyan         14         28      0.30        6         22      1024
59
 17                     CRC32_SW         24         32      0.41        6         30      1024
60
 18                  CRC32C_HW64         15         25      0.34        6         25      1024
61
 19                CRC32C_HW64_B         17         27      0.55        7         40      1024
62
 20                 HashIdentity         12         21      0.47        9         34      1024
63
 21            HashIdentity_SHR8        243        314     34.68       71       2532      1024
#7638140
Lesenswert?

Das ist merkwürdig. Immerhin beinhalten die ursprünglichen Daten, und die von meiner Funktion gehashten Daten, die gleiche Menge an Informationen, also gleich viel Entropie.
Vermutlich kommt der Unterschied daher, dass einerseits nur ein Teil des Hashs verwendet wird, und andererseits am Anfang / Ende der Daten noch ein paar Nullen padding angehängt werden, wenn die Daten kein vielfaches von sizeof(UINT) sind. Von dem Teil wird auch der Teilhash genommen. Im schlimmsten Fall hat man in dem Fall nur 10 sich unabhängig ändernde Bits.
Würde man die Identity Funktion auf gleiche weise mit Radding Bytes versehen, hätte man im schlimmsten Fall sogar nur 8 sich ändernde bits.

Aber ich kann ja nicht davon ausgehen, dass ich bei meiner hash Funktion mit Integern fester Grösse nur grosse Zahlen erhalte, daher hätte ich das Problem in der Praxis auch, insbesondere auch, weil ich ja signifikanten Bits des Hash bevorzuge.

Gast #7638332
Lesenswert?

Der Hash mag ja noch viel Information enthalten, aber spätestens bei der Abbildung des Hashes auf die Hash Map wird Information weggeschmissen, besonders im Fall das die Hash Map Grösse eine 2-er Potenz ist - da werden die oberen Bits einfach weggeschnitten. Die Idee mit der umkehrbaren Hash Funktion ist ganz interessant, schränkt aber die Hash Funktion zu sehr ein. Aus den geposteten Tabellen kannst du dir ja eine brauchbare Hash Funktionen heraussuchen. Die einfachen Hash Funktionen wie Identity versagen in vielen Anwendungsfällen, wenn etwa Zeiger aus mehreren Speicherbereichen vorkommen. Die CRC Funktionen sind ziemlich gut und mit HW Unterstützung schnell. Die FNV Funktion ist für kurze Keys gut, für lange aber langsam. Die SBOX, Murmur2 und Paul Hsieh Funktionen sind auch gut (In der Tabelle oben stimmen die Cycles manchmal nicht, wenn ein Taskswitch passiert).

#7638755
Lesenswert?

Ich habe jetzt eine neue umkehrbare hash Funktion. Ein xor mit einer Zufallszahl, und eine Multiplikation (wie bei Knuth multiplikativer Hash Funktion). Durch wählen einer geeigneten Primzahl, und dessen multiplikativem Invers, ist sie umkehrbar.

1
T hash(T x){
2
  return (x ^ r) * choose_prime(T);
3
}
4

5
T unhash(T x){
6
  return (x * choose_prime_inverse(T)) ^ r;
7
}

Das xor mit der Zufallszahl habe ich drin, in der Hoffnung, dass bei unterschiedlichen Zufallszahlen nicht die selben Hashs kollidieren, aber ob das effektiv funktioniert, habe ich noch nicht getestet. Falls nicht nehme ich es wieder raus, der Hash selbst wird dadurch ja nicht besser.

Ich habe mir, je nachdem wie gross der Positive Integer Datetyp ist, unterschiedliche Primzahlen rausgesucht. Für alles von 1 bis und mit 256 bits hab ich eine. Die sind mehr oder weniger zufällig, ich habe welche genommen, die möglichst gross sind, aber trotzdem einigermassen zufällig wirken. Also nicht sowas wie 0x10001.
Naja, zumindest hoffe ich die Nummern sind Primzahlen, bei den grossen kann ich das nicht wirklich zweifelsfrei sagen. Wird schon passen...

Der ganze code ist hier: https://godbolt.org/z/6Wo15qs98 Nicht von den grossen _Generic abschrecken lassen, da wird nur die Primzahl zur compile time ausgesucht. Zumindest bei de paar Grössen, die ich ausprobiert habe, hat das funktioniert.

Alles über 64bit (also was ein PC nicht direkt kann) wird aber recht ineffizient. Vielleicht werde ich da dass dann in der Praxis stattdessen als ein paar 64bit Werte behandeln, und die dann XORen.

Ich muss nachher mal noch eine Variante für variable Datenlängen machen, um das hier etwas zu testen. Eventuell sollte ich auch mal noch was hinzufügen, um die Bitreihenfolge umzukehren. Das bringt ja wenig, wenn wir die unteren Bits testen, aber ich die oberen verwende...

Gast #7638804
Lesenswert?

Daniel A. schrieb:

Der ganze code ist hier: https://godbolt.org/z/6Wo15qs98 Nicht von den grossen _Generic abschrecken lassen, da wird nur die Primzahl zur compile time ausgesucht. Zumindest bei de paar Grössen, die ich ausprobiert habe, hat das funktioniert.

Knapp 1000 Zeilen für eine Multiplikation mit einer Konstanten.. Ich hätte nicht gedacht, das das in C überhaupt möglich ist. Ich habe mir den ASM Code angeschaut, der ist einfacher lesbar 😉.

Die Hash Funktion sollte so sein, dass sie zur Runtime mit beliebigen Daten umgehen kann. Sonst baust du endlose Spezialfälle, und die Hash Map wird langsam, da der Instruction-Cache zugemüllt wird.

Ich habe deine Funktion für 64 Bit eingestampft mit r = 0:

1
INLINE UINT Daniel_hash64(UINT64 x)
2
{
3
    return x * 0xFD2D84CA00000225;
4
}
5

6
UINT Daniel_hash3(PCSTR key, SIZE_T len)
7
{
8
    return Daniel_hash64(*(UINT64*)key);
9
}

(wie bei Knuth multiplikativer Hash Funktion).

Die Idee bei den multiplikativen Hash Funktionen ist, dass die Bits nach oben wandern und dabei durchgemischt werden, bei langen Keys haben die ersten Bytes aber keinen Einfluss mehr auf den Hashwert. Das hast du bei deiner Hash Funktion nicht, du schiebst durch die Multiplikation die relevanten Bits nach oben, vermischt sie aber nicht. Je nachdem wo die wichtigen Bits im Zeiger stehen, sind sie für die Hash-Map relevant oder nicht. Das ist der grosse Vorteil der CRC Funktionen: Da wird eine pseudo Zufallszahl durch ein Schieberegister mit Feedback generiert, Bits die oben rauswandern, werden unten wieder reingestopft. Auch bei sehr langen Keys hat das erste Byte noch einen Einfluss auf den Hashwert.

Hier die Ergebnisse:

1
[0]$ ./hash_test.exe dic_handles1500.txt -x ### 1500 aufsteigende Zeiger ###
2
# ------------------------------------------------------------------------------------------------
3
# Lines read: 1500
4
# Table size: 2048 (11 bits)
5
# Fill-factor: 0.73
6
# Mean chars in line: 8
7
# ------------------------------------------------------------------------------------------------
8
# Id               Hash-Function  Insert/Search-Cycles Coll-Rate Max-Coll Total-Coll  Used-RAM
9
# ------------------------------------------------------------------------------------------------
10
  1                  Paul Larson         39         51      0.92       22       1385     16384
11
  2                    Bernstein         43         55      1.25       64       1882     16384
12
  3   Sedgewick (Algorithm in C)         18         25      0.00        0          0     16384
13
  4            Kernighan Ritchie         27         32      0.98        4       1466     16384
14
  5     x65599 (Red Dragon Book)         47         57      1.80       87       2707     16384
15
  6                        FNV1A         45         57      1.60       50       2393     16384
16
  7                   Paul Hsieh         42         54      1.19       27       1789     16384
17
  8          OneAtTime (Jenkins)         55         66      1.43       57       2141     16384
18
  9            Lookup3 (Jenkins)         43         55      1.38       36       2065     16384
19
 10                      Murmur2         83         92      1.76       66       2639     16384
20
 11                     Murmur2A         47         55      1.51       48       2271     16384
21
 12                 David Hanson        134        119     11.82       53      17731     16384
22
 13                         SBox         53         61      2.02       53       3032     16384
23
 14    Alfalfa (Georgi Sanmayce)         80         81      5.83      224       8738     16384
24
 15                    Jesteress         32         42      1.45       13       2178     16384
25
 16                       Meiyan         32         45      1.45       13       2178     16384
26
 17                     CRC32_SW         29         32      0.53        3        794     16384
27
 18                  CRC32C_HW64         17         23      0.54       16        805     16384
28
 19                CRC32C_HW64_B         35         40      0.00        0          0     16384
29
 20                   CRC32C_ROT         10         15      0.00        0          0     16384
30
 21                 Daniel_hash3        795        612     93.25      187     139876     16384
31

32
[0]$ ./hash_test.exe dic_handles.txt -x ### 73 DLL Zeiger eines Programms ###
33
# ------------------------------------------------------------------------------------------------
34
# Lines read: 73
35
# Table size: 128 (7 bits)
36
# Fill-factor: 0.57
37
# Mean chars in line: 8
38
# ------------------------------------------------------------------------------------------------
39
# Id               Hash-Function  Insert/Search-Cycles Coll-Rate Max-Coll Total-Coll  Used-RAM
40
# ------------------------------------------------------------------------------------------------
41
  1                  Paul Larson        951        965      0.93       10         68      1024
42
  2                    Bernstein       1310       1318      0.75       10         55      1024
43
  3   Sedgewick (Algorithm in C)         23         32      0.45        7         33      1024
44
  4            Kernighan Ritchie         22         30      0.25        4         18      1024
45
  5     x65599 (Red Dragon Book)        646        657      0.86        9         63      1024
46
  6                        FNV1A         27         35      0.53        7         39      1024
47
  7                   Paul Hsieh        189        196      0.60        8         44      1024
48
  8          OneAtTime (Jenkins)         35         45      0.59        7         43      1024
49
  9            Lookup3 (Jenkins)         23         32      0.66        8         48      1024
50
 10                      Murmur2        108        119      0.34        5         25      1024
51
 11                     Murmur2A         25         34      0.64        9         47      1024
52
 12                 David Hanson        111        100      9.60       26        701      1024
53
 13                         SBox         31         40      0.86        8         63      1024
54
 14    Alfalfa (Georgi Sanmayce)         23         35      0.60        4         44      1024
55
 15                    Jesteress        132        142      0.30        6         22      1024
56
 16                       Meiyan         15         28      0.30        6         22      1024
57
 17                     CRC32_SW         24         31      0.41        6         30      1024
58
 18                  CRC32C_HW64         15         24      0.34        6         25      1024
59
 19                CRC32C_HW64_B         18         26      0.55        7         40      1024
60
 20                   CRC32C_ROT         16         23      0.40        4         29      1024
61
 21                 Daniel_hash3         13         20      0.42        7         31      1024
62

63
[0]$ ./hash_test.exe dic_handles3.txt -x ### Zeiger aus 3 Speicherbereichen ###
64
# ------------------------------------------------------------------------------------------------
65
# Lines read: 2700
66
# Table size: 4096 (12 bits)
67
# Fill-factor: 0.66
68
# Mean chars in line: 8
69
# ------------------------------------------------------------------------------------------------
70
# Id               Hash-Function  Insert/Search-Cycles Coll-Rate Max-Coll Total-Coll  Used-RAM
71
# ------------------------------------------------------------------------------------------------
72
  1                  Paul Larson         38         51      0.73       22       1981     32768
73
  2                    Bernstein         44         56      0.91       45       2453     32768
74
  3   Sedgewick (Algorithm in C)         57         63      4.08       22      11011     32768
75
  4            Kernighan Ritchie         26         32      0.98        4       2638     32768
76
  5     x65599 (Red Dragon Book)         41         53      0.89       27       2400     32768
77
  6                        FNV1A         43         55      1.09       48       2934     32768
78
  7                   Paul Hsieh         40         52      0.89       22       2407     32768
79
  8          OneAtTime (Jenkins)         52         63      0.95       28       2578     32768
80
  9            Lookup3 (Jenkins)         38         51      0.94       21       2533     32768
81
 10                      Murmur2         41         52      1.12       37       3030     32768
82
 11                     Murmur2A         42         51      0.88       29       2381     32768
83
 12                 David Hanson        145        137     11.29       56      30491     32768
84
 13                         SBox         42         53      0.93       26       2509     32768
85
 14    Alfalfa (Georgi Sanmayce)         49         59      1.59      129       4291     32768
86
 15                    Jesteress         33         43      0.90       14       2427     32768
87
 16                       Meiyan         32         43      0.90       14       2427     32768
88
 17                     CRC32_SW         74         79      2.97       34       8032     32768
89
 18                  CRC32C_HW64         36         39      2.45       40       6608     32768
90
 19                CRC32C_HW64_B         18         25      0.46        9       1238     32768
91
 20                   CRC32C_ROT         21         29      0.83        8       2249     32768
92
 21                 Daniel_hash3        480        382     54.99      156     148466     32768
#7638853
Lesenswert?

Udo K. schrieb:

Die Idee bei den multiplikativen Hash Funktionen ist, dass die Bits nach oben wandern und dabei durchgemischt werden, bei langen Keys haben die ersten Bytes aber keinen Einfluss mehr auf den Hashwert. Das hast du bei deiner Hash Funktion nicht, du schiebst durch die Multiplikation die relevanten Bits nach oben, vermischt sie aber nicht. Je nachdem wo die wichtigen Bits im Zeiger stehen, sind sie für die Hash-Map relevant oder nicht.

In meinem Fall macht es keinen Sinn, die Bits von Oben wieder nach Unten zu mischen, denn ich verwende die oberen Bits.

Die meisten Hashmaps machen ja index = hash % size, wenn size eine 2er Potenz ist, entspricht das index = hash & (size-1). Ich speichere die Grösse meines Hashmap gleich als log2 ab, den ich verdopple / halbiere dessen Grösse ja immer nur. Ich verwende dann effektiv index = hash >> (sizeof(hash)*CHAR_BIT-lbsize).

Das ist der Grund, warum es bei mir keine rolle spielt, wenn die unteren Bits nicht so gut gemischt sind. Und auch wenn mehr der unteren Bits verwendet werden / das hash map grösser wird, spielt es keine rolle. Der Extremfall wäre ja wenn alle Bits verwendet würden, in dem Fall hätte es sogar gar keine Kollisionen mehr.

Wenn man das in dem test da umdreht, wird das Ergebnis sicher besser.

1
uint64_t hash(uint64_t x){
2
  uint64_t a = x * 0xFD2D84CA00000225ull;
3
  uint64_t r = 0;
4
  for(int i=0; i<64; i++)
5
    r |= ( (uint64_t)!!( a & (1ull<<i) ) ) << (63-i);
6
  return r;
7
}

Udo K. schrieb:

Knapp 1000 Zeilen für eine Multiplikation mit einer Konstanten.. Ich hätte nicht gedacht, das das in C überhaupt möglich ist. Ich habe mir den ASM Code angeschaut, der ist einfacher lesbar 😉.

Ich habe eine Art Template, wo ich die Funktion für viele verschiedene Integer Typen definiere. Bei einigen Typen weiss man nicht, wie gross sie sind. 16bit, 32bit, 64bit, oder gar ein vielfaches von 9 statt 8bit? Wäre alles möglich. Also schaue ich einfach, wie viele Bits hat der Typ, und suche zur Compile Time eine passende Konstante aus.

Die Hash Funktion sollte so sein, dass sie zur Runtime mit beliebigen Daten umgehen kann.

Das kann sie auch. Am Ende ist immer noch eine einfache Multiplikation einer Variable mit einer Konstante. Ist nur jenachdem, ob das jetzt ein short oder ein long ist, eine andere.

Sonst baust du endlose Spezialfälle, und die Hash Map wird langsam, da der Instruction-Cache zugemüllt wird.

Naja, dort habe ich auch einen HashMap und HashSet Typ pro key Typ. Ein wenig wie bei C++ Templates.

Gast #7638931
Lesenswert?

Schaut besser aus: Coll-Rate ist überschaubar, an der Geschwindigkeit musst du noch was machen:

1
INLINE UINT64 Daniel_hash64(UINT64 x){
2
  UINT64 a = x * 0xFD2D84CA00000225ull;
3
  UINT64 r = 0;
4
  for(int i=0; i<64; i++)
5
    r |= ( (UINT64)!!( a & (1ull<<i) ) ) << (63-i);
6
  return r;
7
}
8

9
UINT Daniel_hash3(PCSTR key, SIZE_T len)
10
{
11
    return Daniel_hash64(*(UINT64*)key);
12
}
1
[0]$ hash_test.exe dic_handles.txt -x
2
# ------------------------------------------------------------------------------------------------
3
# Lines read: 73
4
# Table size: 128 (7 bits)
5
# Fill-factor: 0.57
6
# Mean chars in line: 8
7
# ------------------------------------------------------------------------------------------------
8
# Id               Hash-Function  Insert/Search-Cycles Coll-Rate Max-Coll Total-Coll  Used-RAM
9
# ------------------------------------------------------------------------------------------------
10
  1                  Paul Larson         26         39      0.93       10         68      1024
11
  2                    Bernstein         72         80      0.75       10         55      1024
12
  3   Sedgewick (Algorithm in C)         23         31      0.45        7         33      1024
13
  4            Kernighan Ritchie        670        677      0.25        4         18      1024
14
  5     x65599 (Red Dragon Book)         28         36      0.86        9         63      1024
15
  6                        FNV1A         24         32      0.53        7         39      1024
16
  7                   Paul Hsieh         25         31      0.60        8         44      1024
17
  8          OneAtTime (Jenkins)         34         44      0.59        7         43      1024
18
  9            Lookup3 (Jenkins)         23         32      0.66        8         48      1024
19
 10                      Murmur2         20         30      0.34        5         25      1024
20
 11                     Murmur2A         25         33      0.64        9         47      1024
21
 12                 David Hanson        111        100      9.60       26        701      1024
22
 13                         SBox         29         36      0.86        8         63      1024
23
 14    Alfalfa (Georgi Sanmayce)         56         64      0.60        4         44      1024
24
 15                    Jesteress         16         26      0.30        6         22      1024
25
 16                       Meiyan         15         28      0.30        6         22      1024
26
 17                     CRC32_SW         24         31      0.41        6         30      1024
27
 18                  CRC32C_HW64         15         23      0.34        6         25      1024
28
 19                CRC32C_HW64_B         18         26      0.55        7         40      1024
29
 20                   CRC32C_ROT         49         56      0.40        4         29      1024
30
 21                 Daniel_hash3        151        168      0.37        5         27      1024
1
[0]$ hash_test.exe dic_handles1500.txt -x
2
# ------------------------------------------------------------------------------------------------
3
# Lines read: 1500
4
# Table size: 2048 (11 bits)
5
# Fill-factor: 0.73
6
# Mean chars in line: 8
7
# ------------------------------------------------------------------------------------------------
8
# Id               Hash-Function  Insert/Search-Cycles Coll-Rate Max-Coll Total-Coll  Used-RAM
9
# ------------------------------------------------------------------------------------------------
10
  1                  Paul Larson         39         51      0.92       22       1385     16384
11
  2                    Bernstein         44         54      1.25       64       1882     16384
12
  3   Sedgewick (Algorithm in C)         40         45      0.00        0          0     16384
13
  4            Kernighan Ritchie         26         31      0.98        4       1466     16384
14
  5     x65599 (Red Dragon Book)         47         56      1.80       87       2707     16384
15
  6                        FNV1A         46         56      1.60       50       2393     16384
16
  7                   Paul Hsieh         41         54      1.19       27       1789     16384
17
  8          OneAtTime (Jenkins)         55         65      1.43       57       2141     16384
18
  9            Lookup3 (Jenkins)         43         56      1.38       36       2065     16384
19
 10                      Murmur2         46         54      1.76       66       2639     16384
20
 11                     Murmur2A         47         54      1.51       48       2271     16384
21
 12                 David Hanson        135        120     11.82       53      17731     16384
22
 13                         SBox         53         62      2.02       53       3032     16384
23
 14    Alfalfa (Georgi Sanmayce)         82         80      5.83      224       8738     16384
24
 15                    Jesteress         33         43      1.45       13       2178     16384
25
 16                       Meiyan         33         45      1.45       13       2178     16384
26
 17                     CRC32_SW         29         32      0.53        3        794     16384
27
 18                  CRC32C_HW64         17         23      0.54       16        805     16384
28
 19                CRC32C_HW64_B         12         17      0.00        0          0     16384
29
 20                   CRC32C_ROT         11         16      0.00        0          0     16384
30
 21                 Daniel_hash3        179        190      3.08       36       4624     16384

Hier noch ein Fall mit Zeigern aus 4 Speicherbereichen:

1
[0]$ hash_test.exe dic_handles4.txt -x
2
# ------------------------------------------------------------------------------------------------
3
# Lines read: 3000
4
# Table size: 4096 (12 bits)
5
# Fill-factor: 0.73
6
# Mean chars in line: 8
7
# ------------------------------------------------------------------------------------------------
8
# Id               Hash-Function  Insert/Search-Cycles Coll-Rate Max-Coll Total-Coll  Used-RAM
9
# ------------------------------------------------------------------------------------------------
10
  1                  Paul Larson         48         59      1.40       47       4195     32768
11
  2                    Bernstein         46         59      1.11       42       3328     32768
12
  3   Sedgewick (Algorithm in C)         48         59      1.75       30       5236     32768
13
  4            Kernighan Ritchie         41         52      1.13       31       3387     32768
14
  5     x65599 (Red Dragon Book)         47         58      1.32       42       3973     32768
15
  6                        FNV1A         50         62      1.53       68       4603     32768
16
  7                   Paul Hsieh         49         60      1.64       63       4910     32768
17
  8          OneAtTime (Jenkins)         58         68      1.52       71       4565     32768
18
  9            Lookup3 (Jenkins)         56         69      1.44       46       4316     32768
19
 10                      Murmur2         46         56      1.53       56       4583     32768
20
 11                     Murmur2A         47         55      1.30       36       3889     32768
21
 12                 David Hanson         81         85      5.03       57      15081     32768
22
 13                         SBox         48         57      1.45       50       4341     32768
23
 14    Alfalfa (Georgi Sanmayce)         52         61      2.00      125       5986     32768
24
 15                    Jesteress         92         89      7.92       79      23763     32768
25
 16                       Meiyan         92         91      7.92       79      23763     32768
26
 17                     CRC32_SW         44         51      1.22       24       3652     32768
27
 18                  CRC32C_HW64         30         39      1.05       23       3137     32768
28
 19                CRC32C_HW64_B         28         37      0.75       23       2236     32768
29
 20                   CRC32C_ROT         32         41      1.09       22       3256     32768
30
 21                 Daniel_hash3        171        182      1.98       36       5925     32768
#7638954
Lesenswert?

Klaus schrieb:

Daniel A. schrieb:

Naja, das kommt von der Umkehrung der Bitreihenfolge

Ich verstehs noch nicht. Willst du eine Hash-Funktion schreiben, die nur dann halbwegs ok funktioniert, wenn bestimmte Teile der Daten gut verteilt sind?

Nein, die Verteilung der Daten vor dem Hashen spielt keine rolle. Es ist nur so, dass die oberen Bits des Hash bessere Avalanche Eigenschaften als die unteren haben.

Die Hash Funktion werde ich nur für mein neues HashMap verwenden. Dieses bevorzugt die oberen Bits des Hash. Und darauf ist die Hash Funktion ausgelegt. Wie man ja gerade gesehen hat, funktioniert sie gut, wenn man sie so verwendet, wie ich es vor habe. Works as designed.

Gast #7638991
Lesenswert?

Ok, dann freue ich mich schon mal auf deine Hash Table.

Hier ein Vorschlag für ein C Interface. Ich nehme an, dass der Key in den User-Daten an einem fixen Offset abgespeichert ist. Die speziellen HT Funktionen rufen die allgemeinen HT Funktionen mit den passenden Funktionszeigern auf.

1
#ifndef HASHTBL_H
2
#define HASHTBL_H
3

4
#include <windows.h>
5

6

7
typedef INT   (*HT_CMP)(PVOID, PVOID);  /* return 0 if the arguments are equal */
8
typedef UINT  (*HT_HASH)(PVOID);        /* return hash value of the argument */
9

10
/*
11
 * General hash table functions.
12
 */
13

14
typedef struct _HASH_TABLE HASH_TABLE;
15

16
HASH_TABLE* HT_Init(UINT table_size, UINT flags, UINT key_offset);
17
BOOL    HT_Destroy(HASH_TABLE *ht);
18
BOOL    HT_Dump(CONST HASH_TABLE *ht);
19
INT     HT_Check(CONST HASH_TABLE *ht, BOOL check_entries, HT_HASH hashfp);
20
PVOID   HT_Search(CONST HASH_TABLE *ht, PVOID search_key, HT_HASH hashfp, HT_CMP cmpfp);
21
PVOID   HT_Insert(HASH_TABLE *ht, PVOID user_data, HT_HASH hashfp, HT_CMP cmpfp);
22
PVOID   HT_InsertNoResize(HASH_TABLE *ht, PVOID user_data, HT_HASH hashfp, HT_CMP cmpfp);
23
PVOID   HT_Remove(HASH_TABLE *ht, PVOID search_key, HT_HASH hashfp, HT_CMP cmpfp);
24
PVOID   HT_RemoveMe(HASH_TABLE *ht, PVOID user_data, HT_HASH hashfp, HT_CMP cmpfp);
25
BOOL    HT_Resize(HASH_TABLE *ht, UINT new_size, HT_HASH hashfp, HT_CMP cmpfp);
26
UINT    HT_Hash(PVOID key, SIZE_T len);
27

28

29
/*
30
 * Hash table statistic information.
31
 */
32
typedef struct {
33
    UINT64    table_size;       /* Table size */
34
    UINT64    used_entries;     /* Used entries */
35
    UINT64    max_dib;          /* Max DIB (Distance to Initial Bucket) */
36
    UINT64    collisions;       /* Sum of the DIB of all entries */
37
} HT_STAT;
38

39

40
BOOL    HT_GetStat(HASH_TABLE *ht, HT_STAT *sp, HT_HASH hashfp);
41

42

43
/*
44
 * Hash table iterator functions.
45
 */
46
typedef struct {
47
    CONST HASH_TABLE *ht;       /* Pointer to hash table */
48
    UINT64      position;       /* Current iterator position */
49
} HT_ITERATOR;
50

51

52
VOID    HT_IterInit(HT_ITERATOR *it, CONST HASH_TABLE *ht);
53
PVOID   HT_Next(HT_ITERATOR *it);
54

55

56
/*
57
 * Special Hash table for zero terminated strings.
58
 * Key is a PCSTR.
59
 */
60
PVOID   HT_String_Search(CONST HASH_TABLE *ht, PCSTR key);
61
PVOID   HT_String_Insert(HASH_TABLE *ht, PVOID data);
62
PVOID   HT_String_InsertNoResize(HASH_TABLE *ht, PVOID data);
63
PVOID   HT_String_Remove(HASH_TABLE *ht, PCSTR key);
64
PVOID   HT_String_RemoveMe(HASH_TABLE *ht, PVOID data);
65
BOOL    HT_String_Resize(HASH_TABLE *ht, UINT new_size);
66
INT     HT_String_Check(CONST HASH_TABLE *ht, BOOL check_entries);
67
BOOL    HT_String_GetStat(HASH_TABLE *ht, HT_STAT *sp);
68

69

70

71
/*
72
 * Special Hash table for Windows Handles or PVOID.
73
 * Key is a HANDLE (aka PVOID).
74
 */
75
PVOID   HT_Handle_Search(CONST HASH_TABLE *ht, HANDLE key);
76
PVOID   HT_Handle_Insert(HASH_TABLE *ht, PVOID data);
77
PVOID   HT_Handle_InsertNoResize(HASH_TABLE *ht, PVOID user_data);
78
PVOID   HT_Handle_Remove(HASH_TABLE *ht, HANDLE key);
79
PVOID   HT_Handle_RemoveMe(HASH_TABLE *ht, PVOID data);
80
BOOL    HT_Handle_Resize(HASH_TABLE *ht, UINT new_size);
81
INT     HT_Handle_Check(CONST HASH_TABLE *ht,  BOOL check_entries);
82
BOOL    HT_Handle_GetStat(HASH_TABLE *ht, HT_STAT *sp);
83

84

85
#endif /* HASHTBL_H */
Gast #7639003
Lesenswert?

Daniel A. schrieb:

Works as designed.

Zeiger die auf aufeinanderfolgende 16 MB Bereiche zeigen, sind nicht so optimal. Selbst bei minimalem Fill Faktor von 0.02 versagt die Funktion.

1
[0]$ head dic_handles5.txt
2
0000000010000000
3
0000000011000000
4
0000000012000000
5
0000000013000000
6
0000000014000000
7
0000000015000000
8
0000000016000000
9
0000000017000000
10
0000000018000000
11
0000000019000000
12

13
[0]$ ./hash_test.exe dic_handles5.txt -x -s14
14
# ------------------------------------------------------------------------------------------------
15
# Lines read: 400
16
# Table size: 16384 (14 bits)
17
# Fill-factor: 0.02
18
# Mean chars in line: 8
19
# ------------------------------------------------------------------------------------------------
20
# Id               Hash-Function  Insert/Search-Cycles Coll-Rate Max-Coll Total-Coll  Used-RAM
21
# ------------------------------------------------------------------------------------------------
22
  1                  Paul Larson         26         31      0.02        2          7    131072
23
  2                    Bernstein         19         26      0.01        1          2    131072
24
  3   Sedgewick (Algorithm in C)        105        111      0.00        0          0    131072
25
  4            Kernighan Ritchie         18         23      0.00        0          0    131072
26
  5     x65599 (Red Dragon Book)         18         22      0.01        2          3    131072
27
  6                        FNV1A         18         23      0.01        1          4    131072
28
  7                   Paul Hsieh         18         23      0.01        1          6    131072
29
  8          OneAtTime (Jenkins)         32         36      0.01        1          3    131072
30
  9            Lookup3 (Jenkins)         16         23      0.02        2          8    131072
31
 10                      Murmur2         16         21      0.01        1          3    131072
32
 11                     Murmur2A         20         24      0.02        2          8    131072
33
 12                 David Hanson         23         27      0.10        2         40    131072
34
 13                         SBox         20         23      0.01        1          3    131072
35
 14    Alfalfa (Georgi Sanmayce)         16         22      0.01        1          3    131072
36
 15                    Jesteress         39         41      2.94        7       1176    131072
37
 16                       Meiyan         38         43      2.94        7       1176    131072
38
 17                     CRC32_SW         18         22      0.00        0          0    131072
39
 18                  CRC32C_HW64         11         16      0.00        0          0    131072
40
 19                CRC32C_HW64_B         12         17      0.00        0          0    131072
41
 20                   CRC32C_ROT         11         16      0.00        0          0    131072
42
 21                 Daniel_hash3       1271        953    135.98      379      54391    131072
#7639202
Lesenswert?

Klaus schrieb:

Daniel A. schrieb:

(Ich weiss auch nicht wirklich, was eine gute / schlechte Konstante aus macht).

Warum versuchst Du dann das Rad neu zu erfinden und nicht auf Bewährtes zurückzugreifen?

Eine Multiplikation als Hash Funktion nun wirklich nichts neues, das hat sogar einen Namen.
Und warum ich gerade diese Methode nehme, steht weiter oben. Weil die Funktion so umkehrbar ist.

Gast #7639212
Lesenswert?

Du kannst auch einfach einen Zeiger auf die Daten abspeichern. Der Hash kann schnell neu berechnet werden. Das ist einfach und umkehrbar. Mit 32 Bit Hash Werten und beliebigen Daten funktioniert Umkehrbarkeit nicht. Die Daten sind auch nicht immer gleich lang.

#7639253
Lesenswert?

Udo K. schrieb:

Ich nehme an, dass der Key in den User-Daten an einem fixen Offset abgespeichert ist.

Eine Version, bei der das so gelöst wird, kann ich später auch noch hinzufügen. Bei der Variante an der ich gerade Arbeite, wird es nicht nötig sein, den Key noch zusätzlich zu speichern. Das bietet gewisse Vorteile, aber auch gewisse Nachteile.

Udo K. schrieb:

Du kannst auch einfach einen Zeiger auf die Daten abspeichern.

Ja, beim HashMap werde ich so oder so einen Pointer als Wert speichern. Aber nicht unbedingt im HashSet.

Der Hash kann schnell neu berechnet werden.

Das kommt ganz auf die Hash Funktion an. Eine simple Multiplikation würde tatsächlich nicht ins Gewicht fallen.

Das ist einfach und umkehrbar. Mit 32 Bit Hash Werten und beliebigen Daten funktioniert Umkehrbarkeit nicht. Die Daten sind auch nicht immer gleich lang.

Wenn die Daten beliebig lang sein können, kann das Hashen schon recht langsam werden. Deshalb funktioniert das HashMap, an dem ich gerade arbeite, anders.

Mein Key ist ein Integer, ein Pointer, oder ein bereits eindeutiges String Objekt (wobei das Objekt eine feste Grösse hat, bei kleinen Strings der String selbst, anderenfalls einen eindeutigen Pointer auf den String (d.h. es kann keine unterschiedlichen Pointer auf die selben Werte geben)).

Damit habe ich immer eine fixe Grösse der zu hashenden Daten.
Solange der Hash mindestens so gross ist wie die Daten, kann ich den auch umkehrbar machen. Dann sind Hash und Daten gleichwertig.

Eine Version für beliebige andere Arten von Daten, mit eigener Hash / Vergleichsfunktion, kann ich später auch noch hinzufügen. Aber zuerst will ich mal diese Version fertig bekommen.


Die Primzahlen oben wurden übrigens mit einem Python Skript generiert. Ich hatte einfach die erste vor (i**2 * 9779516207054258 + i**2 * 110241896472871) // (10**16) genommen.
Ich versuche mal 2 andere.

Vielleicht geht eine richtig zufällige besser:
0x4E7184CB320E45E9llu / 0xEA5E24FD2782E259llu (inverse)
Oder vielleicht eine wo ungefähr jedes 5te Bit gesetzt ist:
0x108421084210841Fllu / 0xF7DF7DF7DF7DF7DFllu (inverse)

1
from sympy import prevprime, nextprime
2
from os import urandom
3

4
def gen_c_const(x):
5
  s = ''
6
  i = 0
7
  while x:
8
    r = x %  2**64
9
    x = x // 2**64
10
    r = '((T)0x%Xllu)' % r
11
    if i: r = '(%s<<%d)' % (r,i)
12
    i += 64
13
    s = r + s
14
    if x: s = ' | ' + s
15
  return s
16

17
a=''
18
b=''
19

20
for i in range(8,289):
21
  j = 2**i
22
  #s = (j * 9779516207054258 + j * 110241896472871) // (10**16) # This wasn't good enough
23
  #s = int.from_bytes(urandom((i+7)//8)) % j
24
  s = 0x84210842108421084210842108421084210842108421084210842108421084210842108421084210842108421084210842108421 % j
25
  pp = prevprime(s)
26
  ipp = pow(pp, -1, j)
27
  #np = nextprime(s)
28
  #inp = pow(np, -1, j)
29
  a += "    char(*)[%d]: %s, \\\n" % (i, gen_c_const(pp))
30
  b += "    char(*)[%d]: %s, \\\n" % (i, gen_c_const(ipp))
31
  #print("0x%X 0x%X: ~0x%X: 0x%X*0x%X 0x%X*0x%X" % (i,j,s,pp,ipp,np,inp))
32

33
print(a)
34
print(b)
Gast #7639309
Lesenswert?

Daniel A. schrieb:

Vielleicht geht eine richtig zufällige besser: 0x4E7184CB320E45E9llu / 0xEA5E24FD2782E259llu (inverse)

Die ist ziemlich gut. Ich habe einen Fall gefunden, wo sie abkackt. Zeiger aus drei Speicherbereichen, einer im Addressraum um die 28 Bit, einer um die 56 Bit, und ein Bereich mit zufälligen Addressen. Da versagen auch etliche alte Standard-Hash, die wohl nie für Zeiger gebaut wurden. Die CRC ist aber immer ganz vorne dabei - meine Empfehlung. Ich schaue in der Tabelle auf Collision-Rate und Max-Collisions. Die Instruction Cycles sind nicht immer zuverlässig. Ich würde es jetzt damit belassen. Wenn du das ernsthaft weiterverfolgen willst, musst du dir eine Hash Tabelle nach deinen Vorstellungen bauen, und mit Statistik Code testen. Der Code den ich verwenden ist von https://www.strchr.com/hash_functions abgekupfert (eine der besten Hash Seiten im Web).

1
[0]$ ./hash_test.exe dic_handles6c.txt -x
2
# ------------------------------------------------------------------------------------------------
3
# Lines read: 1500
4
# Table size: 2048 (11 bits)
5
# Fill-factor: 0.73
6
# Mean chars in line: 8
7
# ------------------------------------------------------------------------------------------------
8
# Id               Hash-Function  Insert/Search-Cycles Coll-Rate Max-Coll Total-Coll  Used-RAM
9
# ------------------------------------------------------------------------------------------------
10
  1                  Paul Larson        148        143     13.92      216      20887     16384
11
  2                    Bernstein        687        534     79.25      811     118875     16384
12
  3   Sedgewick (Algorithm in C)        196        169     19.83      228      29738     16384
13
  4            Kernighan Ritchie        987        728    115.74      980     173614     16384
14
  5     x65599 (Red Dragon Book)        355        282     39.27      409      58898     16384
15
  6                        FNV1A         40         53      1.13       31       1696     16384
16
  7                   Paul Hsieh         43         54      1.41       49       2114     16384
17
  8          OneAtTime (Jenkins)         51         63      1.24       36       1863     16384
18
  9            Lookup3 (Jenkins)         40         53      1.48       77       2225     16384
19
 10                      Murmur2         39         50      1.20       35       1796     16384
20
 11                     Murmur2A         43         53      1.22       33       1831     16384
21
 12                 David Hanson         50         61      2.07       41       3106     16384
22
 13                         SBox         43         66      1.63       52       2440     16384
23
 14    Alfalfa (Georgi Sanmayce)        189        173     18.95      241      28425     16384
24
 15                    Jesteress        214        180     22.57      421      33851     16384
25
 16                       Meiyan        214        182     22.57      421      33851     16384
26
 17                     CRC32_SW         38         54      1.15       23       1720     16384
27
 18                  CRC32C_HW64         30         40      1.04       21       1558     16384
28
 19                   CRC32C_ROT         27         38      1.08       18       1624     16384
29
 20                 Daniel_hash3        248        248     11.64      910      17462     16384
Gast #7639420
Lesenswert?

Daniel A. schrieb:

Danke, ich werde mir das mal ansehen.

Noch eine Frage zu den Statistiken, eine Sache verstehe ich da noch nicht. "# Lines read: 1500" ist doch die Anzahl Werte, oder? Wie kann es zu 17462 Kollisionen kommen, wenn es nur 1500 Einträge gibt?

Es werden alle 1500 Einträge gesucht, und die Kollisionen aller Einträge aufaddiert. Wenn also jeder Eintrag 10 Kollisionen hat, kommt 15000 raus.

In dem Fall oben hat jeder Eintrag im Mittel 11.6 Kollisionen, und mindestens 1 Eintrag hat 910 Kollisionen.

Zur Info: Die Hash Tabelle verwendet in den Tests Open Addressing und keine Linked List.

In der Tabelle oben sieht man auch schön, dass die moderneren Hash Funktionen gut mit 64-Bit Zeigern zurechtkommen, während die klassischen Funktionen - die für Text entworfen sind - damit Probleme haben. Die schlimmsten Hash Funktionen wie die Fletcher Checksum oder Weinberger sind der Tabelle schon draussen.

Böse sind auch Testdaten mit langen identischen Prefixen. Da kann man gleich weitere Funktionen für eine allgemeine Hash-Lib aussortieren. FNV1A hat hier zwar eine halbwegs passable Kollisionsrate, ist aber wegen der langen Strings langsam.

1
[0]$ head dic_xxx1500.txt
2
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx0000
3
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx0001
4
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx0002
5
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx0003
6
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx0004
7
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx0005
8

9
[0]$ ./hash_test.exe dic_xxx1500.txt
10
# ------------------------------------------------------------------------------------------------
11
# Lines read: 1500
12
# Table size: 2048 (11 bits)
13
# Fill-factor: 0.73
14
# Mean chars in line: 101
15
# ------------------------------------------------------------------------------------------------
16
# Id               Hash-Function  Insert/Search-Cycles Coll-Rate Max-Coll Total-Coll  Used-RAM
17
# ------------------------------------------------------------------------------------------------
18
  1                  Paul Larson       1359       1281     74.31      693     111459     16384
19
  2                    Bernstein       1172       1111     60.72      524      91080     16384
20
  3   Sedgewick (Algorithm in C)        345        351      5.11       50       7660     16384
21
  4            Kernighan Ritchie        756        719     33.41      363      50110     16384
22
  5     x65599 (Red Dragon Book)       1621       1514     91.24      955     136857     16384
23
  6                        FNV1A        279        301      1.37       27       2048     16384
24
  7                   Paul Hsieh        139        159      1.27       37       1905     16384
25
  8          OneAtTime (Jenkins)        420        441      1.48       39       2215     16384
26
  9            Lookup3 (Jenkins)        147        165      1.53       48       2300     16384
27
 10                      Murmur2        109        130      1.35       49       2032     16384
28
 11                     Murmur2A        110        127      1.21       36       1814     16384
29
 12                 David Hanson        142        158      1.54       41       2316     16384
30
 13                         SBox        174        191      1.26       74       1897     16384
31
 14    Alfalfa (Georgi Sanmayce)        796        761     36.53      553      54796     16384
32
 15                    Jesteress         69         89      1.20       19       1796     16384
33
 16                       Meiyan         76         96      1.29       48       1930     16384
34
 17                     CRC32_SW        205        207      0.52       11        775     16384
35
 18                  CRC32C_HW64         45         62      0.84       16       1263     16384
36
 19              CRC32C_HW64_ROT         48         66      0.84       16       1263     16384
37
 20                 Daniel_hash3      11182       9854    749.50     1499    1124250     16384

Mit dem umgekehrten Fall (Postfix) kommen die Funktionen besser zurecht, bis auf David Hanson und Alfalfa, die völlig abkacken.

1
[0]$ head -5 dic_1500xxx.txt
2
0000xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
3
0001xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
4
0002xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
5
0003xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
6
0004xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
7

8
[0]$ ./hash_test.exe dic_1500xxx.txt
9
# ------------------------------------------------------------------------------------------------
10
# Lines read: 1500
11
# Table size: 2048 (11 bits)
12
# Fill-factor: 0.73
13
# Mean chars in line: 101
14
# ------------------------------------------------------------------------------------------------
15
# Id               Hash-Function  Insert/Search-Cycles Coll-Rate Max-Coll Total-Coll  Used-RAM
16
# ------------------------------------------------------------------------------------------------
17
  1                  Paul Larson        260        287      1.13       36       1688     16384
18
  2                    Bernstein        260        284      1.11       27       1662     16384
19
  3   Sedgewick (Algorithm in C)        268        281      0.55       19        822     16384
20
  4            Kernighan Ritchie        301        321      5.49       26       8234     16384
21
  5     x65599 (Red Dragon Book)        258        286      0.98       35       1468     16384
22
  6                        FNV1A        263        290      1.30       43       1945     16384
23
  7                   Paul Hsieh        129        153      1.32       57       1982     16384
24
  8          OneAtTime (Jenkins)        450        473      1.16       33       1744     16384
25
  9            Lookup3 (Jenkins)        130        153      1.02       23       1531     16384
26
 10                      Murmur2         99        125      1.48       52       2226     16384
27
 11                     Murmur2A        102        125      1.32       30       1984     16384
28
 12                 David Hanson       6332       5888    749.50     1499    1124250     16384
29
 13                         SBox        163        182      1.15       48       1721     16384
30
 14    Alfalfa (Georgi Sanmayce)       6466       6030    749.50     1499    1124250     16384
31
 15                    Jesteress        125        135      9.77      100      14648     16384
32
 16                       Meiyan        116        127      8.55      112      12818     16384
33
 17                     CRC32_SW        241        246      0.47       15        707     16384
34
 18                  CRC32C_HW64         68         85      3.87       39       5810     16384
35
 19              CRC32C_HW64_ROT         44         66      0.82       14       1224     16384
36
 20                 Daniel_hash3        399        401     29.33      101      43991     16384

Wenn die Zeilen richtig lang werden, kacken alle Funktionen ab, die Strings Byte für Byte abarbeiten. Die CRC32C HW Funktionen arbeiten mit 8 Bytes auf einmal, und ziehen davon.

1
Ein Beispiel mit 504 Zeichen pro Zeile:
2

3
[0]$ ./hash_test.exe dic_1500xxxxxx.txt
4
# ------------------------------------------------------------------------------------------------
5
# Lines read: 1500
6
# Table size: 2048 (11 bits)
7
# Fill-factor: 0.73
8
# Mean chars in line: 504
9
# ------------------------------------------------------------------------------------------------
10
# Id               Hash-Function  Insert/Search-Cycles Coll-Rate Max-Coll Total-Coll  Used-RAM
11
# ------------------------------------------------------------------------------------------------
12
  1                  Paul Larson       1293       1358      1.02       24       1527     16384
13
  2                    Bernstein       1293       1357      0.92       31       1376     16384
14
  3   Sedgewick (Algorithm in C)       1288       1351      0.35       12        524     16384
15
  4            Kernighan Ritchie       1315       1379      3.69       24       5530     16384
16
  5     x65599 (Red Dragon Book)       1295       1360      1.37       59       2050     16384
17
  6                        FNV1A       1304       1368      1.58       80       2374     16384
18
  7                   Paul Hsieh        521        577      1.30       34       1953     16384
19
  8          OneAtTime (Jenkins)       1954       2007      1.26       30       1883     16384
20
  9            Lookup3 (Jenkins)        537        589      1.40       38       2095     16384
21
 10                      Murmur2        365        421      1.41       96       2110     16384
22
 11                     Murmur2A        366        420      1.33       40       1995     16384
23
 12                 David Hanson       7003       6691    749.50     1499    1124250     16384
24
 13                         SBox        687        736      1.49       51       2230     16384
25
 14    Alfalfa (Georgi Sanmayce)       7855       7556    749.50     1499    1124250     16384
26
 15                    Jesteress        261        313     10.19      166      15284     16384
27
 16                       Meiyan        261        313     10.19      166      15284     16384
28
 17                     CRC32_SW        953        998      0.82       16       1234     16384
29
 18                  CRC32C_HW64        112        188      0.74       14       1112     16384
30
 19              CRC32C_HW64_ROT        130        195      1.23       14       1852     16384
31
 20                 Daniel_hash3        423        471     29.33      101      43991     16384
#7646440
Lesenswert?

Mittlerweile habe ich eine erste funktionierende Version: https://github.com/Daniel-Abrecht/dpa-utils/commit/f1229f6470826f37b461aa5695c6abcd75970635

Das wichtige ist in der Datei: https://github.com/Daniel-Abrecht/dpa-utils/blob/master/src/set-and-map.c

API sieht so aus:

1
int dpa_u_set_<T>_add(dpa_u_set_<T>_t* that, <T> key);
2
bool dpa_u_set_<T>_remove(dpa_u_set_<T>_t* that, <T> key);
3
bool dpa_u_set_<T>_has(const dpa_u_set_<T>_t* that, <T> key);
4
void dpa_u_set_<T>_clear(dpa_u_set_<T>_t* that);
5
size_t dpa_u_set_<T>_count(const dpa_u_set_<T>_t* that);
6

7
bool dpa_u_map_<T>_remove(dpa_u_map_<T>_t* that, <T> key);
8
bool dpa_u_map_<T>_has(const dpa_u_map_<T>_t* that, <T> key);
9
void dpa_u_map_<T>_clear(dpa_u_map_<T>_t* that);
10
size_t dpa_u_map_<T>_count(const dpa_u_map_<T>_t* that);
11
int dpa_u_map_<T>_exchange(dpa_u_map_<T>_t* that, <T> key, void** value);
12
int dpa_u_map_<T>_set(dpa_u_map_<T>_t* that, <T> key, void* value);
13
dpa_u_optional_pointer_t dpa_u_map_<T>_get(const dpa_u_map_<T>_t* that, <T> key);
14
dpa_u_optional_pointer_t dpa_u_map_<T>_get_and_remove(dpa_u_map_<T>_t* that, <T> key);

Wobei <T> eines der folgenden ist:

Name Type
string dpa_u_bo_unique_t
pointer void*
uc unsigned char
us unsigned short
u unsigned
lu unsigned long
llu unsigned long long
z size_t
u8 uint8_t
u16 uint16_t
u24 uint24_t
u32 uint32_t
u64 uint64_t
u128 dpa_uint128_t
u256 dpa_uint256_t

Wendet man dann z.B. so an:

1
  dpa_u_map_u_t container = {0};
2
  dpa_u_map_u_set(&container, 123, "Hello World!");
3
  dpa_u_optional_pointer_t v = dpa_u_map_u_get(&container, 123);
4
  if(v.present)
5
    puts(v.value);
6
  dpa_u_map_u_clear(&container);

Ich muss noch etwas mehr testen. Vermutlich kann man auch noch einiges optimieren. Ich habe noch nicht nachgesehen, wie schnell es ist.
Und ich will später noch Funktionen hinzufügen, um über die Sets / Maps zu iterieren, und um Schnittmengen zu bilden. Da dürfte die Sortierung der Maps / Sets dann nützlich werden.
Aber die Basics, Hinzufügen und Löschen von Einträgen, scheint schon mal zu gehen.

Gast #7646830
Lesenswert?

Ich habe mal versucht das unter Windows zu compilieren:

1
[130]$ gcc -I ../include -c set-and-map.c
2
In file included from ../include/dpa/utils/refcount.h:6,
3
                 from ../include/dpa/utils/bo.h:5,
4
                 from ../include/dpa/utils/_set-and-map.generator:1,
5
                 from ../include/dpa/utils/set.h:6,
6
                 from set-and-map.c:26:
7
../include/dpa/utils/_bo/types.h:50:1: error: static assertion failed: "dpa_u_bo_simple_ro_t has an unexpected size
8
"
9
   50 | static_assert(sizeof(dpa_u_bo_simple_ro_t) == DPA__U_BO_COMMON_SIZE, "dpa_u_bo_simple_ro_t has an unexpecte
10
d size");
11
      | ^~~~~~~~~~~~~
12
../include/dpa/utils/_bo/types.h:58:1: error: static assertion failed: "dpa_u_bo_simple_t has an unexpected size"
13
   58 | static_assert(sizeof(dpa_u_bo_simple_t) == DPA__U_BO_COMMON_SIZE, "dpa_u_bo_simple_t has an unexpected size
14
");
15
      | ^~~~~~~~~~~~~
16
../include/dpa/utils/_bo/types.h:85:1: error: static assertion failed: "dpa_u_bo_ro_t has an unexpected size"
17
   85 | static_assert(sizeof(dpa_u_bo_ro_t) == DPA__U_BO_COMMON_SIZE, "dpa_u_bo_ro_t has an unexpected size");
18
      | ^~~~~~~~~~~~~
19
../include/dpa/utils/_bo/types.h:96:1: error: static assertion failed: "dpa_u_bo_t has an unexpected size"
20
   96 | static_assert(sizeof(dpa_u_bo_t) == DPA__U_BO_COMMON_SIZE, "dpa_u_bo_t has an unexpected size");
21
      | ^~~~~~~~~~~~~
22
set-and-map.c:79:28: fatal error: src/set-and-map.c: No such file or directory
23
   79 | #define DPA__U_SM_TEMPLATE <src/set-and-map.c>
24
      |                            ^
25
compilation terminated.

Ähnlich mit MS cl

1
[2]$ cl -I ../include -c set-and-map.c
2
Microsoft (R) C/C++ Optimizing Compiler Version 15.00.30729.207 for x64
3
Copyright (C) Microsoft Corporation.  All rights reserved.
4

5
set-and-map.c
6
../include\dpa/utils/common.h(7) : fatal error C1083: Cannot open include file: 'stdnoreturn.h': No such file or di
7
rectory

Und Clang-cl

1
[0]$ clang-cl -I ../include -c set-and-map.c
2
In file included from set-and-map.c:26:
3
In file included from ../include/dpa/utils/set.h:6:
4
In file included from ../include/dpa/utils/_set-and-map.generator:1:
5
In file included from ../include/dpa/utils/bo.h:4:
6
../include/dpa/utils/common.h(20,9): warning: DANGER: C atomic or threads support missing, library will not be threadsafe! [-W#pragma-messages]
7
#pragma message ("DANGER: C atomic or threads support missing, library will not be threadsafe!")
8
        ^
9
In file included from set-and-map.c:26:
10
In file included from ../include/dpa/utils/set.h:6:
11
In file included from ../include/dpa/utils/_set-and-map.generator:1:
12
In file included from ../include/dpa/utils/bo.h:5:
13
In file included from ../include/dpa/utils/refcount.h:10:
14
C:/WinDDK/7600.16385.1/inc/crt/stdlib.h(393,20): error: __declspec attributes must be an identifier or string literal
15
_CRTIMP __declspec(noreturn) void __cdecl exit(__in int _Code);
16
                   ^
17
C:/msys64/mingw64/lib/clang/16/include/stdnoreturn.h(13,18): note: expanded from macro 'noreturn'
18
#define noreturn _Noreturn
19
                 ^
20
In file included from set-and-map.c:26:
21
In file included from ../include/dpa/utils/set.h:6:
22
In file included from ../include/dpa/utils/_set-and-map.generator:1:
23
In file included from ../include/dpa/utils/bo.h:5:
24
In file included from ../include/dpa/utils/refcount.h:10:
25
C:/WinDDK/7600.16385.1/inc/crt/stdlib.h(394,20): error: __declspec attributes must be an identifier or string literal
26
_CRTIMP __declspec(noreturn) void __cdecl _exit(__in int _Code);
27
                   ^
28
C:/msys64/mingw64/lib/clang/16/include/stdnoreturn.h(13,18): note: expanded from macro 'noreturn'
29
#define noreturn _Noreturn
30
                 ^
31
In file included from set-and-map.c:81:
32
In file included from ../include/dpa/utils/_set-and-map.generator:9:
33
../include/dpa/utils/_set-and-map.common.h(40,10): fatal error: 'src/set-and-map.c' file not found
34
#include DPA__U_SM_TEMPLATE
35
         ^~~~~~~~~~~~~~~~~~
36
set-and-map.c(79,28): note: expanded from macro 'DPA__U_SM_TEMPLATE'
37
#define DPA__U_SM_TEMPLATE <src/set-and-map.c>
38
                           ^~~~~~~~~~~~~~~~~~~
39
<scratch space>(126,1): note: expanded from here
40
<src/set-and-map.c>
41
^~~~~~~~~~~~~~~~~~~
42
1 warning and 3 errors generated.

Ich will bitte ein Header File und ein Lib File oder ein C File. Das muss mit jedem der 3 grossen Compiler durchlaufen. Über das Makefile und den Source Code traue ich mich nicht drüber. Das geht über meinen Horizont.

#7646952
Lesenswert?

Udo K. schrieb:

Ich habe mal versucht das unter Windows zu compilieren:

Ich habe (zuhause) halt leider keinen Windows PC, das macht das Testen etwas schwieriger.

Die gcc Fehlermeldung sieht sehr nach dem hier aus: https://stackoverflow.com/questions/7789668/why-would-the-size-of-a-packed-structure-be-different-on-linux-and-windows-when Insbesondere -mno-ms-bitfields. Aber gut zu wissen, was der Windows GCC das macht, das habe ich noch nicht getestet. Es gibt da wohl ein Attribut, welches das selbe macht __attribute__((gcc_struct)) bzw. [[gnu::gcc_struct]] in c23. Ich muss mal schauen, das ich das bei gcc bei dem Struct anwende.

Bei msvc hat man das selbe Problem auch, aber dort setze ich automatisch -DDPA_U_BO_NOT_PACKED, das stellt das struct packing aus. Mach das dpa_u_bo_unique_t Objekt aber grösser, was in einem open addressing hash map, wo es ja kopiert werden muss, nicht ideal ist.
Für msvc habe ich vorhin noch einen kleinen Fix ins repo gepusht, dort wird max_align_t nicht definiert. Ist hallt immer noch nicht ganz C11 Konform... Ich kompiliere das zum Testen in der WSL, mit gnu make, mit make use=msvc. Log ist im Anhang, da sieht man, mit welchen Parametern der Compiler aufgerufen wird, da braucht es noch ein paar.

Und was bei all deinen Aufrufen fehlt, ist noch -I. Das src/set-and-map.c ist ein Template, das Inkludiert sich indirekt selbst ein paar mal, das findet sich selbst sonst nicht. Und eventuell noch --std=c17 btw. /std:c17 , das ist immerhin c11/c17 code.

Udo K. schrieb:

Ich will bitte ein Header File und ein Lib File oder ein C File. Das muss mit jedem der 3 grossen Compiler durchlaufen.

Kann ich machen, ich Kopiere das nachher mal zusammen.

Angehängte Dateien:
#7647085
Lesenswert?

So, nach einigen Präprozessor Tricks und herum kopieren, ist jetzt alles in einem File. Hat länger gedauert und war aufwendiger als ich dachte, ich hoffe, ich habe dabei keine Fehler gemacht. Die Funktionen, die einen dpa_u_bo_unique_t nehmen, hab ich weg gelassen, und einige Präprozessor Checks auch.

Auf Windows hab ich das jetzt nicht ausprobiert, aber sollte eigentlich passen.

Ein kleines Benchmark zum kurz ausprobieren habe ich auch noch angehängt (Da füge ich einfach 2**28 = 268435456 ein):

1
$ gcc -std=c17 -O2 -Wall -Wextra -pedantic -ffunction-sections -fdata-sections -Wl,--gc-sections set-and-map.c set-map-benchmark.c -o x
2
$ ./x
3
             Function |   total |    work |  resize
4
     dpa_u_set_uc_add |  0.759s |  0.759s |  0.000s
5
     dpa_u_set_us_add |  0.720s |  0.693s |  0.027s
6
      dpa_u_set_u_add |  5.119s |  4.820s |  0.299s
7
     dpa_u_set_lu_add |  9.899s |  8.036s |  1.863s
8
    dpa_u_set_llu_add |  9.907s |  8.028s |  1.879s
9
      dpa_u_set_z_add |  9.966s |  8.065s |  1.902s
10
     dpa_u_set_u8_add |  0.763s |  0.763s |  0.000s
11
    dpa_u_set_u16_add |  0.735s |  0.708s |  0.027s
12
    dpa_u_set_u32_add |  5.161s |  4.862s |  0.298s
13
    dpa_u_set_u64_add |  9.956s |  8.096s |  1.860s
14
dpa_u_set_pointer_add |  9.957s |  8.079s |  1.878s
15

16
             Function |   total |    work |  resize
17
     dpa_u_map_uc_set |  3.512s |  2.373s |  1.139s
18
     dpa_u_map_us_set |  2.162s |  1.807s |  0.355s
19
      dpa_u_map_u_set | 17.168s | 14.845s |  2.323s
20
     dpa_u_map_lu_set | 14.163s | 10.884s |  3.279s
21
    dpa_u_map_llu_set | 14.228s | 10.971s |  3.257s
22
      dpa_u_map_z_set | 14.347s | 11.172s |  3.175s
23
     dpa_u_map_u8_set |  3.530s |  2.390s |  1.140s
24
    dpa_u_map_u16_set |  2.146s |  1.787s |  0.359s
25
    dpa_u_map_u32_set | 17.540s | 15.237s |  2.303s
26
    dpa_u_map_u64_set | 14.198s | 10.989s |  3.209s
27
dpa_u_map_pointer_set | 14.076s | 10.903s |  3.173s

Jetzt muss ich das nur noch mit irgendwas vergleichen, keine Ahnung ob das gut ist oder nicht. Ist aber interessant, wie viel die Grösse des Keys ausmacht.

Angehängte Dateien:
Gast #7647102
Lesenswert?

dpa_u_unsequenced ist nirgends definiert?

1
[0]$ cl -nologo -std:c17 -DDPA_U_NO_THREADS -utf-8 -O2 -Iinclude -I. -W0 -GS- -c set-and-map.c
2
set-and-map.c
3
H:\Udo\Projekte\UKEagle\SourceCode\TestAVLTree\current\dpa\set-and-map.h(4): error C2054: expected '(' to follow 'dpa_u_unsequenced'
4
H:\Udo\Projekte\UKEagle\SourceCode\TestAVLTree\current\dpa\set-and-map.h(47): error C2085: 'dpa_u_count_bits': not in formal parameter list
5
H:\Udo\Projekte\UKEagle\SourceCode\TestAVLTree\current\dpa\set-and-map.h(47): error C2143: syntax error: missing ';' before '{'

Inzwischen habe ich den github Code mit MS cl zum compilieren bekommen.

  • /std:c17 geht, std:clatest geht aber nicht, die C Versionsabfrage geht nicht richig
  • Die Warnungen bekomme ich nur mit /W0 weg
  • Die Funktionen haben alle __declspec(dllimport) oder __declspec(dllexport) davor - das ist nur für DLLs gedacht
  • clang liefert immer noch seitenweise Fehler

Wenn ich den C Code als C++ kompiliere kracht es gleich wieder. Sorry, aber das geht so nicht.

#7647124
Lesenswert?

Udo K. schrieb:

dpa_u_unsequenced ist nirgends definiert?

Da habe ich übersehen, das es in set-and-map.h noch an einer stelle verwendet wird, und habe es in die .c gepackt. Wenn __builtin_popcountg oder __builtin_popcountll vorhanden ist (wie bei GCC), nimmt es das, ansonsten definiert es eine fallback Funktion und die verwendet dpa_u_unsequenced. dpa_u_unsequenced selbst ist nur ein optionaler Hinweis an den Compiler, dass die Funktion keine Seiteneffekte hat. Ich korrigiere das schnell mal... (Anhang)

Udo K. schrieb:

Inzwischen habe ich den github Code mit MS cl zum compilieren bekommen.

  • /std:c17 geht, std:clatest geht aber nicht, die C Versionsabfrage geht nicht richig

Doch, die geht einwandfrei. In clang und gcc habe ich --std=c2x / --std=c23 getestet, ohne Probleme. Die Library macht dann auch Gebrauch von den neuen Features. Native Attribute, typeof, usw. Gut möglich, dass msvc halt mal wieder unvollständig ist.

  • Die Warnungen bekomme ich nur mit /W0 weg

Ja, bei msvc gibt es an manchen stellen viele false positives.

  • Die Funktionen haben alle __declspec(dllimport) oder __declspec(dllexport) davor - das ist nur für DLLs gedacht

Ursprünglich wollte ich auch das eine DLL, zusätzlich zur statischen lib, erstellt wird. Nur habe ich die nie zum laufen gebracht.

Ausserdem ist das Konzept unter Windows ziemlich kaputt.
Unter linux sage ich mit attribute((visibility(default))), das ein Symbol sichtbar sein soll. Die Objekt Files packe ich in eine statische Library. Und aus der lasse ich den Linker dann auch noch eine shared library (.so) erstellen. Das geht dort wunderbar.
Aber unter Windows? Dort müsste ich bei der statischen Library ohne dllexport, aber bei der DLL mit. Also müsste ich das doppelt kompilieren. Und dann gibt es noch weitere Einschränkungen, wenn es um das Exportieren / Importieren von Variablen statt Funktionen geht, unter anderem. So ein scheiss.

  • clang liefert immer noch seitenweise Fehler

Da wird er sicher gute Gründe haben.

Wenn ich den C Code als C++ kompiliere kracht es gleich wieder. Sorry, aber das geht so nicht.

Es ist halt nun mal kein C++ Code, sondern C. Sorry, da bist du selber schuld.

Angehängte Dateien:
Gast #7647306
Lesenswert?

Daniel A. schrieb:

  • clang liefert immer noch seitenweise Fehler

Da wird er sicher gute Gründe haben.

Wenn ich den C Code als C++ kompiliere kracht es gleich wieder. Sorry, aber das geht so nicht.

Es ist halt nun mal kein C++ Code, sondern C. Sorry, da bist du selber schuld.

Du bist ganz schön weltfremd. Wie soll ich deine Hash Map und die C++ std::map gemeinsam testen? In der guten alten Zeit wäre das kein Problem. Da hätte man ein 30 Zeilen Header File mit der Deklaration der 5 Funktionen, dazu "extern C" und etwas Doku und gut ist es. Die Implementierung lebt in einer Lib oder einem C99 Source File. Ich als Anwender muss davon nichts wissen, das kann jeder 0815 C Compiler auch noch in 10 Jahren übersetzen.

Dein Header File hat aber über 1000 Zeilen und reizt alle möglichen schlecht unterstützen C Konstrukte aus; von den meisten habe ich vor diesem Thread noch nicht mal was gehört und fast alles kommt mir unnötig kompliziert vor. Warum programmierst du nicht C++? Bei deinem Programmierstil wäre das kompatibler, stabiler und lesbarer. Die MS und Windows Jammerei ignoriere ich mal. Mir wird immer ein Rätsel bleiben warum Informatiker mit aller Kraft die wenigen Arbeitgeber die noch 6-stellig zahlen schlecht reden.

Ich kann mit allem möglichen Code gut leben, solange ich als Anwender damit nicht in Berührung komme, und solange das Ding compiliert und tut was es soll und das gut. Und meine Hochachtung vor deinem Programmierwissen. Nur versuche dich mal in Leute reinzudenken, die nicht so gut aufgestellt sind oder das als Hobby machen, und die deinen Code verstehen wollen. Meiner Erfahrung nach ist verständlicher Code auch wartbarer und performanter.

Ich habe nach etlichen unnötigen Hacks deine Hash Map mit MS CL compiliert und mit anderen Suchverfahren verglichen. Die Ergebnisse sind gut, rechtfertigen aber meiner Meinung nach nicht die Beschränkung der Hashfunktion und den Code Bloat. Aber das ist ja noch eine 0-er Version und da ist sicher Potential nach oben. Zur Info: Die rote "Direct-Access" Kurve ganz unten ist der reine Speicherzugriff auf die Daten. Die graue RobinHood-Hash-Direct-Key Kurve ist Robin Hood mit perfekter Hash Funktion ohne Kollisionen. Die grüne RobinHood-Hash Kurve verwendet einen CRC32C Hash. Dann kommt deine Funktion, die etwa gleichauf std::unordered Hash ist. Im Bereich um die 3000 Einträge gibt es zu viele Kollisionen, im Bereich unter 100 Einträge ist der Overhead relativ gross, da ist sogar die AVL Suche schneller.

Gruss, Udo

Angehängte Dateien:
Gast #7647321
Lesenswert?

Oliver S. schrieb:

In der guten alten Zeit gab es noch gar keine hash maps. Da gab es Lochkarten. Oder welche Zeit meinst du?

Hash Maps gibts schon länger als C. Aber der Wildwuchs an Programmiersprachen Features ist seit 10-20 Jahren schon extrem. Programmieren ist für mich ein Hobby, und die vielen Features sind für mich nicht nützlich und verursachen unnötigen Schreibaufwand. Im ASM Output sieht man dann meist auch gar keinen Unterschied, der Compiler optimiert besser als der Anwender.

#7647397
Lesenswert?

Erstmal, danke für das angehängter Benchmark. Das sieht doch recht gut aus.

Udo K. schrieb:

Warum programmierst du nicht C++? Bei deinem Programmierstil wäre das kompatibler, stabiler und lesbarer.

Bevor ich zu C gewechselt bin, hatte ich C++ genutzt. Du hast ja gesehen, was ich mit C anstellen kann, nun stell dir mal vor, was ich mit all den Features von C++ machen würde.

Udo K. schrieb:

Nur versuche dich mal in Leute reinzudenken, die nicht so gut aufgestellt sind oder das als Hobby machen, und die deinen Code verstehen wollen.

Das Projekt von mir war auch nur ein Hobby. Da mache ich es so, wie ich gerade Lust habe.

Udo K. schrieb:

Ich habe nach etlichen unnötigen Hacks deine Hash Map mit MS CL compiliert und mit anderen Suchverfahren verglichen. Die Ergebnisse sind gut, rechtfertigen aber meiner Meinung nach nicht die Beschränkung der Hashfunktion und den Code Bloat.

Nochmal danke dafür. Um den Code Bloat zu reduzieren, und nichts doppelt zu schreiben, inkludiere ich die Dateien eben mehrfach. Damit habe ich dann 110 statt 1200 Zeilen beim Header File. Also ich finde das übersichtlicher, als wenn ich nichts unterteilen und alles in ein File packen würde.


Ich habe das selbe benchmark wie vorher mal mit den C++ set / map Klassen nachgebaut, als Verglich.

gcc, C++ set / map:

1
$ g++ -O2 -Wall -Wextra -pedantic bench.cpp -o bench
2
$ ./bench.cpp
3
set<    uc> add | 10.525s
4
set<    us> add | 29.794s
5
set<     u> add | 87.840s
6
set<    lu> add | 81.112s
7
set<   llu> add | 99.008s
8
set<     z> add | 85.098s
9
set<    u8> add | 11.795s
10
set<   u16> add | 29.618s
11
set<   u32> add | 81.833s
12
set<   u64> add | 81.999s
13
set<     p> add | 80.658s
14
set<__int128_t> add | 114.183s
15

16
map<    uc> add |  9.220s
17
map<    us> add | 28.469s
18
map<     u> add | 107.741s
19
map<    lu> add | 95.344s
20
map<   llu> add | 101.766s
21
map<     z> add | 95.376s
22
map<    u8> add | 10.369s
23
map<   u16> add | 30.314s
24
map<   u32> add | 102.433s
25
map<   u64> add | 93.427s
26
map<     p> add | 104.723s
27
map<__int128_t> add | 88.617s

clang, C++ set / map:

1
set<    uc> add | 11.323s
2
set<    us> add | 27.782s
3
set<     u> add | 117.430s
4
set<    lu> add | 110.283s
5
set<   llu> add | 111.867s
6
set<     z> add | 112.654s
7
set<    u8> add | 12.415s
8
set<   u16> add | 27.915s
9
set<   u32> add | 109.823s
10
set<   u64> add | 111.677s
11
set<     p> add | 112.676s
12
set<__int128_t> add | 133.795s
13

14
map<    uc> add | 13.560s
15
map<    us> add | 36.143s
16
map<     u> add | 118.259s
17
map<    lu> add | 120.783s
18
map<   llu> add | 120.638s
19
map<     z> add | 121.839s
20
map<    u8> add | 13.491s
21
map<   u16> add | 38.620s
22
map<   u32> add | 119.589s
23
map<   u64> add | 120.836s
24
map<     p> add | 114.451s
25
map<__int128_t> add | 135.914s

gcc, Mein set / map (selber PC, aber ein anderer als gestern abend):

1
             Function |   total |    work |  resize
2
     dpa_u_set_uc_add |  0.864s |  0.864s |  0.000s
3
     dpa_u_set_us_add |  0.956s |  0.921s |  0.035s
4
      dpa_u_set_u_add |  7.135s |  6.784s |  0.351s
5
     dpa_u_set_lu_add | 21.386s | 18.767s |  2.619s
6
    dpa_u_set_llu_add | 24.449s | 21.062s |  3.387s
7
      dpa_u_set_z_add | 24.034s | 21.085s |  2.950s
8
     dpa_u_set_u8_add |  1.428s |  1.428s |  0.000s
9
    dpa_u_set_u16_add |  1.533s |  1.476s |  0.057s
10
    dpa_u_set_u32_add | 11.239s | 10.534s |  0.705s
11
    dpa_u_set_u64_add | 19.974s | 17.449s |  2.526s
12
   dpa_u_set_u128_add | 46.393s | 40.270s |  6.123s
13
dpa_u_set_pointer_add | 29.691s | 25.702s |  3.989s
14

15
             Function |   total |    work |  resize
16
     dpa_u_map_uc_set |  7.148s |  4.891s |  2.257s
17
     dpa_u_map_us_set |  5.566s |  4.613s |  0.953s
18
      dpa_u_map_u_set | 37.319s | 34.401s |  3.918s
19
     dpa_u_map_lu_set | 40.535s | 35.293s |  5.242s
20
    dpa_u_map_llu_set | 38.812s | 32.829s |  5.983s
21
      dpa_u_map_z_set | 39.403s | 33.734s |  5.669s
22
     dpa_u_map_u8_set |  7.576s |  5.140s |  2.436s
23
    dpa_u_map_u16_set |  5.917s |  4.898s |  1.019s
24
    dpa_u_map_u32_set | 37.992s | 34.297s |  3.694s
25
    dpa_u_map_u64_set | 36.835s | 31.862s |  4.973s
26
   dpa_u_map_u128_set | 60.030s | 52.256s |  7.774s
27
dpa_u_map_pointer_set | 43.443s | 36.785s |  6.659s

Das war auf Debian mit g++. Meine Implementierung ist rund 2-12x schneller! Zumindest beim Hinzufügen, und bei grossen Datenmengen.

Was mich beim std::set und std::map erstaunt, ist, dass die Grösse des Keys nicht so starken Einfluss auf die Performance hat, wie bei meinem Hash Set / Hash Map. Und auch der Unterschied zwischen der Performance des Maps und des Sets ist bei der STL geringer. Ich frage mich, woran das liegen könnte. Vielleicht hätte ich key & value ins selbe Array packen sollen, statt in separate?

Wie dem auch sei, an den kleinen Datentypen (unsigned char / short / uint16_t, usw.), sieht man, dass sich die Spezialisierung da durchaus gelohnt hat, nicht nur zum sparen von Speicherplatz, auch Geschwindigkeitsmässig.

Angehängte Dateien:
#7647482
Lesenswert?

Momentan schreibe ich mir eine Utility Library. Ich packe da alles rein, was bei anderen / zukünftigen Projekten nützlich sein könnte.
Früher habe ich immer mal wieder Code zwischen Projekten herum kopiert. Jetzt mach ich das einmal richtig, so dass ich all den Kram an einem Ort zentral habe.

Memory Management primitive, Refcounting, Mathe Funktionen, UTF-8 Hilfsfunktionen, diverse Hash Maps & Sets für diverse unterschiedliche use-cases, übliche Macros (z.B. container_of), etc.

Es ist nicht so, das ich es für eine konkrete Anwendung brauchen würde. Es muss eigentlich auch nicht besser sein als bestehendes, auch wenn es erfreulich ist, wenn es gut performt.

Das wird quasi meine kleine persönliche Bastelkiste. Eine kleine Welt, wo alles so läuft, wie mir das gefällt, und wo alles drin ist, was ich so brauche.

Momentan fehlt noch so einiges, aber ich komme doch recht gut voran.

Gast #7647563
Lesenswert?

Daniel A. schrieb:

Was mich beim std::set und std::map erstaunt, ist, dass die Grösse des Keys nicht so starken Einfluss auf die Performance hat, wie bei meinem Hash Set / Hash Map. Und auch der Unterschied zwischen der Performance des Maps und des Sets ist bei der STL geringer. Ich frage mich, woran das liegen könnte. Vielleicht hätte ich key & value ins selbe Array packen sollen, statt in separate?

Hier unter Windows 10 ist bei deinem Test map schneller als set - interessant. Alles ist ziemlich unabhängig von der Grösse der Keys. Der Test ist aber ziemlich uninteressant. Er testet ja nur das Einfügen. In praktisch allen Fällen ist die Geschwindigkeit beim Suchen interessant. Gibt es einen Grund dass du das nicht testest? Ein realistisches Szenario addiert immer wieder "Sets" von einigen 100-1000 Einträgen, sucht dann wiederholt, entfernt Sets etc. etc. Und das über zumindest 15-60 Minuten. Da kann es zu Fragmentierung und lokalen Anhäufungen in der Map kommen. Sieht man im angehängten Bild: Über die Zeit ändern sich die Kollisionen, und die Zugriffszeiten. Eine mittelmässige Hash Map degeneriert da mit der Zeit, eine gute hält die Performance.

Das es kaum Unterschiede bei der Grösse der Keys gibt klingt erst mal vernünftig, da die Zeit vom Lesen einer Cache Line (32 Byte) bestimmt wird. Die Einträge liegen ja zufällig im Speicher. Der hohe Speicherverbrauch der C++ Container dürfte auch an der mittelmässigen Performance in meinen Test weiter oben Schuld sein.

PS: Beim Testen sollte in der inneren Schleife kein Funktionsaufruf drinnen sein, das verhindert Optimierungen, und hier geht es um Nanosekunden.

Angehängte Dateien:
#7647573
Lesenswert?

Udo K. schrieb:

Er testet ja nur das Einfügen. In praktisch allen Fällen ist die Geschwindigkeit beim Suchen interessant. Gibt es einen Grund dass du das nicht testest?

Das Einfügen zu testen war am einfachsten. Und wenn das Einfügen schnell ist, sollte es auch der Lookup sein, denn ich mache beim Einfügen immer zuerst einen Lookup, um die Position raus zu kriegen, wo der Eintrag ist oder hin soll.

Gast #7649253
Lesenswert?

Compiliert habe ich mit einem aktuellen cl:

1
cl -nologo -W0 -std:c17 -O2 -GS- -Zp8 -GL -c dpa/set-and-map.c

Ich konnte set-and-map.c nicht als C++ Source compilieren wegen zig kleiner Fehler (z.B. Rückgabewert von malloc muss immer gecasted werden und das "restrict" keyword geht nicht).

Ich habe daher set-and-map.c als C File mit -O2 und Codegenerierung zur Linkzeit compiliert (-GL), und dann das obj File mit dem C++ Testprogram gelinkt. Meiner Erfahrung nach geht das verdammt gut, und macht inline Funktionen überflüssig.

Ich verwenden in den Tests eine Tabelle mit LINE Strukturen von 32 Bytes:

1
typedef struct line
2
{
3
    AVLNODE     node;                   /* 16 Bytes */
4
#if STRING_KEYS
5
    char        id[DATA_SIZE-16];       /* string key */
6
#else
7
    key_t       id;                     /* integer key */
8
#endif
9
#if FILL_DATA > 0
10
    BYTE        fill_data[FILL_DATA];
11
#endif
12
} LINE;

Kann ich deine DPA set (nicht map) Funktionen mit dem integer key in den Strukturen verwenden (mit offset=16)? So wie ich das verstehe wird bei den set Funktionen kein separater key abgespeichert, da der key in den Daten enthalten ist, die set Funktion könnte damit schneller sein.

#7649509
Lesenswert?

Ich weiss jetzt nicht genau, was du vor hast. Den Key selbst kannst du schon zum Set hinzufügen | Abfragen | Löschen.
Du kannst auch einen Pointer auf die Struktur darin speichern. Der wird dann aber nicht dereferenziert, der Pointer selbst wäre dann der Key, nicht das, auf das er zeigt.

Gast #7649518
Lesenswert?

Daniel A. schrieb:

Ich weiss jetzt nicht genau, was du vor hast. Den Key selbst kannst du schon zum Set hinzufügen | Abfragen | Löschen. Du kannst auch einen Pointer auf die Struktur darin speichern. Der wird dann aber nicht dereferenziert, der Pointer selbst wäre dann der Key, nicht das, auf das er zeigt.

Der Key ist in den Daten enthalten. Die Hash Map soll einen Zeiger auf die Daten speichern, und muss wissen dass der Key an einem Offset in den Daten drinnen steht. Damit entfällt das getrennte Speichern von Key und Daten.

#7649590
Lesenswert?

Das habe ich noch nicht implementiert. Habe ich eigentlich auch nicht vor.
Das würde sicher etwas Speicherplatz sparen, aber ich bezweifle, dass das schneller wäre. Der Lookup ist momentan (für die meisten Datentypen) 2 Vergleiche:

1
  while(1){
2
    const T ekey = that->key_list[i];
3
    T psl_b = ekey - ((T)i<<shift);
4
    if(psl_b == psl_a)
5
      return (struct lookup_result){i, true};
6
    if(psl_a <= (T)(psl_b-1)) // Das -1 ist ein hack, psl_b == 0 wäre ein leerer Eintrag.
7
      return (struct lookup_result){i, false};
8
    psl_a -= I;
9
    i = (i+1) & mask;
10
  }

Wobei, eigentlich könnte ich den Vergleich psl_b == psl_a aus der Schleife raus nehmen. Muss ich später mal machen.

Jedenfalls, momentan sind das ein paar simple Vergleiche aufeinanderfolgender Einträge. Ich hab die Hashs, und vergleiche sie.
Würde ich stattdessen einen Pointer auf den Key Speichern, müsste ich zusätzlich den Pointer dereferenzieren, und den key Hashen, bevor ich den Vergleich machen kann. Das sind mehr Operationen, und den Cash würde es vermutlich auch nicht freuen.

Beitrag #7649890 wurde von einem Moderator gelöscht.
Gast #7649895
Lesenswert?

Daniel A. schrieb:

Das habe ich noch nicht implementiert. Habe ich eigentlich auch nicht vor.

Schade, das ist ein ziemlich häufiger Anwendungsfall.

Mir ist aufgefallen das der Rückgabewert ein struct ist (dpa_u_optional_pointer_t). Das ist suboptimal. Erstens schmeisst du ein wertvolles Register weg (rax ist unbenutzt, struct werden in rcx als Zeiger übergeben), zweitens kostet der Aufruf der Suchfunktion Performance, weil erst die Rückgabewerte auf den Stack kopiert werden muss. Ich kann nicht einfach den Rückgabewert auf NULL vergleichen, sondern muss den "present" des dpa_u_optional_pointer_t Eintrag auf dem Stack abfragen, und wenn der true ist, muss ich mir den Zeiger Wert vom Stack holen. Deine Funktion ist damit auch die einzige aller Suchfunktionen, die Optimierung des Programms zur Link Zeit braucht, um auf eine passable Performance zu kommen. Ich nehme an, dass der Optimizer diese komplizierten Rückgabewerte zur Linkzeit auflöst. Im Anhang die Performance mit O1, O2 und O2 mit globaler Link-Zeit Optimierung.

Angehängte Dateien:
#7649933
Lesenswert?

Udo K. schrieb:

Mir ist aufgefallen das der Rückgabewert ein struct ist (dpa_u_optional_pointer_t). Das ist suboptimal.

Oh. Das das etwas ausmachen könnte, daran hatte ich noch überhaupt nicht gedacht. Der Grund für das dpa_u_optional_pointer_t war eigentlich nur, dass die Funktion dann als [[reproducible]], also ohne Seiteneffekte, markiert werden kann.
Wobei, das einzige was das bringt, ist das wenn man sowas macht:

1
if(dpa_u_map_u_get(&map, 123).present)
2
  puts(dpa_u_map_u_get(&map, 123).value);

Dann können, zumindest gcc und clang, den zweiten Aufruf von dpa_u_map_u_get weg optimieren.

Muss ich mir mal genauer ansehen, ist ja einfach anzupassen. Vielleicht könnte ich auch eine inline Wrapper Funktion machen, um das beste beider Welten zu bekommen. Der Compiler könnte die dann weg optimieren, und ich könnte ihm immer noch mitteilen, das es keine Seiteneffekte gibt. (Wenn ich den Wert per Pointer zurückgebe, ist das ein Seiteneffekt, und mit reproducible würde der Compiler dann annehmen, dass der Wert nicht geändert wurde, daher kann ich nicht beides direkt machen).

https://en.cppreference.com/w/c/language/attributes/unsequenced

Gast #7650363
Lesenswert?

Ich habe dein set-and-map.c mit einem aktuellen msys2 gcc compiliert und zum Testprogram gelinkt. Die Laufzeit ist marginal schlechter, die Exe ist aber von 30k auf 120k gewachsen, bringt also nichts. Was du probieren kannst, ist deine Funktionen C++ kompatibel zu machen. Dann kann ich das ganze set-and-map File in das Testprogram inkludieren, oder du packst alles mit inline in den Header. Meiner Meinung nach ist die Aufrufkonvention aber nicht ganz optimal, sonst würde das Link-Time optimieren nichts mehr bringen. Ohne die Link-Time Optimierung ist eine einfache binäre Suche mit 21 Zeilen Sourceode bis 10000 Elemente schneller als deine Hashtabelle. Unter 30 Elemente hast du das langsamste Suchverfahren erfunden. Das nenne ich suboptimal. Die neueren Tests liefen auf einem Laptop, die ersten Tests mit besseren Ergebnissen auf einem schnelleren Rechner mit Core i9 und sehr schnellem RAM.

Gast #7650425
Lesenswert?

Daniel A. schrieb:

Unter 30 Elemente hast du das langsamste Suchverfahren erfunden.

Es ist immer noch normales Robin Hood Hashing, der Algorithmus ist nicht das Problem. Ich muss nur die Implementierung noch etwas verbessern.

Das ist halt der Unterschied zwischen Theorie und Praxis 😇

Wenn du neue Ideen hast, probiere ich es gerne aus. Die Sourcen vom Testprogram kann ich bei Bedarf rausrücken, aber das läuft nur unter Windows und war ein schneller Wochenend-Hack, der mit der Zeit noch aufgebohrt worden ist.

Gruss & gutes Gelingen, Udo

Gast #7650798
Lesenswert?

Im Anhang die Sourcen. Ich habe mal versucht den Win spezifischen Teil rauszunehmen. Da musst du sicher noch was anpassen damit der gcc glücklich ist. Für die Zeitmessung verwende ich den NT Performance Counter, da müsstest du clock() oder was unter Linux für genaue Zeitmessung verwendet wird, einsetzen. Wenn du Zeiten unter 1ns messen willst, dann spielt auch Alignment und Layout der Funktionen eine Rolle. Da musst du probieren, falls dir das wichtig ist. Aufrufen geht mit: build.sh -> run4.sh -> run4.png

Angehängte Dateien:
#7651236
Lesenswert?

Danke, mittlerweile habe ich es kompiliert bekommen. Für die PerfCountStamp muss ich mir vermutlich noch was anderes suchen als clock(), das ist vermutlich nicht genau genug, aber ich habe schon mal einen Output. Im Anhang noch die Dateien, die ich verändert habe, und das Diagramm.

Edit: Ich sehe gerade, die Bezeichnungen im Diagramm muss ich noch anpassen. War mit "gcc (Debian 12.2.0-14) 12.2.0", auf einem "12th Gen Intel(R) Core(TM) i5-1235U", Optionen -O2.

Angehängte Dateien:
#7651878
Lesenswert?

Zur Zeitmessung verwende ich nun clock_gettime(CLOCK_PROCESS_CPUTIME_ID,...).

Ich konnte es nun auch noch ein wenig Optimieren, indem ich einen Vergleich in der Lookup Funktion aus der Schleife raus nahm, die Lookup Funktion geinlined habe, und noch an einer Stelle mit der Branch Prediction nachgeholfen habe. Vermutlich lässt sich da noch mehr raus holen, ich könnte z.B. bei (that->key_list[i>>shift])-i-1 das -i-1 schon beim Insert machen, dann wäre in der Schleife nur noch ein Vergleich, und der shift würde auch noch wegfallen.

Ich weiss nicht, warum es bei dir im Vergleich bei msvc so viel schlechter abschliesst. Vielleicht ist das Windows ABI da einfach ineffizienter oder so.

PS: -flto macht bei mir keinen unterschied. Wird hier sogar eher schlechter damit.

Angehängte Dateien:

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren