Die Hopscotch-Map-Bibliothek ist eine C++-Implementierung einer schnellen Hash-Map und eines Hash-Sets, die offene Adressierung und Hopscotch-Hashing zur Auflösung von Kollisionen verwendet. Es handelt sich um eine Cache-freundliche Datenstruktur, die in den meisten Fällen eine bessere Leistung als std::unordered_map
bietet und google::dense_hash_map
sehr ähnlich ist, jedoch weniger Speicher verbraucht und mehr Funktionalitäten bietet.
Die Bibliothek stellt die folgenden Hauptklassen bereit: tsl::hopscotch_map
, tsl::hopscotch_set
, tsl::hopscotch_pg_map
und tsl::hopscotch_pg_set
. Die ersten beiden sind schneller und nutzen eine Zweierpotenz-Wachstumspolitik, die letzten beiden nutzen stattdessen eine Prime-Wachstumspolitik und kommen besser mit einer schlechten Hash-Funktion zurecht. Verwenden Sie die Prime-Version, wenn die Möglichkeit besteht, dass sich Muster in den unteren Bits Ihres Hashs wiederholen (z. B. wenn Sie Zeiger mit einer Identitäts-Hash-Funktion speichern). Weitere Informationen finden Sie unter GrowthPolicy.
Zusätzlich zu diesen Klassen stellt die Bibliothek auch tsl::bhopscotch_map
, tsl::bhopscotch_set
, tsl::bhopscotch_pg_map
und tsl::bhopscotch_pg_set
bereit. Diese Klassen stellen eine zusätzliche Anforderung an den Schlüssel, er muss LessThanComparable
sein, sie bieten jedoch eine bessere asymptotische Obergrenze, siehe Details im Beispiel. Wenn Sie jedoch keine besonderen Anforderungen haben (Risiko von Hash-DoS-Angriffen), sollten tsl::hopscotch_map
und tsl::hopscotch_set
in den meisten Fällen ausreichend sein und Ihre Standardauswahl sein, da sie im Allgemeinen eine bessere Leistung erbringen.
Eine Übersicht über Hopscotch-Hashing und einige Implementierungsdetails finden Sie hier.
Ein Benchmark von tsl::hopscotch_map
im Vergleich zu anderen Hash-Maps finden Sie hier. Auf dieser Seite finden Sie auch einige Ratschläge dazu, welche Hash-Tabellenstruktur Sie für Ihren Anwendungsfall ausprobieren sollten (nützlich, wenn Sie sich mit den Implementierungen mehrerer Hash-Tabellen im tsl
-Namespace etwas auskennen).
tsl::hopscotch_map
aus CMakeLists.txt verwenden.find
mit einem anderen Typ als Key
ermöglichen (z. B. wenn Sie eine Karte haben, die std::unique_ptr<foo>
als Schlüssel verwendet, können Sie „ foo*
oder std::uintptr_t
als Schlüsselparameter verwenden find
ohne einen std::unique_ptr<foo>
zu konstruieren, siehe Beispiel).precalculated_hash
in der API).tsl::bhopscotch_map
und tsl::bhopscotch_set
bieten einen Worst-Case von O(log n) bei Suchvorgängen und Löschungen, wodurch diese Klassen resistent gegen Hash-Tabellen-Deny-of-Service-Angriffe (DoS) sind (siehe Details im Beispiel).-fno-exceptions
in Clang und GCC, ohne /EH
-Option in MSVC oder einfach durch Definition TSL_NO_EXCEPTIONS
). std::terminate
wird als Ersatz für die throw
-Anweisung verwendet, wenn Ausnahmen deaktiviert sind.std::unordered_map
und std::unordered_set
sehr ähnlich ist.std::unordered_map
tsl::hopscotch_map
versucht, eine ähnliche Schnittstelle wie std::unordered_map
zu haben, es bestehen jedoch einige Unterschiede.
erase
, alle Iteratoren ungültig (Einzelheiten finden Sie in der API).operator*()
und operator->()
eine Referenz und einen Zeiger auf const std::pair<Key, T>
anstelle von std::pair<const Key, T>
zurück, sodass der Wert T
nicht änderbar ist. Um den Wert zu ändern, müssen Sie die value()
Methode des Iterators aufrufen, um eine veränderbare Referenz zu erhalten. Beispiel: tsl::hopscotch_map< int , int > map = {{ 1 , 1 }, { 2 , 1 }, { 3 , 1 }};
for ( auto it = map.begin(); it != map.end(); ++it) {
// it->second = 2; // Illegal
it. value () = 2 ; // Ok
}
bucket_size
, bucket
, ...). Diese Unterschiede gelten auch zwischen std::unordered_set
und tsl::hopscotch_set
.
Thread-Sicherheits- und Ausnahmegarantien sind die gleichen wie bei std::unordered_map/set
(dh es ist möglich, mehrere Leser ohne Schreiber zu haben).
Die Bibliothek unterstützt mehrere Wachstumsrichtlinien über den Vorlagenparameter GrowthPolicy
. Die Bibliothek stellt drei Richtlinien zur Verfügung, Sie können jedoch bei Bedarf problemlos Ihre eigenen implementieren.
tsl::(b)hopscotch_map/set
verwendete Standardrichtlinie. Diese Richtlinie hält die Größe des Bucket-Arrays der Hash-Tabelle auf eine Zweierpotenz. Diese Einschränkung ermöglicht es der Richtlinie, die Verwendung der langsamen Modulo-Operation zum Zuordnen eines Hashs zu einem Bucket zu vermeiden. Statt hash % 2 n
verwendet sie hash & (2 n - 1)
(siehe schnelles Modulo). Schnell, aber dies kann zu vielen Kollisionen mit einer schlechten Hash-Funktion führen, da das Modulo mit einer Zweierpotenz am Ende nur die höchstwertigen Bits maskiert.tsl::(b)hopscotch_pg_map/set
verwendete Standardrichtlinie. Die Richtlinie hält die Größe des Bucket-Arrays der Hash-Tabelle auf einer Primzahl. Wenn Sie einen Hash einem Bucket zuordnen, führt die Verwendung einer Primzahl als Modulo zu einer besseren Verteilung der Hashes über die Buckets, selbst bei einer schlechten Hash-Funktion. Damit der Compiler die Modulo-Operation optimieren kann, verwendet die Richtlinie eine Nachschlagetabelle mit konstanten Primzahlen-Modulos (Einzelheiten siehe API). Langsamer als tsl::hh::power_of_two_growth_policy
, aber sicherer. Wenn die Leistung schlecht ist, überprüfen Sie overflow_size()
. Wenn es nicht Null ist, kann es zu vielen Hash-Kollisionen kommen. Ändern Sie entweder die Hash-Funktion für etwas Einheitlicheres oder versuchen Sie es mit einer anderen Wachstumsrichtlinie (hauptsächlich tsl::hh::prime_growth_policy
). Leider ist es manchmal schwierig, sich vor Kollisionen (z. B. DoS-Angriff auf die Hash-Map) zu schützen. Überprüfen Sie bei Bedarf auch tsl::bhopscotch_map/set
, das ein Worst-Case-Szenario von O(log n) bei Suchvorgängen anstelle von O(n) bietet, siehe Details im Beispiel.
Um Ihre eigene Richtlinie zu implementieren, müssen Sie die folgende Schnittstelle implementieren.
struct custom_policy {
// Called on hash table construction and rehash, min_bucket_count_in_out is the minimum buckets
// that the hash table needs. The policy can change it to a higher number of buckets if needed
// and the hash table will use this value as bucket count. If 0 bucket is asked, then the value
// must stay at 0.
explicit custom_policy (std:: size_t & min_bucket_count_in_out);
// Return the bucket [0, bucket_count()) to which the hash belongs.
// If bucket_count() is 0, it must always return 0.
std:: size_t bucket_for_hash (std:: size_t hash) const noexcept ;
// Return the number of buckets that should be used on next growth
std:: size_t next_bucket_count () const ;
// Maximum number of buckets supported by the policy
std:: size_t max_bucket_count () const ;
// Reset the growth policy as if the policy was created with a bucket count of 0.
// After a clear, the policy must always return 0 when bucket_for_hash() is called.
void clear () noexcept ;
}
Um hopscotch-map zu verwenden, fügen Sie einfach das Include-Verzeichnis zu Ihrem Include-Pfad hinzu. Es handelt sich um eine reine Header- Bibliothek.
Wenn Sie CMake verwenden, können Sie auch das exportierte Ziel tsl::hopscotch_map
aus CMakeLists.txt mit target_link_libraries
verwenden.
# Example where the hopscotch-map project is stored in a third-party directory
add_subdirectory (third-party/hopscotch-map)
target_link_libraries (your_target PRIVATE tsl::hopscotch_map)
Wenn das Projekt über make install
installiert wurde, können Sie auch find_package(tsl-hopscotch-map REQUIRED)
anstelle von add_subdirectory
verwenden.
Der Code sollte mit jedem C++17-Standard-kompatiblen Compiler funktionieren.
Zum Ausführen der Tests benötigen Sie die Boost-Testbibliothek und CMake.
git clone https://github.com/Tessil/hopscotch-map.git
cd hopscotch-map/tests
mkdir build
cd build
cmake ..
cmake --build .
./tsl_hopscotch_map_tests
Die API finden Sie hier.
Alle Methoden sind noch nicht dokumentiert, aber sie replizieren das Verhalten derjenigen in std::unordered_map
und std::unordered_set
, sofern nicht anders angegeben.
# include < cstdint >
# include < iostream >
# include < string >
# include < tsl/hopscotch_map.h >
# include < tsl/hopscotch_set.h >
int main () {
tsl::hopscotch_map<std::string, int > map = {{ " a " , 1 }, { " b " , 2 }};
map[ " c " ] = 3 ;
map[ " d " ] = 4 ;
map. insert ({ " e " , 5 });
map. erase ( " b " );
for ( auto it = map. begin (); it != map. end (); ++it) {
// it->second += 2; // Not valid.
it. value () += 2 ;
}
// {d, 6} {a, 3} {e, 7} {c, 5}
for ( const auto & key_value : map) {
std::cout << " { " << key_value. first << " , " << key_value. second << " } " << std::endl;
}
if (map. find ( " a " ) != map. end ()) {
std::cout << " Found " a " . " << std::endl;
}
const std:: size_t precalculated_hash = std::hash<std::string>()( " a " );
// If we already know the hash beforehand, we can pass it in parameter to speed-up lookups.
if (map. find ( " a " , precalculated_hash) != map. end ()) {
std::cout << " Found " a " with hash " << precalculated_hash << " . " << std::endl;
}
/*
* Calculating the hash and comparing two std::string may be slow.
* We can store the hash of each std::string in the hash map to make
* the inserts and lookups faster by setting StoreHash to true.
*/
tsl::hopscotch_map<std::string, int , std::hash<std::string>,
std::equal_to<std::string>,
std::allocator<std::pair<std::string, int >>,
30 , true > map2;
map2[ " a " ] = 1 ;
map2[ " b " ] = 2 ;
// {a, 1} {b, 2}
for ( const auto & key_value : map2) {
std::cout << " { " << key_value. first << " , " << key_value. second << " } " << std::endl;
}
tsl::hopscotch_set< int > set;
set. insert ({ 1 , 9 , 0 });
set. insert ({ 2 , - 1 , 9 });
// {0} {1} {2} {9} {-1}
for ( const auto & key : set) {
std::cout << " { " << key << " } " << std::endl;
}
}
Heterogene Überladungen ermöglichen die Verwendung anderer Typen als Key
für Such- und Löschvorgänge, sofern die verwendeten Typen hashbar und mit Key
vergleichbar sind.
Um die heterogenen Überladungen in tsl::hopscotch_map/set
zu aktivieren, muss die qualifizierte ID KeyEqual::is_transparent
gültig sein. Es funktioniert genauso wie für std::map::find
. Sie können entweder std::equal_to<>
verwenden oder Ihr eigenes Funktionsobjekt definieren.
Sowohl KeyEqual
als auch Hash
müssen in der Lage sein, mit den verschiedenen Typen umzugehen.
# include < functional >
# include < iostream >
# include < string >
# include < tsl/hopscotch_map.h >
struct employee {
employee ( int id, std::string name) : m_id(id), m_name(std::move(name)) {
}
// Either we include the comparators in the class and we use `std::equal_to<>`...
friend bool operator ==( const employee& empl, int empl_id) {
return empl. m_id == empl_id;
}
friend bool operator ==( int empl_id, const employee& empl) {
return empl_id == empl. m_id ;
}
friend bool operator ==( const employee& empl1, const employee& empl2) {
return empl1. m_id == empl2. m_id ;
}
int m_id;
std::string m_name;
};
// ... or we implement a separate class to compare employees.
struct equal_employee {
using is_transparent = void ;
bool operator ()( const employee& empl, int empl_id) const {
return empl. m_id == empl_id;
}
bool operator ()( int empl_id, const employee& empl) const {
return empl_id == empl. m_id ;
}
bool operator ()( const employee& empl1, const employee& empl2) const {
return empl1. m_id == empl2. m_id ;
}
};
struct hash_employee {
std:: size_t operator ()( const employee& empl) const {
return std::hash< int >()(empl. m_id );
}
std:: size_t operator ()( int id) const {
return std::hash< int >()(id);
}
};
int main () {
// Use std::equal_to<> which will automatically deduce and forward the parameters
tsl::hopscotch_map<employee, int , hash_employee, std::equal_to<>> map;
map. insert ({ employee ( 1 , " John Doe " ), 2001 });
map. insert ({ employee ( 2 , " Jane Doe " ), 2002 });
map. insert ({ employee ( 3 , " John Smith " ), 2003 });
// John Smith 2003
auto it = map. find ( 3 );
if (it != map. end ()) {
std::cout << it-> first . m_name << " " << it-> second << std::endl;
}
map. erase ( 1 );
// Use a custom KeyEqual which has an is_transparent member type
tsl::hopscotch_map<employee, int , hash_employee, equal_employee> map2;
map2. insert ({ employee ( 4 , " Johnny Doe " ), 2004 });
// 2004
std::cout << map2. at ( 4 ) << std::endl;
}
Zusätzlich zu tsl::hopscotch_map
und tsl::hopscotch_set
bietet die Bibliothek zwei weitere „sichere“ Optionen: tsl::bhopscotch_map
und tsl::bhopscotch_set
(alle mit ihren pg
-Gegenstücken).
Diese beiden Additionen haben im schlimmsten Fall eine asymptotische Komplexität von O(log n) für Suchvorgänge und Löschungen und eine amortisierte Worst-Case-Komplexität von O(log n) für Einfügungen (amortisiert aufgrund der Möglichkeit einer erneuten Aufbereitung, die in O(n) wäre). . Selbst wenn die Hash-Funktion alle Elemente demselben Bucket zuordnet, würde O(log n) immer noch gelten.
Dies bietet Sicherheit gegen Hash-Table-Deny-of-Service-Angriffe (DoS).
Um dies zu erreichen, verwenden die sicheren Versionen einen binären Suchbaum für die überflogenen Elemente (siehe Implementierungsdetails) und benötigen daher die Elemente LessThanComparable
. Es ist ein zusätzlicher Parameter Compare
erforderlich.
# include < chrono >
# include < cstdint >
# include < iostream >
# include < tsl/hopscotch_map.h >
# include < tsl/bhopscotch_map.h >
/*
* Poor hash function which always returns 1 to simulate
* a Deny of Service attack.
*/
struct dos_attack_simulation_hash {
std:: size_t operator ()( int id) const {
return 1 ;
}
};
int main () {
/*
* Slow due to the hash function, insertions are done in O(n).
*/
tsl::hopscotch_map< int , int , dos_attack_simulation_hash> map;
auto start = std::chrono::high_resolution_clock::now ();
for ( int i= 0 ; i < 10000 ; i++) {
map. insert ({i, 0 });
}
auto end = std::chrono::high_resolution_clock::now ();
// 110 ms
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end-start);
std::cout << duration. count () << " ms " << std::endl;
/*
* Faster. Even with the poor hash function, insertions end-up to
* be O(log n) in average (and O(n) when a rehash occurs).
*/
tsl::bhopscotch_map< int , int , dos_attack_simulation_hash> map_secure;
start = std::chrono::high_resolution_clock::now ();
for ( int i= 0 ; i < 10000 ; i++) {
map_secure. insert ({i, 0 });
}
end = std::chrono::high_resolution_clock::now ();
// 2 ms
duration = std::chrono::duration_cast<std::chrono::milliseconds>(end-start);
std::cout << duration. count () << " ms " << std::endl;
}
Der Code ist unter der MIT-Lizenz lizenziert. Weitere Informationen finden Sie in der LICENSE-Datei.