├── .clang-format ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── ExcaliburHash ├── .clang-format ├── CMakeLists.txt ├── ExcaliburHash.h ├── ExcaliburKeyInfo.h └── wyhash.h ├── ExcaliburHashTest01.cpp ├── ExcaliburHashTest02.cpp ├── ExcaliburHashTest03.cpp ├── ExcaliburHashTest04.cpp ├── ExcaliburHashTest05.cpp ├── Images ├── ClearAndInsertRnd.png ├── ClearAndInsertSeq.png ├── CtorDtor.png ├── CtorSingleEmplaceDtor.png ├── InsertAccessWithProbability10.png ├── InsertAccessWithProbability50.png ├── InsertRndAndRemove.png ├── InsertRndClearAndReInsert.png ├── SearchExisting.png ├── SearchNonExisting.png ├── amd_summary.png └── intel_summary.png ├── LICENSE ├── README.md ├── appveyor.yml ├── codecov19.cmd ├── data └── random.bin ├── gen_vs19.cmd ├── gen_vs19_x86.cmd └── gen_vs22.cmd /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | 3 | ColumnLimit: 140 4 | PointerAlignment: Left 5 | BreakBeforeBraces: Allman 6 | BreakConstructorInitializers: BeforeComma 7 | BreakInheritanceList: BeforeComma 8 | IndentWidth: 4 9 | TabWidth: 4 10 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-unix: 7 | strategy: 8 | matrix: 9 | os: [ubuntu, macos] 10 | compiler: [g++, clang++] 11 | defines: [standard] 12 | exclude: 13 | - os: macos 14 | compiler: g++ 15 | name: ${{matrix.os}} ${{matrix.compiler}} 16 | runs-on: ${{matrix.os}}-latest 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Update submodules 20 | run: git submodule update --init --recursive 21 | - name: CMake Configure 22 | run: mkdir build && cd build && cmake .. 23 | - name: Build 24 | run: cd build && cmake --build . --config Release 25 | - name: Run unit tests 26 | run: cd build && ./ExcaliburHashTest 27 | build-windows: 28 | runs-on: windows-latest 29 | strategy: 30 | matrix: 31 | arch: [Win32, x64] 32 | defines: [standard] 33 | steps: 34 | - uses: actions/checkout@v1 35 | - name: Update submodules 36 | run: git submodule update --init --recursive 37 | - name: CMake Configure 38 | run: mkdir build && cd build && cmake .. 39 | - name: Build 40 | run: cd build && cmake --build . --config Release 41 | - name: Run unit tests 42 | run: build/Release/ExcaliburHashTest.exe 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build2019 2 | build2019_32 3 | build2022 4 | build2022_32 5 | build 6 | codecov_report 7 | *.log 8 | 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "extern/googletest"] 2 | path = extern/googletest 3 | url = https://github.com/google/googletest.git 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | # set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "" FORCE) 4 | 5 | # if (MSVC) 6 | # set(CMAKE_VS_GLOBALS "EnableASAN=true") 7 | # endif() 8 | 9 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) 10 | 11 | set(PROJ_NAME ExcaliburHashTest) 12 | project(${PROJ_NAME}) 13 | 14 | set (CMAKE_CXX_STANDARD 17) 15 | 16 | set(TEST_SOURCES 17 | ExcaliburHashTest01.cpp 18 | ExcaliburHashTest02.cpp 19 | ExcaliburHashTest03.cpp 20 | ExcaliburHashTest04.cpp 21 | ExcaliburHashTest05.cpp 22 | ) 23 | 24 | set (TEST_EXE_NAME ${PROJ_NAME}) 25 | add_executable(${TEST_EXE_NAME} ${TEST_SOURCES}) 26 | set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT ${TEST_EXE_NAME}) 27 | 28 | set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) 29 | add_subdirectory("${PROJECT_SOURCE_DIR}/extern/googletest" "extern/googletest") 30 | target_link_libraries(${TEST_EXE_NAME} gtest_main) 31 | 32 | if(MSVC) 33 | target_compile_options(${TEST_EXE_NAME} PRIVATE /W4 /WX) 34 | add_definitions(-D_CRT_SECURE_NO_WARNINGS) 35 | 36 | # target_compile_options(${TEST_EXE_NAME} PRIVATE "$<$:/Zi>") 37 | # target_link_options(${TEST_EXE_NAME} PRIVATE "$<$:/DEBUG>") 38 | # target_link_options(${TEST_EXE_NAME} PRIVATE "$<$:/OPT:REF>") 39 | # target_link_options(${TEST_EXE_NAME} PRIVATE "$<$:/OPT:ICF>") 40 | 41 | else() 42 | target_compile_options(${TEST_EXE_NAME} PRIVATE -Wall -Wextra -pedantic -Werror) 43 | endif() 44 | 45 | # add sm_hash_map 46 | add_subdirectory("${PROJECT_SOURCE_DIR}/ExcaliburHash") 47 | target_link_libraries(${TEST_EXE_NAME} ExcaliburHash) 48 | -------------------------------------------------------------------------------- /ExcaliburHash/.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | 3 | ColumnLimit: 140 4 | PointerAlignment: Left 5 | BreakBeforeBraces: Allman 6 | BreakConstructorInitializers: BeforeComma 7 | BreakInheritanceList: BeforeComma 8 | IndentWidth: 4 9 | TabWidth: 4 10 | IndentPPDirectives: BeforeHash 11 | -------------------------------------------------------------------------------- /ExcaliburHash/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(ExcaliburHash INTERFACE) 2 | target_include_directories(ExcaliburHash INTERFACE ./) 3 | # target_compile_features(ExcaliburHash INTERFACE cxx_std_17) 4 | 5 | -------------------------------------------------------------------------------- /ExcaliburHash/ExcaliburHash.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #if !defined(EXLBR_ALLOC) || !defined(EXLBR_FREE) 9 | #if defined(_WIN32) 10 | // Windows 11 | #include 12 | #define EXLBR_ALLOC(sizeInBytes, alignment) _mm_malloc(sizeInBytes, alignment) 13 | #define EXLBR_FREE(ptr) _mm_free(ptr) 14 | #else 15 | // Posix 16 | #include 17 | #define EXLBR_ALLOC(sizeInBytes, alignment) aligned_alloc(alignment, sizeInBytes) 18 | #define EXLBR_FREE(ptr) free(ptr) 19 | #endif 20 | #endif 21 | 22 | #if !defined(EXLBR_ASSERT) 23 | #include 24 | #define EXLBR_ASSERT(expression) assert(expression) 25 | /* 26 | #define EXLBR_ASSERT(expr) \ 27 | do \ 28 | { \ 29 | if (!(expr)) \ 30 | _CrtDbgBreak(); \ 31 | } while (0) 32 | */ 33 | #endif 34 | 35 | #if !defined(EXLBR_RESTRICT) 36 | #define EXLBR_RESTRICT __restrict 37 | #endif 38 | 39 | #if defined(__GNUC__) 40 | #if !defined(EXLBR_LIKELY) 41 | #define EXLBR_LIKELY(x) __builtin_expect(!!(x), 1) 42 | #endif 43 | #if !defined(EXLBR_UNLIKELY) 44 | #define EXLBR_UNLIKELY(x) __builtin_expect(!!(x), 0) 45 | #endif 46 | #else 47 | #if !defined(EXLBR_LIKELY) 48 | #define EXLBR_LIKELY(x) (x) 49 | #endif 50 | #if !defined(EXLBR_UNLIKELY) 51 | #define EXLBR_UNLIKELY(x) (x) 52 | #endif 53 | #endif 54 | 55 | #include 56 | 57 | namespace Excalibur 58 | { 59 | 60 | /* 61 | 62 | TODO: Description 63 | 64 | TODO: Design descisions/principles 65 | 66 | TODO: Memory layout 67 | 68 | */ 69 | template > class HashTable 70 | { 71 | struct has_values : std::bool_constant::type>::value> 72 | { 73 | }; 74 | 75 | static inline constexpr uint32_t k_MinNumberOfBuckets = 16; 76 | 77 | template static void construct(void* EXLBR_RESTRICT ptr, Args&&... args) 78 | { 79 | new (ptr) T(std::forward(args)...); 80 | } 81 | template static void destruct(T* EXLBR_RESTRICT ptr) { ptr->~T(); } 82 | 83 | template struct Storage 84 | { 85 | }; 86 | 87 | template struct Storage 88 | { 89 | struct TItem 90 | { 91 | using TValueStorage = typename std::aligned_storage::type; 92 | TKey m_key; 93 | TValueStorage m_value; 94 | 95 | inline TItem(TKey&& other) noexcept 96 | : m_key(std::move(other)) 97 | { 98 | } 99 | [[nodiscard]] inline bool isValid() const noexcept { return TKeyInfo::isValid(m_key); } 100 | [[nodiscard]] inline bool isEmpty() const noexcept { return TKeyInfo::isEqual(TKeyInfo::getEmpty(), m_key); } 101 | [[nodiscard]] inline bool isTombstone() const noexcept { return TKeyInfo::isEqual(TKeyInfo::getTombstone(), m_key); } 102 | [[nodiscard]] inline bool isEqual(const TKey& key) const noexcept { return TKeyInfo::isEqual(key, m_key); } 103 | 104 | [[nodiscard]] inline TKey* key() noexcept { return &m_key; } 105 | [[nodiscard]] inline TValue* value() noexcept 106 | { 107 | TValue* value = reinterpret_cast(&m_value); 108 | return value; 109 | } 110 | }; 111 | }; 112 | 113 | template struct Storage 114 | { 115 | struct TItem 116 | { 117 | TKey m_key; 118 | 119 | inline TItem(TKey&& other) noexcept 120 | : m_key(std::move(other)) 121 | { 122 | } 123 | [[nodiscard]] inline bool isValid() const noexcept { return TKeyInfo::isValid(m_key); } 124 | [[nodiscard]] inline bool isEmpty() const noexcept { return TKeyInfo::isEqual(TKeyInfo::getEmpty(), m_key); } 125 | [[nodiscard]] inline bool isTombstone() const noexcept { return TKeyInfo::isEqual(TKeyInfo::getTombstone(), m_key); } 126 | [[nodiscard]] inline bool isEqual(const TKey& key) const noexcept { return TKeyInfo::isEqual(key, m_key); } 127 | 128 | [[nodiscard]] inline TKey* key() noexcept { return &m_key; } 129 | // inline TValue* value() noexcept{ return nullptr; } 130 | }; 131 | }; 132 | 133 | using TItem = typename Storage::TItem; 134 | 135 | template static inline T shr(T v, T shift) noexcept 136 | { 137 | static_assert(std::is_integral::value && std::is_unsigned::value, "Type T should be an integral unsigned type."); 138 | return (v >> shift); 139 | } 140 | 141 | [[nodiscard]] inline uint32_t nextPow2(uint32_t v) noexcept 142 | { 143 | EXLBR_ASSERT(v != 0); 144 | v--; 145 | v |= v >> 1; 146 | v |= v >> 2; 147 | v |= v >> 4; 148 | v |= v >> 8; 149 | v |= v >> 16; 150 | v++; 151 | return v; 152 | } 153 | 154 | [[nodiscard]] static inline size_t align(size_t cursor, size_t alignment) noexcept 155 | { 156 | return (cursor + (alignment - 1)) & ~(alignment - 1); 157 | } 158 | [[nodiscard]] static inline bool isPointerAligned(void* cursor, size_t alignment) noexcept 159 | { 160 | return (uintptr_t(cursor) & (alignment - 1)) == 0; 161 | } 162 | 163 | template struct IteratorHelper 164 | { 165 | [[nodiscard]] static TIterator begin(const HashTable& ht) noexcept 166 | { 167 | if (ht.empty()) 168 | { 169 | return end(ht); 170 | } 171 | 172 | TItem* EXLBR_RESTRICT item = ht.m_storage; 173 | while (true) 174 | { 175 | if (item->isValid()) 176 | { 177 | return TIterator(&ht, item); 178 | } 179 | item++; 180 | } 181 | } 182 | 183 | [[nodiscard]] static TIterator end(const HashTable& ht) noexcept 184 | { 185 | const uint32_t numBuckets = ht.m_numBuckets; 186 | TItem* const endItem = ht.m_storage + numBuckets; 187 | return TIterator(&ht, endItem); 188 | } 189 | }; 190 | 191 | inline void moveFrom(HashTable&& other) 192 | { 193 | // note: the current hash table is supposed to be destroyed/non-initialized 194 | if (!other.isUsingInlineStorage()) 195 | { 196 | // if we are not using inline storage than it's a simple pointer swap 197 | constructInline(TKeyInfo::getEmpty()); 198 | m_storage = other.m_storage; 199 | m_numBuckets = other.m_numBuckets; 200 | m_numElements = other.m_numElements; 201 | other.m_storage = nullptr; 202 | // don't need to zero rest of the members because dtor doesn't use them 203 | // other.m_numBuckets = 0; 204 | // other.m_numElements = 0; 205 | } 206 | else 207 | { 208 | // if using inline storage than let's move items from one inline storage into another 209 | TItem* otherInlineItems = reinterpret_cast(&other.m_inlineStorage); 210 | TItem* inlineItems = moveInline(otherInlineItems); 211 | 212 | m_storage = inlineItems; 213 | m_numBuckets = other.m_numBuckets; 214 | m_numElements = other.m_numElements; 215 | // note: other's online items will be destroyed automatically when its dtor called 216 | // other.m_storage = nullptr; 217 | // other.m_numBuckets = 0; 218 | // other.m_numElements = 0; 219 | } 220 | } 221 | 222 | inline void copyFrom(const HashTable& other) 223 | { 224 | if (other.empty()) 225 | { 226 | return; 227 | } 228 | 229 | const uint32_t numBuckets = other.m_numBuckets; 230 | TItem* EXLBR_RESTRICT item = other.m_storage; 231 | TItem* const enditem = item + numBuckets; 232 | for (; item != enditem; item++) 233 | { 234 | if (item->isValid()) 235 | { 236 | if constexpr (has_values::value) 237 | { 238 | emplace(*item->key(), *item->value()); 239 | } 240 | else 241 | { 242 | emplace(*item->key()); 243 | } 244 | } 245 | } 246 | } 247 | 248 | [[nodiscard]] inline bool isUsingInlineStorage() const noexcept 249 | { 250 | const TItem* inlineStorage = reinterpret_cast(&m_inlineStorage); 251 | return (inlineStorage == m_storage); 252 | } 253 | 254 | template inline TItem* constructInline(Args&&... args) 255 | { 256 | TItem* inlineItems = reinterpret_cast(&m_inlineStorage); 257 | for (unsigned i = 0; i < kNumInlineItems; i++) 258 | { 259 | construct((inlineItems + i), std::forward(args)...); 260 | } 261 | return inlineItems; 262 | } 263 | 264 | inline TItem* moveInline(TItem* otherInlineItems) 265 | { 266 | TItem* inlineItems = reinterpret_cast(&m_inlineStorage); 267 | 268 | if constexpr (has_values::value) 269 | { 270 | // move all keys and valid values 271 | for (unsigned i = 0; i < kNumInlineItems; i++) 272 | { 273 | TItem* inlineItem = (inlineItems + i); 274 | TItem* otherInlineItem = (otherInlineItems + i); 275 | const bool hasValidValue = otherInlineItem->isValid(); 276 | // move construct key 277 | construct((inlineItems + i), std::move(*otherInlineItem->key())); 278 | 279 | // move inline storage value (if any) 280 | if (hasValidValue) 281 | { 282 | TValue* value = inlineItem->value(); 283 | TValue* otherValue = otherInlineItem->value(); 284 | // move construct value 285 | construct(value, std::move(*otherValue)); 286 | } 287 | } 288 | } 289 | else 290 | { 291 | // move only keys 292 | for (unsigned i = 0; i < kNumInlineItems; i++) 293 | { 294 | // move construct key 295 | construct((inlineItems + i), std::move(*(otherInlineItems + i)->key())); 296 | } 297 | } 298 | 299 | return inlineItems; 300 | } 301 | 302 | inline uint32_t create(uint32_t numBuckets) 303 | { 304 | numBuckets = (numBuckets < k_MinNumberOfBuckets) ? k_MinNumberOfBuckets : numBuckets; 305 | 306 | // numBuckets has to be power-of-two 307 | EXLBR_ASSERT(numBuckets > 0); 308 | EXLBR_ASSERT((numBuckets & (numBuckets - 1)) == 0); 309 | 310 | size_t numBytes = sizeof(TItem) * numBuckets; 311 | // note: 64 to match CPU cache line size 312 | size_t alignment = std::max(alignof(TItem), size_t(64)); 313 | numBytes = align(numBytes, alignment); 314 | EXLBR_ASSERT((numBytes % alignment) == 0); 315 | 316 | void* raw = EXLBR_ALLOC(numBytes, alignment); 317 | EXLBR_ASSERT(raw); 318 | m_storage = reinterpret_cast(raw); 319 | EXLBR_ASSERT(raw == m_storage); 320 | EXLBR_ASSERT(isPointerAligned(m_storage, alignof(TItem))); 321 | 322 | m_numBuckets = numBuckets; 323 | m_numElements = 0; 324 | 325 | TItem* EXLBR_RESTRICT item = m_storage; 326 | TItem* const endItem = item + numBuckets; 327 | for (; item != endItem; item++) 328 | { 329 | construct(item, TKeyInfo::getEmpty()); 330 | } 331 | 332 | return numBuckets; 333 | } 334 | 335 | inline void destroy() 336 | { 337 | const uint32_t numBuckets = m_numBuckets; 338 | TItem* EXLBR_RESTRICT item = m_storage; 339 | TItem* const endItem = item + numBuckets; 340 | for (; item != endItem; item++) 341 | { 342 | // destroy value if need 343 | if constexpr (!std::is_trivially_destructible::value) 344 | { 345 | if (item->isValid()) 346 | { 347 | destruct(item->value()); 348 | } 349 | } 350 | 351 | // note: this won't automatically call value dtors! 352 | destruct(item); 353 | } 354 | } 355 | 356 | inline void destroyAndFreeMemory() 357 | { 358 | if constexpr (!std::is_trivially_destructible::value || !std::is_trivially_destructible::value) 359 | { 360 | destroy(); 361 | } 362 | 363 | if (!isUsingInlineStorage()) 364 | { 365 | EXLBR_FREE(m_storage); 366 | } 367 | } 368 | 369 | [[nodiscard]] inline TItem* findImpl(const TKey& key) const noexcept 370 | { 371 | EXLBR_ASSERT(!TKeyInfo::isEqual(TKeyInfo::getTombstone(), key)); 372 | EXLBR_ASSERT(!TKeyInfo::isEqual(TKeyInfo::getEmpty(), key)); 373 | const size_t numBuckets = m_numBuckets; 374 | TItem* const firstItem = m_storage; 375 | TItem* const endItem = firstItem + numBuckets; 376 | const size_t hashValue = TKeyInfo::hash(key); 377 | const size_t bucketIndex = hashValue & (numBuckets - 1); 378 | TItem* startItem = firstItem + bucketIndex; 379 | TItem* EXLBR_RESTRICT currentItem = startItem; 380 | do 381 | { 382 | if (EXLBR_LIKELY(currentItem->isEqual(key))) 383 | { 384 | return currentItem; 385 | } 386 | 387 | if (currentItem->isEmpty()) 388 | { 389 | return endItem; 390 | } 391 | 392 | currentItem++; 393 | currentItem = (currentItem == endItem) ? firstItem : currentItem; 394 | } while (currentItem != startItem); 395 | return endItem; 396 | } 397 | 398 | public: 399 | class IteratorBase 400 | { 401 | protected: 402 | [[nodiscard]] inline const TKey* getKey() const noexcept 403 | { 404 | EXLBR_ASSERT(m_item->isValid()); 405 | return m_item->key(); 406 | } 407 | [[nodiscard]] inline const TValue* getValue() const noexcept 408 | { 409 | EXLBR_ASSERT(m_item->isValid()); 410 | return m_item->value(); 411 | } 412 | 413 | static TItem* getNextValidItem(TItem* item, TItem* endItem) noexcept 414 | { 415 | do 416 | { 417 | item++; 418 | } while (item < endItem && !item->isValid()); 419 | return item; 420 | } 421 | 422 | void copyFrom(const IteratorBase& other) 423 | { 424 | m_ht = other.m_ht; 425 | m_item = other.m_item; 426 | } 427 | 428 | public: 429 | IteratorBase() = delete; 430 | 431 | IteratorBase(const IteratorBase& other) noexcept 432 | : m_ht(other.m_ht) 433 | , m_item(other.m_item) 434 | { 435 | } 436 | 437 | IteratorBase(const HashTable* ht, TItem* item) noexcept 438 | : m_ht(ht) 439 | , m_item(item) 440 | { 441 | } 442 | 443 | bool operator==(const IteratorBase& other) const noexcept 444 | { 445 | // note: m_ht comparison is redundant and hence skipped 446 | return m_item == other.m_item; 447 | } 448 | bool operator!=(const IteratorBase& other) const noexcept 449 | { 450 | // note: m_ht comparison is redundant and hence skipped 451 | return m_item != other.m_item; 452 | } 453 | 454 | IteratorBase& operator++() noexcept 455 | { 456 | TItem* endItem = m_ht->m_storage + m_ht->m_numBuckets; 457 | m_item = getNextValidItem(m_item, endItem); 458 | return *this; 459 | } 460 | 461 | IteratorBase operator++(int) noexcept 462 | { 463 | IteratorBase res = *this; 464 | ++*this; 465 | return res; 466 | } 467 | 468 | protected: 469 | const HashTable* m_ht; 470 | TItem* m_item; 471 | friend class HashTable; 472 | }; 473 | 474 | class IteratorK : public IteratorBase 475 | { 476 | public: 477 | IteratorK() = delete; 478 | 479 | IteratorK(const HashTable* ht, TItem* item) noexcept 480 | : IteratorBase(ht, item) 481 | { 482 | } 483 | 484 | [[nodiscard]] inline const TKey& operator*() const noexcept { return *IteratorBase::getKey(); } 485 | [[nodiscard]] inline const TKey* operator->() const noexcept { return IteratorBase::getKey(); } 486 | }; 487 | 488 | template class TIteratorV : public IteratorBase 489 | { 490 | public: 491 | TIteratorV() = delete; 492 | 493 | TIteratorV(const HashTable* ht, TItem* item) noexcept 494 | : IteratorBase(ht, item) 495 | { 496 | } 497 | 498 | [[nodiscard]] inline TIteratorValue& operator*() const noexcept { return *const_cast(IteratorBase::getValue()); } 499 | [[nodiscard]] inline TIteratorValue* operator->() const noexcept { return const_cast(IteratorBase::getValue()); } 500 | }; 501 | 502 | template class TIteratorKV : public IteratorBase 503 | { 504 | public: 505 | // pretty much similar to std::reference_wrapper, but supports late initialization 506 | template struct reference 507 | { 508 | TYPE* ptr = nullptr; 509 | 510 | explicit reference(TYPE* _ptr) noexcept 511 | : ptr(_ptr) 512 | { 513 | } 514 | 515 | reference(const reference&) noexcept = default; 516 | reference(reference&&) noexcept = default; 517 | reference& operator=(const reference&) noexcept = default; 518 | reference& operator=(reference&&) noexcept = default; 519 | void set(TYPE* _ptr) noexcept { ptr = _ptr; } 520 | TYPE& get() const noexcept 521 | { 522 | EXLBR_ASSERT(ptr); 523 | return *ptr; 524 | } 525 | 526 | operator TYPE&() const noexcept { return get(); } 527 | }; 528 | 529 | using KeyValue = std::pair, const reference>; 530 | 531 | private: 532 | void updateTmpKV() const noexcept 533 | { 534 | const reference& refKey = tmpKv.first; 535 | const_cast&>(refKey).set(IteratorBase::getKey()); 536 | const reference& refVal = tmpKv.second; 537 | const_cast&>(refVal).set(const_cast(IteratorBase::getValue())); 538 | } 539 | 540 | public: 541 | TIteratorKV() = delete; 542 | 543 | TIteratorKV(const TIteratorKV& other) 544 | : IteratorBase(other) 545 | , tmpKv(reference(nullptr), reference(nullptr)) 546 | { 547 | } 548 | 549 | TIteratorKV& operator=(const TIteratorKV& other) noexcept 550 | { 551 | IteratorBase::copyFrom(other); 552 | // note: we'll automatically update tmpKv on the next access = no need to copy 553 | return *this; 554 | } 555 | 556 | TIteratorKV(const HashTable* ht, TItem* item) noexcept 557 | : IteratorBase(ht, item) 558 | , tmpKv(reference(nullptr), reference(nullptr)) 559 | { 560 | } 561 | 562 | [[nodiscard]] inline const TKey& key() const noexcept { return *IteratorBase::getKey(); } 563 | [[nodiscard]] inline TIteratorValue& value() const noexcept { return *const_cast(IteratorBase::getValue()); } 564 | [[nodiscard]] inline KeyValue& operator*() const noexcept 565 | { 566 | updateTmpKV(); 567 | return tmpKv; 568 | } 569 | 570 | [[nodiscard]] inline KeyValue* operator->() const noexcept 571 | { 572 | updateTmpKV(); 573 | return &tmpKv; 574 | } 575 | 576 | private: 577 | mutable KeyValue tmpKv; 578 | }; 579 | 580 | using IteratorKV = TIteratorKV; 581 | using ConstIteratorKV = TIteratorKV; 582 | using IteratorV = TIteratorV; 583 | using ConstIteratorV = TIteratorV; 584 | 585 | HashTable() noexcept 586 | //: m_storage(nullptr) 587 | : m_numBuckets(kNumInlineItems) 588 | , m_numElements(0) 589 | { 590 | m_storage = constructInline(TKeyInfo::getEmpty()); 591 | } 592 | 593 | ~HashTable() 594 | { 595 | if (m_storage) 596 | { 597 | destroyAndFreeMemory(); 598 | } 599 | } 600 | 601 | inline void clear() 602 | { 603 | if (empty()) 604 | { 605 | return; 606 | } 607 | 608 | const uint32_t numBuckets = m_numBuckets; 609 | TItem* EXLBR_RESTRICT item = m_storage; 610 | TItem* const endItem = item + numBuckets; 611 | for (; item != endItem; item++) 612 | { 613 | // destroy value if need 614 | if constexpr (!std::is_trivially_destructible::value) 615 | { 616 | if (item->isValid()) 617 | { 618 | destruct(item->value()); 619 | } 620 | } 621 | 622 | // set key to empty 623 | *item->key() = TKeyInfo::getEmpty(); 624 | } 625 | // TODO: shrink if needed? 626 | m_numElements = 0; 627 | } 628 | 629 | private: 630 | template 631 | inline std::pair emplaceToExisting(size_t numBuckets, TK&& key, Args&&... args) 632 | { 633 | // numBuckets has to be power-of-two 634 | EXLBR_ASSERT(numBuckets > 0); 635 | EXLBR_ASSERT((numBuckets & (numBuckets - 1)) == 0); 636 | const size_t hashValue = TKeyInfo::hash(key); 637 | const size_t bucketIndex = hashValue & (numBuckets - 1); 638 | TItem* const firstItem = m_storage; 639 | TItem* const endItem = firstItem + numBuckets; 640 | TItem* EXLBR_RESTRICT currentItem = firstItem + bucketIndex; 641 | TItem* EXLBR_RESTRICT insertItem = nullptr; 642 | 643 | while (true) 644 | { 645 | if (currentItem->isEqual(key)) 646 | { 647 | return std::make_pair(IteratorKV(this, currentItem), false); 648 | } 649 | if (currentItem->isEmpty()) 650 | { 651 | insertItem = ((insertItem == nullptr) ? currentItem : insertItem); 652 | 653 | // move key 654 | *insertItem->key() = std::move(key); 655 | // construct value if need 656 | if constexpr (has_values::value) 657 | { 658 | construct(insertItem->value(), std::forward(args)...); 659 | } 660 | m_numElements++; 661 | return std::make_pair(IteratorKV(this, insertItem), true); 662 | } 663 | if (currentItem->isTombstone() && insertItem == nullptr) 664 | { 665 | insertItem = currentItem; 666 | } 667 | currentItem++; 668 | currentItem = (currentItem == endItem) ? firstItem : currentItem; 669 | } 670 | } 671 | 672 | inline void reinsert(size_t numBucketsNew, TItem* EXLBR_RESTRICT item, TItem* const enditem) noexcept 673 | { 674 | // re-insert existing elements 675 | for (; item != enditem; item++) 676 | { 677 | if (item->isValid()) 678 | { 679 | if constexpr (has_values::value) 680 | { 681 | emplaceToExisting(numBucketsNew, std::move(*item->key()), std::move(*item->value())); 682 | } 683 | else 684 | { 685 | emplaceToExisting(numBucketsNew, std::move(*item->key())); 686 | } 687 | 688 | // destroy old value if need 689 | if constexpr ((!std::is_trivially_destructible::value) && (has_values::value)) 690 | { 691 | destruct(item->value()); 692 | } 693 | } 694 | 695 | // note: this won't automatically call value dtors! 696 | destruct(item); 697 | } 698 | } 699 | 700 | template 701 | inline std::pair emplaceReallocate(uint32_t numBucketsNew, TK&& key, Args&&... args) 702 | { 703 | const uint32_t numBuckets = m_numBuckets; 704 | TItem* storage = m_storage; 705 | TItem* EXLBR_RESTRICT item = storage; 706 | TItem* const enditem = item + numBuckets; 707 | 708 | // check if such element is already exist 709 | // in this case we don't need to do anything 710 | TItem* existingItem = findImpl(key); 711 | if (existingItem != enditem) 712 | { 713 | return std::make_pair(IteratorKV(this, existingItem), false); 714 | } 715 | 716 | bool isInlineStorage = isUsingInlineStorage(); 717 | 718 | numBucketsNew = create(numBucketsNew); 719 | 720 | // 721 | // insert a new element (one of the args might still point to the old storage in case of hash table 'aliasing' 722 | // 723 | // i.e. 724 | // auto it = table.find("key"); 725 | // table.emplace("another_key", it->second); // <--- when hash table grows it->second will point to a memory we are about to free 726 | std::pair it = emplaceToExisting(size_t(numBucketsNew), key, args...); 727 | 728 | reinsert(size_t(numBucketsNew), item, enditem); 729 | 730 | if (!isInlineStorage) 731 | { 732 | EXLBR_FREE(storage); 733 | } 734 | 735 | return it; 736 | } 737 | 738 | public: 739 | template inline std::pair emplace(TK&& key, Args&&... args) 740 | { 741 | static_assert(std::is_same::type>::type>::value, 742 | "Expected unversal reference of TKey type. Wrong key type?"); 743 | 744 | EXLBR_ASSERT(!TKeyInfo::isEqual(TKeyInfo::getTombstone(), key)); 745 | EXLBR_ASSERT(!TKeyInfo::isEqual(TKeyInfo::getEmpty(), key)); 746 | EXLBR_ASSERT(!TKeyInfo::isEqual(TKeyInfo::getEmpty(), TKeyInfo::getTombstone())); 747 | uint32_t numBuckets = m_numBuckets; 748 | 749 | // numBucketsThreshold = (numBuckets * 3/4) (but implemented using bit shifts) 750 | const uint32_t numBucketsThreshold = shr(numBuckets, 1u) + shr(numBuckets, 2u); 751 | if (EXLBR_LIKELY(m_numElements <= numBucketsThreshold)) 752 | { 753 | return emplaceToExisting(numBuckets, key, args...); 754 | } 755 | return emplaceReallocate(numBuckets * 2, key, args...); 756 | } 757 | 758 | [[nodiscard]] inline ConstIteratorKV find(const TKey& key) const noexcept 759 | { 760 | TItem* item = findImpl(key); 761 | return ConstIteratorKV(this, item); 762 | } 763 | [[nodiscard]] inline IteratorKV find(const TKey& key) noexcept 764 | { 765 | TItem* item = findImpl(key); 766 | return IteratorKV(this, item); 767 | } 768 | 769 | inline TItem* eraseImpl(const IteratorBase it) 770 | { 771 | TItem* EXLBR_RESTRICT item = m_storage; 772 | TItem* const endItem = item + m_numBuckets; 773 | 774 | if (it == IteratorHelper::end(*this)) 775 | { 776 | return endItem; 777 | } 778 | 779 | EXLBR_ASSERT(m_numElements != 0); 780 | m_numElements--; 781 | 782 | if constexpr ((!std::is_trivially_destructible::value) && (has_values::value)) 783 | { 784 | TValue* itemValue = const_cast(it.getValue()); 785 | destruct(itemValue); 786 | } 787 | 788 | // hash table now is empty. convert all tombstones to empty keys 789 | if (m_numElements == 0) 790 | { 791 | for (; item != endItem; item++) 792 | { 793 | *item->key() = TKeyInfo::getEmpty(); 794 | } 795 | return endItem; 796 | } 797 | 798 | // overwrite key with empty key 799 | TKey* itemKey = const_cast(it.getKey()); 800 | *itemKey = TKeyInfo::getTombstone(); 801 | return IteratorBase::getNextValidItem(it.m_item, endItem); 802 | } 803 | 804 | inline IteratorKV erase(const IteratorKV& it) 805 | { 806 | TItem* item = eraseImpl(it); 807 | return IteratorKV(this, item); 808 | } 809 | 810 | inline ConstIteratorKV erase(const ConstIteratorKV& it) 811 | { 812 | TItem* item = eraseImpl(it); 813 | return ConstIteratorKV(this, item); 814 | } 815 | 816 | inline bool erase(const TKey& key) 817 | { 818 | auto it = find(key); 819 | erase(it); 820 | return (it != iend()); 821 | } 822 | 823 | inline bool reserve(uint32_t numBucketsNew) 824 | { 825 | if (numBucketsNew == 0 || numBucketsNew < capacity()) 826 | { 827 | return false; 828 | } 829 | numBucketsNew = nextPow2(numBucketsNew); 830 | 831 | const uint32_t numBuckets = m_numBuckets; 832 | TItem* storage = m_storage; 833 | TItem* EXLBR_RESTRICT item = storage; 834 | TItem* const enditem = item + numBuckets; 835 | bool isInlineStorage = isUsingInlineStorage(); 836 | 837 | numBucketsNew = create(numBucketsNew); 838 | 839 | reinsert(numBucketsNew, item, enditem); 840 | 841 | if (!isInlineStorage) 842 | { 843 | EXLBR_FREE(storage); 844 | } 845 | 846 | return true; 847 | } 848 | 849 | [[nodiscard]] inline uint32_t size() const noexcept { return m_numElements; } 850 | [[nodiscard]] inline uint32_t capacity() const noexcept { return m_numBuckets; } 851 | [[nodiscard]] inline bool empty() const noexcept { return (m_numElements == 0); } 852 | 853 | [[nodiscard]] inline bool has(const TKey& key) const noexcept { return (find(key) != iend()); } 854 | 855 | inline TValue& operator[](const TKey& key) 856 | { 857 | std::pair emplaceIt = emplace(key); 858 | return emplaceIt.first.value(); 859 | } 860 | 861 | [[nodiscard]] inline IteratorK begin() const { return IteratorHelper::begin(*this); } 862 | [[nodiscard]] inline IteratorK end() const { return IteratorHelper::end(*this); } 863 | 864 | [[nodiscard]] inline ConstIteratorV vbegin() const { return IteratorHelper::begin(*this); } 865 | [[nodiscard]] inline ConstIteratorV vend() const { return IteratorHelper::end(*this); } 866 | [[nodiscard]] inline IteratorV vbegin() { return IteratorHelper::begin(*this); } 867 | [[nodiscard]] inline IteratorV vend() { return IteratorHelper::end(*this); } 868 | 869 | [[nodiscard]] inline ConstIteratorKV ibegin() const { return IteratorHelper::begin(*this); } 870 | [[nodiscard]] inline ConstIteratorKV iend() const { return IteratorHelper::end(*this); } 871 | [[nodiscard]] inline IteratorKV ibegin() { return IteratorHelper::begin(*this); } 872 | [[nodiscard]] inline IteratorKV iend() { return IteratorHelper::end(*this); } 873 | 874 | template struct TypedIteratorHelper 875 | { 876 | const HashTable* ht; 877 | TypedIteratorHelper(const HashTable* _ht) 878 | : ht(_ht) 879 | { 880 | } 881 | TIterator begin() { return IteratorHelper::begin(*ht); } 882 | TIterator end() { return IteratorHelper::end(*ht); } 883 | }; 884 | 885 | using Keys = TypedIteratorHelper; 886 | using Values = TypedIteratorHelper; 887 | using Items = TypedIteratorHelper; 888 | using ConstValues = TypedIteratorHelper; 889 | using ConstItems = TypedIteratorHelper; 890 | 891 | [[nodiscard]] inline Keys keys() const { return Keys(this); } 892 | [[nodiscard]] inline ConstValues values() const { return ConstValues(this); } 893 | [[nodiscard]] inline ConstItems items() const { return ConstItems(this); } 894 | 895 | [[nodiscard]] inline Values values() { return Values(this); } 896 | [[nodiscard]] inline Items items() { return Items(this); } 897 | 898 | // copy ctor 899 | HashTable(const HashTable& other) 900 | { 901 | EXLBR_ASSERT(&other != this); 902 | m_storage = constructInline(TKeyInfo::getEmpty()); 903 | create(other.m_numBuckets); 904 | copyFrom(other); 905 | } 906 | 907 | // copy assignment 908 | HashTable& operator=(const HashTable& other) 909 | { 910 | if (&other == this) 911 | { 912 | return *this; 913 | } 914 | destroyAndFreeMemory(); 915 | m_storage = constructInline(TKeyInfo::getEmpty()); 916 | create(other.m_numBuckets); 917 | copyFrom(other); 918 | return *this; 919 | } 920 | 921 | // move ctor 922 | HashTable(HashTable&& other) noexcept 923 | //: m_storage(nullptr) 924 | //, m_numBuckets(1) 925 | //, m_numElements(0) 926 | { 927 | EXLBR_ASSERT(&other != this); 928 | moveFrom(std::move(other)); 929 | } 930 | 931 | // move assignment 932 | HashTable& operator=(HashTable&& other) noexcept 933 | { 934 | if (&other == this) 935 | { 936 | return *this; 937 | } 938 | destroyAndFreeMemory(); 939 | moveFrom(std::move(other)); 940 | return *this; 941 | } 942 | 943 | private: 944 | // prefix m_ to be able to easily see member access from the code (it could be more expensive in the inner loop) 945 | TItem* m_storage; // 8 946 | uint32_t m_numBuckets; // 4 947 | uint32_t m_numElements; // 4 948 | 949 | template inline static constexpr bool isPow2(INTEGRAL_TYPE x) noexcept 950 | { 951 | static_assert(std::is_integral::value, "isPow2 must be called on an integer type."); 952 | return (x & (x - 1)) == 0 && (x != 0); 953 | } 954 | 955 | // We need this inline storage to keep `m_storage` not null all the time. 956 | // This will save us from `empty()` check inside `find()` function implementation 957 | static_assert(kNumInlineItems != 0, "Num inline items can't be zero!"); 958 | static_assert(isPow2(kNumInlineItems), "Num inline items should be power of two"); 959 | typename std::aligned_storage::type m_inlineStorage; 960 | static_assert(sizeof(m_inlineStorage) == (sizeof(TItem) * kNumInlineItems), "Incorrect sizeof"); 961 | }; 962 | 963 | // hashmap declaration 964 | template > 965 | using HashMap = HashTable; 966 | 967 | // hashset declaration 968 | template > 969 | using HashSet = HashTable; 970 | 971 | } // namespace Excalibur 972 | -------------------------------------------------------------------------------- /ExcaliburHash/ExcaliburKeyInfo.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "wyhash.h" 6 | 7 | namespace Excalibur 8 | { 9 | 10 | // generic type (without implementation) 11 | template struct KeyInfo 12 | { 13 | // static inline T getTombstone() noexcept; 14 | // static inline T getEmpty() noexcept; 15 | // static inline size_t hash(const T& key) noexcept; 16 | // static inline bool isEqual(const T& lhs, const T& rhs) noexcept; 17 | // static inline bool isValid(const T& key) noexcept; 18 | }; 19 | 20 | template <> struct KeyInfo 21 | { 22 | static inline bool isValid(const int32_t& key) noexcept { return key < INT32_C(0x7ffffffe); } 23 | static inline int32_t getTombstone() noexcept { return INT32_C(0x7fffffff); } 24 | static inline int32_t getEmpty() noexcept { return INT32_C(0x7ffffffe); } 25 | static inline size_t hash(const int32_t& key) noexcept { return Excalibur::wyhash::hash(key); } 26 | static inline bool isEqual(const int32_t& lhs, const int32_t& rhs) noexcept { return lhs == rhs; } 27 | }; 28 | 29 | template <> struct KeyInfo 30 | { 31 | static inline bool isValid(const uint32_t& key) noexcept { return key < UINT32_C(0xfffffffe); } 32 | static inline uint32_t getTombstone() noexcept {return UINT32_C(0xfffffffe); } 33 | static inline uint32_t getEmpty() noexcept {return UINT32_C(0xffffffff); } 34 | static inline size_t hash(const uint32_t& key) noexcept { return Excalibur::wyhash::hash(key); } 35 | static inline bool isEqual(const uint32_t& lhs, const uint32_t& rhs) noexcept { return lhs == rhs; } 36 | }; 37 | 38 | template <> struct KeyInfo 39 | { 40 | static inline bool isValid(const int64_t& key) noexcept { return key < INT64_C(0x7ffffffffffffffe); } 41 | static inline int64_t getTombstone() noexcept { return INT64_C(0x7fffffffffffffff); } 42 | static inline int64_t getEmpty() noexcept { return INT64_C(0x7ffffffffffffffe); } 43 | static inline size_t hash(const int64_t& key) noexcept { return Excalibur::wyhash::hash(key); } 44 | static inline bool isEqual(const int64_t& lhs, const int64_t& rhs) noexcept { return lhs == rhs; } 45 | }; 46 | 47 | template <> struct KeyInfo 48 | { 49 | static inline bool isValid(const uint64_t& key) noexcept { return key < UINT64_C(0xfffffffffffffffe); } 50 | static inline uint64_t getTombstone() noexcept { return UINT64_C(0xfffffffffffffffe); } 51 | static inline uint64_t getEmpty() noexcept { return UINT64_C(0xffffffffffffffff); } 52 | static inline size_t hash(const uint64_t& key) noexcept { return Excalibur::wyhash::hash(key); } 53 | static inline bool isEqual(const uint64_t& lhs, const uint64_t& rhs) noexcept { return lhs == rhs; } 54 | }; 55 | 56 | template <> struct KeyInfo 57 | { 58 | static inline bool isValid(const std::string& key) noexcept { return !key.empty() && key.data()[0] != char(1); } 59 | static inline std::string getTombstone() noexcept 60 | { 61 | // and let's hope that small string optimization will do the job 62 | return std::string(1, char(1)); 63 | } 64 | static inline std::string getEmpty() noexcept { return std::string(); } 65 | static inline size_t hash(const std::string& key) noexcept { return std::hash{}(key); } 66 | static inline bool isEqual(const std::string& lhs, const std::string& rhs) noexcept { return lhs == rhs; } 67 | }; 68 | 69 | 70 | } // namespace Excalibur 71 | -------------------------------------------------------------------------------- /ExcaliburHash/wyhash.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #if defined(_MSC_VER) 6 | #define EXLBR_VISUAL_STUDIO (1) 7 | #define EXLBR_FORCE_INLINE __forceinline 8 | #endif 9 | 10 | #if defined(__clang__) 11 | #define EXLBR_CLANG (1) 12 | #define EXLBR_FORCE_INLINE __attribute__((always_inline)) 13 | #endif 14 | 15 | #if defined(__GNUC__) 16 | #define EXLBR_GCC (1) 17 | #endif 18 | 19 | #if defined(_M_X64) || defined(__aarch64__) || defined(__x86_64__) || defined(__x86_64) || defined(__amd64__) || defined(__amd64) || \ 20 | defined(__LP64__) || defined(_WIN64) 21 | #define EXLBR_64 (1) 22 | #else 23 | #define EXLBR_32 (1) 24 | #endif 25 | 26 | #if EXLBR_VISUAL_STUDIO 27 | #include 28 | #endif 29 | 30 | namespace Excalibur 31 | { 32 | 33 | namespace wyhash 34 | { 35 | 36 | #if EXLBR_64 37 | 38 | inline uint64_t _hash64(uint64_t v) 39 | { 40 | #if defined(EXLBR_VISUAL_STUDIO) 41 | { 42 | uint64_t h; 43 | uint64_t l = _umul128(v, UINT64_C(0x9E3779B97F4A7C15), &h); 44 | return l ^ h; 45 | } 46 | #elif defined(EXLBR_CLANG) || defined(EXLBR_GCC) 47 | { 48 | __uint128_t r = v; 49 | r *= UINT64_C(0x9E3779B97F4A7C15); 50 | return (uint64_t)(r >> 64U) ^ (uint64_t)(r); 51 | } 52 | #else 53 | { 54 | #error Unsupported compiler 55 | } 56 | #endif 57 | } 58 | 59 | #elif EXLBR_32 60 | 61 | inline uint32_t _hash32(uint32_t v) 62 | { 63 | #if EXLBR_VISUAL_STUDIO 64 | { 65 | uint64_t lh = __emulu(v, UINT32_C(0x9e3779b1)); 66 | return (uint32_t)(lh >> 32U) ^ (uint32_t)(lh); 67 | } 68 | #elif defined(EXLBR_CLANG) || defined(EXLBR_GCC) 69 | { 70 | uint64_t lh = uint64_t(v) * uint64_t(0x9e3779b1); 71 | return (uint32_t)(lh >> 32U) ^ (uint32_t)(lh); 72 | } 73 | #else 74 | { 75 | #error Unsupported compiler 76 | } 77 | #endif 78 | } 79 | #else 80 | #error Unsupported platform. Only 64/32-bit platforms supported 81 | #endif 82 | 83 | inline size_t hash(uint64_t v) 84 | { 85 | #if EXLBR_64 86 | return _hash64(v); 87 | #elif EXLBR_32 88 | uint32_t vv = (uint32_t)(v >> 32U) ^ (uint32_t)(v); 89 | return _hash32(vv); 90 | #else 91 | #error Unsupported platform. Only 64/32-bit platforms supported 92 | #endif 93 | } 94 | 95 | inline size_t hash(uint32_t v) 96 | { 97 | #if EXLBR_64 98 | return _hash64(uint64_t(v)); 99 | #elif EXLBR_32 100 | return _hash32(v); 101 | #else 102 | #error Unsupported platform. Only 64/32-bit platforms supported 103 | #endif 104 | } 105 | 106 | 107 | inline size_t hash(int64_t v) { return hash(uint64_t(v)); } 108 | inline size_t hash(int32_t v) { return hash(uint32_t(v)); } 109 | 110 | 111 | } // namespace wyhash 112 | 113 | } // namespace Excalibur -------------------------------------------------------------------------------- /ExcaliburHashTest01.cpp: -------------------------------------------------------------------------------- 1 | #include "ExcaliburHash.h" 2 | #include "gtest/gtest.h" 3 | #include 4 | 5 | TEST(SmFlatHashMap, SimplestTest) 6 | { 7 | Excalibur::HashTable ht; 8 | EXPECT_TRUE(ht.empty()); 9 | auto it1 = ht.emplace(1, 2); 10 | EXPECT_TRUE(it1.second); 11 | EXPECT_EQ(ht.size(), 1u); 12 | auto it2 = ht.find(1); 13 | EXPECT_EQ(it1.first, it2); 14 | EXPECT_EQ(it2.key(), 1); 15 | EXPECT_EQ(it2.value(), 2); 16 | } 17 | 18 | TEST(SmFlatHashMap, EmptyValuesTest) 19 | { 20 | // use hash table as map (no values stored at all) 21 | Excalibur::HashTable ht; 22 | EXPECT_TRUE(ht.empty()); 23 | 24 | const int kNumElements = 99999; 25 | for (int i = 1; i < kNumElements; i++) 26 | { 27 | auto it = ht.emplace(i); 28 | ASSERT_TRUE(it.second); 29 | ASSERT_EQ(ht.size(), uint32_t(i)); 30 | } 31 | EXPECT_FALSE(ht.empty()); 32 | 33 | for (int i = 1; i < kNumElements; i++) 34 | { 35 | bool pos = ht.has(i); 36 | ASSERT_TRUE(pos); 37 | 38 | bool neg = ht.has(-i); 39 | ASSERT_FALSE(neg); 40 | } 41 | 42 | for (int i = 1; i < kNumElements; i++) 43 | { 44 | auto it = ht.emplace(i); 45 | ASSERT_FALSE(it.second); 46 | } 47 | 48 | for (uint32_t i = 1; i < kNumElements; i++) 49 | { 50 | bool isErased = ht.erase(i); 51 | ASSERT_TRUE(isErased); 52 | } 53 | EXPECT_TRUE(ht.empty()); 54 | } 55 | 56 | TEST(SmFlatHashMap, BasicTest) 57 | { 58 | // empty hash table 59 | Excalibur::HashTable ht; 60 | EXPECT_TRUE(ht.empty()); 61 | EXPECT_EQ(ht.size(), 0u); 62 | EXPECT_GE(ht.capacity(), 0u); 63 | 64 | // emplace elements 65 | const uint32_t kNumElements = 99999; 66 | for (uint32_t i = 0; i < kNumElements; i++) 67 | { 68 | int k = 256 * i + 1; 69 | int v = 3 + i; 70 | auto it = ht.emplace(k, v); 71 | EXPECT_TRUE(it.second); 72 | } 73 | EXPECT_FALSE(ht.empty()); 74 | EXPECT_EQ(ht.size(), kNumElements); 75 | EXPECT_GE(ht.capacity(), kNumElements); 76 | 77 | // check if those elements exists and have expected value 78 | for (uint32_t i = 0; i < kNumElements; i++) 79 | { 80 | int k = 256 * i + 1; 81 | auto htVal = ht.find(k); 82 | ASSERT_NE(htVal, ht.iend()); 83 | 84 | int refVal = 3 + i; 85 | EXPECT_EQ(htVal.value(), refVal); 86 | } 87 | 88 | // try to emplace the same keys 89 | for (uint32_t i = 0; i < kNumElements; i++) 90 | { 91 | int k = 256 * i + 1; 92 | 93 | auto it = ht.emplace(k, -13); 94 | EXPECT_FALSE(it.second); 95 | 96 | ASSERT_NE(it.first, ht.iend()); 97 | int refVal = 3 + i; 98 | EXPECT_EQ(it.first.value(), refVal); 99 | } 100 | EXPECT_FALSE(ht.empty()); 101 | EXPECT_EQ(ht.size(), kNumElements); 102 | EXPECT_GE(ht.capacity(), kNumElements); 103 | 104 | // check if those elements exists and have expected value and then remove then 105 | for (uint32_t i = 0; i < kNumElements; i++) 106 | { 107 | int k = 256 * i + 1; 108 | auto htVal = ht.find(k); 109 | ASSERT_NE(htVal, ht.iend()); 110 | 111 | int refVal = 3 + i; 112 | EXPECT_EQ(htVal.value(), refVal); 113 | 114 | bool isErased = ht.erase(k); 115 | EXPECT_TRUE(isErased); 116 | } 117 | EXPECT_TRUE(ht.empty()); 118 | EXPECT_EQ(ht.size(), 0u); 119 | EXPECT_GE(ht.capacity(), 0u); 120 | 121 | ht.emplace(13, 6); 122 | EXPECT_FALSE(ht.empty()); 123 | EXPECT_EQ(ht.size(), 1u); 124 | EXPECT_GE(ht.capacity(), 1u); 125 | 126 | ht.clear(); 127 | EXPECT_TRUE(ht.empty()); 128 | EXPECT_EQ(ht.size(), 0u); 129 | EXPECT_GE(ht.capacity(), 0u); 130 | 131 | // clear for hash table that already empty 132 | ht.clear(); 133 | EXPECT_TRUE(ht.empty()); 134 | EXPECT_EQ(ht.size(), 0u); 135 | EXPECT_GE(ht.capacity(), 0u); 136 | } 137 | 138 | TEST(SmFlatHashMap, EmptyHash) 139 | { 140 | Excalibur::HashTable ht; 141 | EXPECT_TRUE(ht.empty()); 142 | EXPECT_EQ(ht.size(), 0u); 143 | EXPECT_GE(ht.capacity(), 0u); 144 | 145 | auto v0 = ht.find(0); 146 | EXPECT_EQ(v0, ht.iend()); 147 | 148 | auto v1 = ht.find(13); 149 | EXPECT_EQ(v1, ht.iend()); 150 | 151 | bool e0 = ht.erase(0); 152 | EXPECT_FALSE(e0); 153 | 154 | bool e1 = ht.erase(13); 155 | EXPECT_FALSE(e1); 156 | } 157 | 158 | TEST(SmFlatHashMap, IteratorTest) 159 | { 160 | Excalibur::HashTable ht; 161 | EXPECT_TRUE(ht.empty()); 162 | EXPECT_EQ(ht.size(), 0u); 163 | EXPECT_GE(ht.capacity(), 0u); 164 | 165 | // const int kNumElements = 1333; 166 | const int kNumElements = 17; 167 | int64_t valuesSum = 0; 168 | int64_t keysSum = 0; 169 | for (int i = 0; i < kNumElements; i++) 170 | { 171 | int key = i; 172 | int value = -(key * 2 + key); 173 | ht.emplace(key, value); 174 | keysSum += key; 175 | valuesSum += value; 176 | } 177 | 178 | // iterators syntax like in Python 179 | 180 | // default (key) iterator 181 | int64_t keysSumTest = 0; 182 | int step = 0; 183 | for (const int& key : ht) 184 | { 185 | keysSumTest += key; 186 | step++; 187 | } 188 | EXPECT_EQ(keysSum, keysSumTest); 189 | 190 | // keys() iterator 191 | int64_t keysSumTest2 = 0; 192 | for (const int& key : ht.keys()) 193 | { 194 | keysSumTest2 += key; 195 | } 196 | EXPECT_EQ(keysSum, keysSumTest2); 197 | 198 | // values() iterator 199 | int64_t valuesSumTest = 0; 200 | for (const int& value : ht.values()) 201 | { 202 | valuesSumTest += value; 203 | } 204 | EXPECT_EQ(valuesSum, valuesSumTest); 205 | 206 | // items() iterator 207 | int64_t keysSumTest3 = 0; 208 | int64_t valuesSumTest2 = 0; 209 | for (const auto& [key, value] : ht.items()) 210 | { 211 | keysSumTest3 += key; 212 | valuesSumTest2 += value; 213 | } 214 | EXPECT_EQ(keysSum, keysSumTest3); 215 | EXPECT_EQ(valuesSum, valuesSumTest2); 216 | 217 | // complex iterator check 218 | std::array visited; 219 | visited.fill(0); 220 | for (auto it = ht.ibegin(); it != ht.iend(); ++it) 221 | { 222 | int key = it.key(); 223 | ASSERT_GE(key, 0); 224 | ASSERT_LT(key, int(visited.size())); 225 | 226 | EXPECT_EQ(visited[key], 0); 227 | visited[key] = 1; 228 | 229 | int val = it.value(); 230 | int refValue = -(key * 2 + key); 231 | EXPECT_EQ(val, refValue); 232 | } 233 | 234 | for (uint8_t v : visited) 235 | { 236 | EXPECT_EQ(v, 1); 237 | } 238 | } 239 | 240 | struct Bar 241 | { 242 | int v; 243 | }; 244 | 245 | namespace Excalibur 246 | { 247 | template <> struct KeyInfo 248 | { 249 | static inline bool isValid(const Bar& key) noexcept { return key.v < 0x7ffffffe; } 250 | static inline Bar getTombstone() noexcept { return Bar{0x7fffffff}; } 251 | static inline Bar getEmpty() noexcept { return Bar{0x7ffffffe}; } 252 | static inline size_t hash(const Bar& key) noexcept { return std::hash{}(key.v); } 253 | static inline bool isEqual(const Bar& lhs, const Bar& rhs) noexcept { return lhs.v == rhs.v; } 254 | }; 255 | } // namespace Excalibur 256 | 257 | TEST(SmFlatHashMap, IteratorTestEdgeCases) 258 | { 259 | Excalibur::HashTable ht; 260 | EXPECT_TRUE(ht.empty()); 261 | { 262 | auto it = ht.begin(); 263 | EXPECT_EQ(it, ht.end()); 264 | } 265 | 266 | const int kNumElements = 378; 267 | int64_t keysSum = 0; 268 | for (int i = 0; i < kNumElements; i++) 269 | { 270 | int kv = i * 3 + 7; 271 | ht.emplace(Bar{kv}); 272 | keysSum += kv; 273 | } 274 | 275 | int64_t keysSumTestA = 0; 276 | int64_t keysSumTestB = 0; 277 | for (auto it = ht.begin(); it != ht.end(); it++) 278 | { 279 | const Bar& itv = *it; 280 | keysSumTestA += itv.v; 281 | keysSumTestB += it->v; 282 | } 283 | EXPECT_EQ(keysSumTestA, keysSum); 284 | EXPECT_EQ(keysSumTestB, keysSum); 285 | 286 | Excalibur::HashTable ht2; 287 | EXPECT_TRUE(ht2.empty()); 288 | int64_t keysSum2 = 0; 289 | int64_t valSum2 = 0; 290 | for (int i = 0; i < kNumElements; i++) 291 | { 292 | int key = i * 3 + 7; 293 | int val = i * 1 + 13; 294 | ht2.emplace(key, Bar{val}); 295 | keysSum2 += key; 296 | valSum2 += val; 297 | } 298 | 299 | int64_t keysSumTestA2 = 0; 300 | int64_t keysSumTestB2 = 0; 301 | int64_t valuesSumTestA2 = 0; 302 | int64_t valuesSumTestB2 = 0; 303 | for (auto it = ht2.ibegin(); it != ht2.iend(); it++) 304 | { 305 | const auto& kvPair = *it; 306 | keysSumTestA2 += kvPair.first; 307 | const Bar& itva = kvPair.second; 308 | valuesSumTestA2 += itva.v; 309 | 310 | keysSumTestB2 += it->first; 311 | const Bar& itvb = it->second; 312 | valuesSumTestB2 += itvb.v; 313 | } 314 | EXPECT_EQ(keysSumTestA2, keysSum2); 315 | EXPECT_EQ(keysSumTestB2, keysSum2); 316 | EXPECT_EQ(valuesSumTestA2, valSum2); 317 | EXPECT_EQ(valuesSumTestB2, valSum2); 318 | 319 | int64_t valuesSumTestA3 = 0; 320 | int64_t valuesSumTestB3 = 0; 321 | for (auto it = ht2.vbegin(); it != ht2.vend(); it++) 322 | { 323 | const Bar& itv = *it; 324 | valuesSumTestA3 += itv.v; 325 | valuesSumTestB3 += it->v; 326 | } 327 | EXPECT_EQ(valuesSumTestA3, valSum2); 328 | EXPECT_EQ(valuesSumTestB3, valSum2); 329 | } 330 | -------------------------------------------------------------------------------- /ExcaliburHashTest02.cpp: -------------------------------------------------------------------------------- 1 | #include "ExcaliburHash.h" 2 | #include "gtest/gtest.h" 3 | 4 | static int ctorCallCount = 0; 5 | static int dtorCallCount = 0; 6 | static int assignCallCount = 0; 7 | 8 | struct ComplexStruct 9 | { 10 | ComplexStruct() = delete; 11 | ComplexStruct(const ComplexStruct& other) = delete; 12 | ComplexStruct& operator=(const ComplexStruct& other) noexcept = delete; 13 | 14 | ComplexStruct(uint32_t _v) noexcept 15 | : v(_v) 16 | { 17 | ctorCallCount++; 18 | } 19 | 20 | ComplexStruct(ComplexStruct&& other) noexcept 21 | : v(other.v) 22 | { 23 | ctorCallCount++; 24 | } 25 | 26 | ~ComplexStruct() noexcept 27 | { 28 | // 29 | dtorCallCount++; 30 | } 31 | 32 | ComplexStruct& operator=(ComplexStruct&& other) noexcept 33 | { 34 | v = other.v; 35 | assignCallCount++; 36 | return *this; 37 | } 38 | 39 | uint32_t v; 40 | }; 41 | 42 | namespace Excalibur 43 | { 44 | template <> struct KeyInfo 45 | { 46 | static inline bool isValid(const ComplexStruct& key) noexcept { return key.v < 0xfffffffe; } 47 | static inline ComplexStruct getTombstone() noexcept { return ComplexStruct{0xfffffffe}; } 48 | static inline ComplexStruct getEmpty() noexcept { return ComplexStruct{0xffffffff}; } 49 | static inline size_t hash(const ComplexStruct& key) noexcept { return std::hash{}(key.v); } 50 | static inline bool isEqual(const ComplexStruct& lhs, const ComplexStruct& rhs) noexcept { return lhs.v == rhs.v; } 51 | }; 52 | 53 | } // namespace Excalibur 54 | 55 | TEST(SmFlatHashMap, CtorDtorCallCount) 56 | { 57 | ctorCallCount = 0; 58 | dtorCallCount = 0; 59 | assignCallCount = 0; 60 | 61 | { 62 | // empty hash table 63 | Excalibur::HashTable ht; 64 | EXPECT_TRUE(ht.empty()); 65 | EXPECT_EQ(ht.size(), 0u); 66 | EXPECT_GE(ht.capacity(), 0u); 67 | 68 | // emplace elements 69 | const uint32_t kNumElements = 5; 70 | for (uint32_t i = 0; i < kNumElements; i++) 71 | { 72 | int v = 3 + i; 73 | auto it = ht.emplace(ComplexStruct{256 * i + 1}, v); 74 | EXPECT_TRUE(it.second); 75 | } 76 | 77 | EXPECT_FALSE(ht.empty()); 78 | EXPECT_EQ(ht.size(), kNumElements); 79 | EXPECT_GE(ht.capacity(), kNumElements); 80 | 81 | // check if those elements exists and have expected value and then remove then 82 | for (uint32_t i = 0; i < kNumElements; i++) 83 | { 84 | uint32_t k = 256 * i + 1; 85 | 86 | auto htVal = ht.find(k); 87 | ASSERT_NE(htVal, ht.iend()); 88 | 89 | int refVal = 3 + i; 90 | EXPECT_EQ(htVal.value(), refVal); 91 | 92 | bool isErased = ht.erase(k); 93 | EXPECT_TRUE(isErased); 94 | } 95 | EXPECT_TRUE(ht.empty()); 96 | EXPECT_EQ(ht.size(), 0u); 97 | EXPECT_GE(ht.capacity(), 0u); 98 | 99 | ht.emplace(ComplexStruct{13}, 13); 100 | ht.emplace(ComplexStruct{6}, 6); 101 | EXPECT_FALSE(ht.empty()); 102 | EXPECT_EQ(ht.size(), 2u); 103 | EXPECT_GE(ht.capacity(), 2u); 104 | 105 | ht.clear(); 106 | EXPECT_TRUE(ht.empty()); 107 | EXPECT_EQ(ht.size(), 0u); 108 | EXPECT_GE(ht.capacity(), 0u); 109 | 110 | ht.emplace(ComplexStruct{13}, 13); 111 | ht.emplace(ComplexStruct{6}, 6); 112 | ht.emplace(ComplexStruct{9}, 9); 113 | ht.emplace(ComplexStruct{15}, 15); 114 | EXPECT_FALSE(ht.empty()); 115 | EXPECT_EQ(ht.size(), 4u); 116 | EXPECT_GE(ht.capacity(), 4u); 117 | } 118 | 119 | EXPECT_EQ(ctorCallCount, dtorCallCount); 120 | } 121 | 122 | 123 | 124 | struct BadHashStruct 125 | { 126 | int v = 0; 127 | }; 128 | 129 | 130 | namespace Excalibur 131 | { 132 | template <> struct KeyInfo 133 | { 134 | static inline bool isValid(const BadHashStruct& key) noexcept { return key.v < 0x7ffffffe; } 135 | static inline BadHashStruct getTombstone() noexcept { return BadHashStruct{0x7fffffff}; } 136 | static inline BadHashStruct getEmpty() noexcept { return BadHashStruct{0x7ffffffe}; } 137 | static inline size_t hash(const BadHashStruct& /*key*/) noexcept 138 | { 139 | // Note: this is a very bad hash function causing 100% collisions 140 | // added intentionally for the test 141 | return 3; 142 | } 143 | static inline bool isEqual(const BadHashStruct& lhs, const BadHashStruct& rhs) noexcept { return lhs.v == rhs.v; } 144 | }; 145 | } // namespace Excalibur 146 | 147 | 148 | TEST(SmFlatHashMap, InsertEraseReinsert) 149 | { 150 | const int kNumElements = 1024; 151 | Excalibur::HashTable ht; 152 | EXPECT_EQ(ht.size(), 0u); 153 | EXPECT_GE(ht.capacity(), 0u); 154 | 155 | // fill table 156 | for (int i = 0; i < kNumElements; i++) 157 | { 158 | ht.emplace(BadHashStruct{i}); 159 | } 160 | EXPECT_EQ(ht.size(), uint32_t(kNumElements)); 161 | 162 | // remove all but one (all buckets are now fill with tombstones) 163 | for (int i = 1; i < kNumElements; i++) 164 | { 165 | ht.erase(BadHashStruct{i}); 166 | } 167 | EXPECT_EQ(ht.size(), uint32_t(1)); 168 | 169 | // (re)fill table again 170 | for (int i = 0; i < kNumElements; i++) 171 | { 172 | ht.emplace(BadHashStruct{i}); 173 | } 174 | EXPECT_EQ(ht.size(), uint32_t(kNumElements)); 175 | } -------------------------------------------------------------------------------- /ExcaliburHashTest03.cpp: -------------------------------------------------------------------------------- 1 | #include "ExcaliburHash.h" 2 | #include "gtest/gtest.h" 3 | 4 | 5 | #define EXLBR_UNUSED(x) (void)(x) 6 | 7 | struct CustomStruct 8 | { 9 | int v = 0; 10 | }; 11 | 12 | namespace Excalibur 13 | { 14 | template <> struct KeyInfo 15 | { 16 | static inline bool isValid(const CustomStruct& key) noexcept { return key.v < 0x7ffffffe; } 17 | static inline CustomStruct getTombstone() noexcept { return CustomStruct{0x7fffffff}; } 18 | static inline CustomStruct getEmpty() noexcept { return CustomStruct{0x7ffffffe}; } 19 | static inline size_t hash(const CustomStruct& /*key*/) noexcept 20 | { 21 | // Note: this is a very bad hash function causing 100% collisions 22 | // added intentionally for the test 23 | return 3; 24 | } 25 | static inline bool isEqual(const CustomStruct& lhs, const CustomStruct& rhs) noexcept { return lhs.v == rhs.v; } 26 | }; 27 | } // namespace Excalibur 28 | 29 | TEST(SmFlatHashMap, BadHashFunction) 30 | { 31 | Excalibur::HashTable ht; 32 | EXPECT_TRUE(ht.empty()); 33 | EXPECT_EQ(ht.size(), 0u); 34 | EXPECT_GE(ht.capacity(), 0u); 35 | 36 | const int kNumElements = 500; 37 | for (int i = 0; i < kNumElements; i++) 38 | { 39 | int v = 3 + i; 40 | auto it = ht.emplace(CustomStruct{256 * i + 1}, v); 41 | EXPECT_TRUE(it.second); 42 | } 43 | 44 | for (int i = 0; i < kNumElements; i++) 45 | { 46 | auto v = ht.find(CustomStruct{256 * i + 1}); 47 | ASSERT_NE(v, ht.iend()); 48 | int refVal = 3 + i; 49 | EXPECT_EQ(v.value(), refVal); 50 | } 51 | 52 | // search for non-existing keys 53 | auto f0 = ht.find(CustomStruct{-3}); 54 | EXPECT_EQ(f0, ht.iend()); 55 | auto f1 = ht.find(CustomStruct{-13}); 56 | EXPECT_EQ(f1, ht.iend()); 57 | 58 | // erase non-existing keys 59 | bool e0 = ht.erase(CustomStruct{-3}); 60 | EXPECT_FALSE(e0); 61 | bool e1 = ht.erase(CustomStruct{-13}); 62 | EXPECT_FALSE(e1); 63 | } 64 | 65 | TEST(SmFlatHashMap, EmplaceEdgeCase) 66 | { 67 | Excalibur::HashTable ht; 68 | EXPECT_TRUE(ht.empty()); 69 | EXPECT_EQ(ht.size(), 0u); 70 | EXPECT_GE(ht.capacity(), 0u); 71 | 72 | const int kNumElements = 40; 73 | for (int i = 0; i < kNumElements; i++) 74 | { 75 | int v = 3 + i; 76 | auto it = ht.emplace(CustomStruct{256 * i + 1}, v); 77 | EXPECT_TRUE(it.second); 78 | } 79 | 80 | // erase half of elements 81 | for (int i = 0; i < (kNumElements / 2); i++) 82 | { 83 | ht.erase(CustomStruct{256 * i + 1}); 84 | } 85 | 86 | // emplace again 87 | for (int i = 0; i < kNumElements; i++) 88 | { 89 | int v = 3 + i; 90 | auto it = ht.emplace(CustomStruct{256 * i + 1}, v); 91 | EXPECT_EQ(it.first.value(), v); 92 | } 93 | } 94 | 95 | TEST(SmFlatHashMap, ComplexStruct) 96 | { 97 | { 98 | Excalibur::HashTable ht; 99 | 100 | const char* keyStr = "Hello"; 101 | 102 | std::string key = keyStr; 103 | 104 | EXPECT_TRUE(ht.empty()); 105 | ht[key] = "World"; 106 | EXPECT_FALSE(ht.empty()); 107 | 108 | // check that key wasn't affected (use after move edge case) 109 | EXPECT_STREQ(key.c_str(), keyStr); 110 | EXPECT_STREQ(key.c_str(), "Hello"); 111 | 112 | auto it = ht.find(key); 113 | ASSERT_NE(it, ht.end()); 114 | 115 | EXPECT_STREQ(it->second.get().c_str(), "World"); 116 | 117 | std::string& val = ht[key]; 118 | EXPECT_STREQ(val.c_str(), "World"); 119 | 120 | val = "Test"; 121 | EXPECT_STREQ(it->second.get().c_str(), "Test"); 122 | 123 | EXPECT_FALSE(ht.empty()); 124 | ht.erase(it); 125 | EXPECT_TRUE(ht.empty()); 126 | 127 | ht[key] = "World"; 128 | ht.clear(); 129 | 130 | // destroy non empty hash table (with non POD key/value type) 131 | ht[key] = "World"; 132 | } 133 | } 134 | 135 | void ConstCorrectnessTest(const Excalibur::HashTable& ht) 136 | { 137 | // this test is more of an API const correctness test (this code is expected to compile without errors) 138 | uint32_t size = ht.size(); 139 | EXPECT_EQ(size, uint32_t(1)); 140 | 141 | uint32_t capacity = ht.capacity(); 142 | EXPECT_GE(capacity, uint32_t(1)); 143 | 144 | bool isEmpty = ht.empty(); 145 | EXPECT_FALSE(isEmpty); 146 | 147 | bool shouldBeTrue = ht.has(1); 148 | EXPECT_TRUE(shouldBeTrue); 149 | 150 | bool shouldBeFalse = ht.has(-1); 151 | EXPECT_FALSE(shouldBeFalse); 152 | 153 | uint64_t sum = 0; 154 | 155 | auto it1 = ht.find(1); 156 | ASSERT_NE(it1, ht.iend()); 157 | sum += it1->first; 158 | sum += it1->second; 159 | 160 | // this should not compile! 161 | /* 162 | int& v = it1->second; 163 | v = 7; 164 | */ 165 | 166 | auto it2 = ht.find(-1); 167 | EXPECT_EQ(it2, ht.iend()); 168 | 169 | for (auto it3 = ht.begin(); it3 != ht.end(); ++it3) 170 | { 171 | sum += *it3; 172 | } 173 | 174 | for (auto it4 = ht.vbegin(); it4 != ht.vend(); ++it4) 175 | { 176 | sum += *it4; 177 | } 178 | 179 | for (auto it5 = ht.ibegin(); it5 != ht.iend(); ++it5) 180 | { 181 | sum += it5->first; 182 | sum += it5->second; 183 | 184 | // this should not compile! 185 | /* 186 | int& v = it5->second; 187 | v = 7; 188 | */ 189 | } 190 | 191 | for (int k : ht) 192 | { 193 | sum += k; 194 | } 195 | 196 | for (int k : ht.keys()) 197 | { 198 | sum += k; 199 | } 200 | 201 | for (int k : ht.values()) 202 | { 203 | sum += k; 204 | } 205 | 206 | for (auto& kv : ht.items()) 207 | { 208 | sum += kv.first; 209 | sum += kv.second; 210 | 211 | // this should not compile! 212 | /* 213 | int& v = kv.second; 214 | v = 7; 215 | */ 216 | } 217 | 218 | EXLBR_UNUSED(sum); 219 | } 220 | 221 | void MutabilityTest(Excalibur::HashTable& ht) 222 | { 223 | // find() can be used to get non-const kv iterator 224 | auto it1 = ht.find(1); 225 | ASSERT_NE(it1, ht.iend()); 226 | int& v = it1->second; 227 | EXPECT_EQ(v, 2); 228 | v = 3; 229 | EXPECT_EQ(v, 3); 230 | 231 | // you can mutate values when iterate 232 | for (auto it2 = ht.vbegin(); it2 != ht.vend(); ++it2) 233 | { 234 | int& val = *it2; 235 | val++; 236 | } 237 | 238 | for (auto it3 = ht.ibegin(); it3 != ht.iend(); ++it3) 239 | { 240 | int& val = it3->second; 241 | val++; 242 | } 243 | 244 | for (int& k : ht.values()) 245 | { 246 | k++; 247 | } 248 | 249 | for (auto& kv : ht.items()) 250 | { 251 | int& val = kv.second; 252 | val++; 253 | } 254 | 255 | auto it4 = ht.find(1); 256 | ASSERT_NE(it4, ht.iend()); 257 | const int& testVal = it4->second; 258 | EXPECT_EQ(testVal, 7); 259 | } 260 | 261 | TEST(SmFlatHashMap, ConstCorrectness) 262 | { 263 | Excalibur::HashTable ht; 264 | ht.emplace(1, 2); 265 | ConstCorrectnessTest(ht); 266 | MutabilityTest(ht); 267 | } 268 | 269 | TEST(SmFlatHashMap, CopyTest) 270 | { 271 | Excalibur::HashTable ht1; 272 | ht1.emplace(1, 2); 273 | EXPECT_EQ(ht1.size(), uint32_t(1)); 274 | EXPECT_NE(ht1.find(1), ht1.iend()); 275 | 276 | Excalibur::HashTable ht2; 277 | EXPECT_NE(ht2.size(), uint32_t(1)); 278 | EXPECT_EQ(ht2.find(1), ht2.iend()); 279 | 280 | ht2 = ht1; 281 | 282 | EXPECT_EQ(ht2.size(), uint32_t(1)); 283 | EXPECT_NE(ht2.find(1), ht2.iend()); 284 | 285 | EXPECT_EQ(ht1.size(), uint32_t(1)); 286 | EXPECT_NE(ht1.find(1), ht1.iend()); 287 | 288 | Excalibur::HashTable ht3(ht1); 289 | 290 | EXPECT_EQ(ht3.size(), uint32_t(1)); 291 | EXPECT_NE(ht3.find(1), ht3.iend()); 292 | 293 | EXPECT_EQ(ht1.size(), uint32_t(1)); 294 | EXPECT_NE(ht1.find(1), ht1.iend()); 295 | 296 | 297 | Excalibur::HashTable ht4; 298 | ht4.emplace(1); 299 | ht4.emplace(2); 300 | EXPECT_EQ(ht4.size(), uint32_t(2)); 301 | EXPECT_TRUE(ht4.has(1)); 302 | EXPECT_TRUE(ht4.has(2)); 303 | EXPECT_FALSE(ht4.has(3)); 304 | 305 | Excalibur::HashTable ht5; 306 | ht5 = ht4; 307 | EXPECT_EQ(ht5.size(), uint32_t(2)); 308 | EXPECT_TRUE(ht5.has(1)); 309 | EXPECT_TRUE(ht5.has(2)); 310 | EXPECT_FALSE(ht5.has(3)); 311 | } 312 | 313 | TEST(SmFlatHashMap, CopyEdgeCases) 314 | { 315 | Excalibur::HashTable ht1; 316 | ht1.emplace(1, -1); 317 | ht1.emplace(2, -2); 318 | ht1.emplace(3, -3); 319 | EXPECT_EQ(ht1.size(), uint32_t(3)); 320 | EXPECT_NE(ht1.find(1), ht1.iend()); 321 | EXPECT_NE(ht1.find(2), ht1.iend()); 322 | EXPECT_NE(ht1.find(3), ht1.iend()); 323 | 324 | // assign to self 325 | 326 | /* 327 | from Clang 7.0.0 release notes 328 | 329 | -Wself-assign and -Wself-assign-field were extended to diagnose self-assignment operations using overloaded operators (i.e. classes). 330 | If you are doing such an assignment intentionally, e.g. in a unit test for a data structure, the first warning can be disabled by passing 331 | -Wno-self-assign-overloaded, also the warning can be suppressed by adding *& to the right-hand side or casting it to the appropriate reference type. 332 | */ 333 | ht1 = *&ht1; 334 | EXPECT_EQ(ht1.size(), uint32_t(3)); 335 | EXPECT_NE(ht1.find(1), ht1.iend()); 336 | EXPECT_NE(ht1.find(2), ht1.iend()); 337 | EXPECT_NE(ht1.find(3), ht1.iend()); 338 | 339 | Excalibur::HashTable ht2; 340 | EXPECT_TRUE(ht2.empty()); 341 | // assign empty 342 | ht1 = ht2; 343 | EXPECT_TRUE(ht1.empty()); 344 | } 345 | 346 | TEST(SmFlatHashMap, MoveTest) 347 | { 348 | Excalibur::HashTable ht1; 349 | ht1.emplace(1, 2); 350 | EXPECT_EQ(ht1.size(), uint32_t(1)); 351 | EXPECT_NE(ht1.find(1), ht1.iend()); 352 | 353 | Excalibur::HashTable ht2; 354 | EXPECT_NE(ht2.size(), uint32_t(1)); 355 | EXPECT_EQ(ht2.find(1), ht2.iend()); 356 | 357 | ht2 = std::move(ht1); 358 | 359 | EXPECT_EQ(ht2.size(), uint32_t(1)); 360 | EXPECT_NE(ht2.find(1), ht2.iend()); 361 | 362 | Excalibur::HashTable ht3(std::move(ht2)); 363 | 364 | EXPECT_EQ(ht3.size(), uint32_t(1)); 365 | EXPECT_NE(ht3.find(1), ht3.iend()); 366 | } 367 | 368 | static const int kNumElementsInTinyTable = 1; 369 | static Excalibur::HashTable makeTinyHashTable(int valueOffset = 0) 370 | { 371 | Excalibur::HashTable ht; 372 | EXPECT_TRUE(ht.empty()); 373 | for (int i = 0; i < kNumElementsInTinyTable; i++) 374 | { 375 | ht.emplace(i, i + valueOffset); 376 | } 377 | EXPECT_EQ(ht.size(), uint32_t(kNumElementsInTinyTable)); 378 | return ht; 379 | } 380 | 381 | static const int kNumElementsInHugeTable = 1000; 382 | static Excalibur::HashTable makeHugeHashTable() 383 | { 384 | Excalibur::HashTable ht; 385 | EXPECT_TRUE(ht.empty()); 386 | for (int i = 0; i < kNumElementsInHugeTable; i++) 387 | { 388 | ht.emplace(i, i); 389 | } 390 | EXPECT_EQ(ht.size(), uint32_t(kNumElementsInHugeTable)); 391 | return ht; 392 | } 393 | 394 | static Excalibur::HashTable makeHashTable(int numElements) 395 | { 396 | Excalibur::HashTable ht; 397 | EXPECT_TRUE(ht.empty()); 398 | for (int i = 0; i < numElements; i++) 399 | { 400 | ht.emplace(i, i); 401 | } 402 | EXPECT_EQ(ht.size(), uint32_t(numElements)); 403 | return ht; 404 | } 405 | 406 | TEST(SmFlatHashMap, MoveEdgeCases) 407 | { 408 | { 409 | // many to one 410 | Excalibur::HashTable htSmall = makeTinyHashTable(); 411 | Excalibur::HashTable htHuge = makeHugeHashTable(); 412 | htSmall = std::move(htHuge); 413 | EXPECT_EQ(htSmall.size(), uint32_t(kNumElementsInHugeTable)); 414 | } 415 | 416 | { 417 | // one to many 418 | Excalibur::HashTable htSmall = makeTinyHashTable(); 419 | Excalibur::HashTable htHuge = makeHugeHashTable(); 420 | htHuge = std::move(htSmall); 421 | EXPECT_EQ(htHuge.size(), uint32_t(kNumElementsInTinyTable)); 422 | } 423 | 424 | { 425 | // many to many 426 | Excalibur::HashTable htHuge1 = makeHugeHashTable(); 427 | Excalibur::HashTable htHuge2 = makeHashTable((kNumElementsInHugeTable - 4)); 428 | htHuge1 = std::move(htHuge2); 429 | EXPECT_EQ(htHuge1.size(), uint32_t((kNumElementsInHugeTable - 4))); 430 | } 431 | 432 | { 433 | // one to one 434 | const int kValueOffset = 13; 435 | Excalibur::HashTable htSmall1 = makeTinyHashTable(); 436 | Excalibur::HashTable htSmall2 = makeTinyHashTable(kValueOffset); 437 | htSmall1 = std::move(htSmall2); 438 | EXPECT_EQ(htSmall1.size(), uint32_t(kNumElementsInTinyTable)); 439 | auto it = htSmall1.ibegin(); 440 | EXPECT_EQ(it.value(), kValueOffset); 441 | } 442 | 443 | { 444 | // zero to one 445 | Excalibur::HashTable htSmall = makeTinyHashTable(); 446 | Excalibur::HashTable htEmpty; 447 | htSmall = std::move(htEmpty); 448 | EXPECT_EQ(htSmall.size(), uint32_t(0)); 449 | } 450 | 451 | { 452 | // zero to many 453 | Excalibur::HashTable htHuge = makeHugeHashTable(); 454 | Excalibur::HashTable htEmpty; 455 | htHuge = std::move(htEmpty); 456 | EXPECT_EQ(htHuge.size(), uint32_t(0)); 457 | } 458 | 459 | { 460 | // move to self 461 | 462 | /* 463 | from Clang 7.0.0 release notes 464 | 465 | -Wself-assign and -Wself-assign-field were extended to diagnose self-assignment operations using overloaded operators (i.e. 466 | classes). If you are doing such an assignment intentionally, e.g. in a unit test for a data structure, the first warning can be 467 | disabled by passing -Wno-self-assign-overloaded, also the warning can be suppressed by adding *& to the right-hand side or casting 468 | it to the appropriate reference type. 469 | */ 470 | 471 | Excalibur::HashTable htHuge = makeHugeHashTable(); 472 | htHuge = std::move(*&htHuge); 473 | EXPECT_EQ(htHuge.size(), uint32_t(kNumElementsInHugeTable)); 474 | } 475 | } 476 | 477 | TEST(SmFlatHashMap, TryToEmplaceDuplicate) 478 | { 479 | // note: CustomStruct intentionally has a very bad hash function that always returns 3 480 | Excalibur::HashTable ht; 481 | 482 | // note i0 and i1 produces collision due to weak hash function 483 | CustomStruct& i0 = ht[CustomStruct{0}]; 484 | EXPECT_EQ(i0.v, 0); 485 | i0.v++; 486 | 487 | CustomStruct& i1 = ht[CustomStruct{1}]; 488 | EXPECT_EQ(i1.v, 0); 489 | i1.v++; 490 | 491 | // erase i0 492 | ht.erase(CustomStruct{0}); 493 | 494 | // check if we found the same i1 or not 495 | CustomStruct& _i1 = ht[CustomStruct{1}]; 496 | EXPECT_EQ(i1.v, 1); 497 | EXPECT_EQ(_i1.v, 1); 498 | 499 | EXPECT_EQ(&_i1.v, &i1.v); 500 | } 501 | 502 | TEST(SmFlatHashMap, ReserveTest) 503 | { 504 | Excalibur::HashTable ht; 505 | EXPECT_TRUE(ht.empty()); 506 | EXPECT_GE(ht.capacity(), 0u); 507 | 508 | ht.reserve(31); 509 | EXPECT_TRUE(ht.empty()); 510 | EXPECT_EQ(ht.capacity(), 32u); 511 | 512 | ht.reserve(32); 513 | EXPECT_TRUE(ht.empty()); 514 | EXPECT_EQ(ht.capacity(), 32u); 515 | 516 | ht.emplace(1, -1); 517 | ht.emplace(9, -9); 518 | EXPECT_EQ(ht.size(), 2u); 519 | EXPECT_EQ(ht.capacity(), 32u); 520 | 521 | ht.reserve(128); 522 | EXPECT_EQ(ht.size(), 2u); 523 | EXPECT_EQ(ht.capacity(), 128u); 524 | EXPECT_TRUE(ht.has(1)); 525 | EXPECT_TRUE(ht.has(9)); 526 | 527 | ht.reserve(55); 528 | EXPECT_EQ(ht.size(), 2u); 529 | EXPECT_EQ(ht.capacity(), 128u); 530 | EXPECT_TRUE(ht.has(1)); 531 | EXPECT_TRUE(ht.has(9)); 532 | } -------------------------------------------------------------------------------- /ExcaliburHashTest04.cpp: -------------------------------------------------------------------------------- 1 | #include "ExcaliburHash.h" 2 | #include "gtest/gtest.h" 3 | #include 4 | #include 5 | 6 | struct ComplexValue 7 | { 8 | ComplexValue(const ComplexValue& other) 9 | : isMoved(other.isMoved) 10 | , isDeleted(other.isDeleted) 11 | { 12 | EXPECT_FALSE(other.isMoved); 13 | EXPECT_FALSE(other.isDeleted); 14 | } 15 | 16 | ComplexValue& operator=(const ComplexValue& other) noexcept 17 | { 18 | EXPECT_FALSE(other.isMoved); 19 | EXPECT_FALSE(other.isDeleted); 20 | isMoved = other.isMoved; 21 | isDeleted = other.isDeleted; 22 | return *this; 23 | } 24 | 25 | ComplexValue() noexcept 26 | : isMoved(false) 27 | , isDeleted(false) 28 | { 29 | } 30 | 31 | ComplexValue(ComplexValue&& other) noexcept 32 | : isMoved(other.isMoved) 33 | , isDeleted(other.isDeleted) 34 | { 35 | EXPECT_FALSE(other.isDeleted); 36 | other.isMoved = true; 37 | } 38 | 39 | ~ComplexValue() noexcept { isDeleted = true; } 40 | 41 | ComplexValue& operator=(ComplexValue&& other) noexcept 42 | { 43 | isDeleted = other.isDeleted; 44 | isMoved = other.isMoved; 45 | other.isMoved = true; 46 | return *this; 47 | } 48 | 49 | bool isMoved; 50 | bool isDeleted; 51 | }; 52 | 53 | TEST(SmFlatHashMap, InsertFromItselfWhileGrow) 54 | { 55 | for (int i = 1; i <= 1000; i++) 56 | { 57 | // printf("Step %d\n", i); 58 | 59 | // create hash map and insert one element 60 | Excalibur::HashTable ht; 61 | 62 | // insert some elements into the hash maps 63 | for (int j = 0; j < i; j++) 64 | { 65 | ht.emplace(j, ComplexValue{}); 66 | } 67 | 68 | // find the first inserted element (get a valid iterator) 69 | auto it = ht.find(0); 70 | ASSERT_NE(it, ht.end()); 71 | 72 | // insert a new element using the above iterator 73 | // (a hash map can grow during insertion and this could invalidate the iterator) 74 | ht.emplace(-1, it->second); 75 | 76 | auto it2 = ht.find(-1); 77 | ASSERT_NE(it2, ht.end()); 78 | } 79 | } 80 | 81 | TEST(SmFlatHashMap, CopyableIterators) 82 | { 83 | Excalibur::HashTable ht; 84 | 85 | int sum = 0; 86 | for (int i = 1; i <= 16; i++) 87 | { 88 | ht.emplace(std::to_string(i), std::to_string(i + 1)); 89 | sum += i; 90 | } 91 | 92 | Excalibur::HashTable::IteratorKV it = ht.find(std::to_string(1)); 93 | ASSERT_NE(it, ht.end()); 94 | const std::string& val = it->second; 95 | 96 | Excalibur::HashTable::IteratorKV it2 = it; 97 | ASSERT_NE(it2, ht.end()); 98 | const std::string& val2 = it2->second; 99 | 100 | EXPECT_EQ(it, it2); 101 | EXPECT_EQ(val, val2); 102 | 103 | ++it2; 104 | EXPECT_NE(it, it2); 105 | 106 | it2 = it; 107 | EXPECT_EQ(it, it2); 108 | 109 | // capture before state 110 | std::vector before; 111 | { 112 | for (auto it3 = ht.ibegin(); it3 != ht.iend(); ++it3) 113 | { 114 | before.emplace_back(it3->first); 115 | } 116 | std::sort(before.begin(), before.end()); 117 | } 118 | 119 | 120 | // iterate and remove 121 | { 122 | for (Excalibur::HashTable::IteratorKV it3 = ht.ibegin(); it3 != ht.iend(); ++it3) 123 | { 124 | if (std::strcmp(it3->first.get().c_str(), "5") == 0) 125 | { 126 | it3 = ht.erase(it3); 127 | } 128 | 129 | if (std::strcmp(it3->first.get().c_str(), "9") == 0) 130 | { 131 | it3 = ht.erase(it3); 132 | } 133 | } 134 | } 135 | 136 | // capture after state 137 | std::vector after; 138 | { 139 | for (auto it3 = ht.ibegin(); it3 != ht.iend(); ++it3) 140 | { 141 | after.emplace_back(it3->first); 142 | } 143 | 144 | std::sort(after.begin(), after.end()); 145 | } 146 | 147 | // make sure we've got the expected results 148 | int sumBefore = 0; 149 | for (const std::string& v : before) 150 | { 151 | int iv = std::stoi(v); 152 | sumBefore += iv; 153 | } 154 | EXPECT_EQ(sumBefore, sum); 155 | 156 | int sumAfter = 0; 157 | for (const std::string& v : after) 158 | { 159 | int iv = std::stoi(v); 160 | sumAfter += iv; 161 | } 162 | EXPECT_EQ(sumBefore-5-9, sumAfter); 163 | } -------------------------------------------------------------------------------- /ExcaliburHashTest05.cpp: -------------------------------------------------------------------------------- 1 | #include "ExcaliburHash.h" 2 | #include "gtest/gtest.h" 3 | #include 4 | #include 5 | 6 | TEST(SmFlatHashMap, InlineStorageTest01) 7 | { 8 | // create hash map and insert one element 9 | Excalibur::HashTable ht; 10 | 11 | EXPECT_GE(ht.capacity(), uint32_t(4)); 12 | 13 | auto it1 = ht.emplace(std::string("hello1"), std::string("world1")); 14 | EXPECT_TRUE(it1.second); 15 | auto it2 = ht.emplace(std::string("hello2"), std::string("world2")); 16 | EXPECT_TRUE(it2.second); 17 | 18 | EXPECT_EQ(ht.size(), uint32_t(2)); 19 | 20 | { 21 | auto _it1 = ht.find("hello1"); 22 | ASSERT_NE(_it1, ht.end()); 23 | const std::string& val1 = _it1->second; 24 | ASSERT_EQ(val1, "world1"); 25 | 26 | auto _it2 = ht.find("hello2"); 27 | ASSERT_NE(_it2, ht.end()); 28 | const std::string& val2 = _it2->second; 29 | ASSERT_EQ(val2, "world2"); 30 | } 31 | 32 | for (int i = 0; i < 1000; i++) 33 | { 34 | ht.emplace(std::to_string(i), "tmp"); 35 | } 36 | 37 | { 38 | auto _it1 = ht.find("hello1"); 39 | ASSERT_NE(_it1, ht.end()); 40 | const std::string& val1 = _it1->second; 41 | ASSERT_EQ(val1, "world1"); 42 | 43 | auto _it2 = ht.find("hello2"); 44 | ASSERT_NE(_it2, ht.end()); 45 | const std::string& val2 = _it2->second; 46 | ASSERT_EQ(val2, "world2"); 47 | } 48 | } 49 | 50 | static int ctorCallCount = 0; 51 | static int dtorCallCount = 0; 52 | static int assignCallCount = 0; 53 | 54 | struct ComplexVal 55 | { 56 | ComplexVal() = delete; 57 | ComplexVal(const ComplexVal& other) = delete; 58 | ComplexVal& operator=(const ComplexVal& other) noexcept = delete; 59 | 60 | ComplexVal(uint32_t _v) noexcept 61 | : v(_v) 62 | , status(Status::Constructed) 63 | { 64 | ctorCallCount++; 65 | } 66 | 67 | ComplexVal(ComplexVal&& other) noexcept 68 | : v(other.v) 69 | , status(Status::MoveConstructed) 70 | { 71 | EXPECT_NE(other.status, Status::Destructed); 72 | ctorCallCount++; 73 | other.status = Status::Moved; 74 | } 75 | 76 | ~ComplexVal() noexcept 77 | { 78 | EXPECT_NE(status, Status::Destructed); 79 | status = Status::Destructed; 80 | dtorCallCount++; 81 | } 82 | 83 | ComplexVal& operator=(ComplexVal&& other) noexcept 84 | { 85 | status = Status::MoveAssigned; 86 | other.status = Status::Moved; 87 | 88 | v = other.v; 89 | assignCallCount++; 90 | return *this; 91 | } 92 | 93 | uint32_t v; 94 | enum class Status 95 | { 96 | Constructed = 0, 97 | MoveConstructed = 1, 98 | MoveAssigned = 2, 99 | Destructed = 3, 100 | Moved = 4, 101 | }; 102 | Status status; 103 | }; 104 | 105 | template void doMoveAssignmentTest(TFrom& hm1, TTo& hm2, size_t numValuesToInsert) 106 | { 107 | // insert values 108 | for (size_t i = 0; i < numValuesToInsert; i++) 109 | { 110 | hm1.emplace(int(i), uint32_t(i + 13)); 111 | } 112 | 113 | { 114 | EXPECT_TRUE(hm1.has(0)); 115 | EXPECT_TRUE(hm1.has(1)); 116 | EXPECT_TRUE(hm1.has(2)); 117 | 118 | auto it1 = hm1.find(0); 119 | auto it2 = hm1.find(1); 120 | auto it3 = hm1.find(2); 121 | 122 | EXPECT_NE(it1, hm1.end()); 123 | EXPECT_NE(it2, hm1.end()); 124 | EXPECT_NE(it3, hm1.end()); 125 | 126 | const int& key1 = it1->first; 127 | const ComplexVal& value1 = it1->second; 128 | 129 | const int& key2 = it2->first; 130 | const ComplexVal& value2 = it2->second; 131 | 132 | const int& key3 = it3->first; 133 | const ComplexVal& value3 = it3->second; 134 | 135 | EXPECT_EQ(key1, 0); 136 | EXPECT_EQ(key2, 1); 137 | EXPECT_EQ(key3, 2); 138 | 139 | EXPECT_EQ(value1.v, uint32_t(13)); 140 | EXPECT_EQ(value2.v, uint32_t(14)); 141 | EXPECT_EQ(value3.v, uint32_t(15)); 142 | } 143 | 144 | // move assign to other hash map 145 | hm2 = std::move(hm1); 146 | 147 | { 148 | EXPECT_TRUE(hm2.has(0)); 149 | EXPECT_TRUE(hm2.has(1)); 150 | EXPECT_TRUE(hm2.has(2)); 151 | 152 | auto it1 = hm2.find(0); 153 | auto it2 = hm2.find(1); 154 | auto it3 = hm2.find(2); 155 | 156 | EXPECT_NE(it1, hm2.end()); 157 | EXPECT_NE(it2, hm2.end()); 158 | EXPECT_NE(it3, hm2.end()); 159 | 160 | const int& key1 = it1->first; 161 | const ComplexVal& value1 = it1->second; 162 | 163 | const int& key2 = it2->first; 164 | const ComplexVal& value2 = it2->second; 165 | 166 | const int& key3 = it3->first; 167 | const ComplexVal& value3 = it3->second; 168 | 169 | EXPECT_EQ(key1, 0); 170 | EXPECT_EQ(key2, 1); 171 | EXPECT_EQ(key3, 2); 172 | 173 | EXPECT_EQ(value1.v, uint32_t(13)); 174 | EXPECT_EQ(value2.v, uint32_t(14)); 175 | EXPECT_EQ(value3.v, uint32_t(15)); 176 | } 177 | } 178 | 179 | TEST(SmFlatHashMap, InlineStorageTest02) 180 | { 181 | { 182 | // move inline -> inline 183 | ctorCallCount = 0; 184 | dtorCallCount = 0; 185 | assignCallCount = 0; 186 | { 187 | Excalibur::HashMap hm1; 188 | Excalibur::HashMap hm2; 189 | 190 | doMoveAssignmentTest(hm1, hm2, 3); 191 | } 192 | EXPECT_EQ(ctorCallCount, dtorCallCount); 193 | } 194 | 195 | { 196 | // move heap -> heap 197 | ctorCallCount = 0; 198 | dtorCallCount = 0; 199 | assignCallCount = 0; 200 | { 201 | Excalibur::HashMap hm1; 202 | Excalibur::HashMap hm2; 203 | 204 | doMoveAssignmentTest(hm1, hm2, 100); 205 | } 206 | EXPECT_EQ(ctorCallCount, dtorCallCount); 207 | } 208 | } 209 | 210 | TEST(SmFlatHashMap, AliasNameTest) 211 | { 212 | { 213 | Excalibur::HashMap hm; 214 | auto it1 = hm.emplace(1, 2); 215 | EXPECT_TRUE(it1.second); 216 | auto it2 = hm.emplace(2, 3); 217 | EXPECT_TRUE(it2.second); 218 | 219 | auto _it1 = hm.find(1); 220 | ASSERT_NE(_it1, hm.end()); 221 | 222 | auto _it2 = hm.find(2); 223 | ASSERT_NE(_it2, hm.end()); 224 | 225 | auto _it3 = hm.find(3); 226 | ASSERT_EQ(_it3, hm.end()); 227 | 228 | const int& val1 = _it1->second; 229 | const int& val2 = _it2->second; 230 | ASSERT_EQ(val1, 2); 231 | ASSERT_EQ(val2, 3); 232 | } 233 | 234 | { 235 | Excalibur::HashSet hs; 236 | auto it1 = hs.emplace(1); 237 | EXPECT_TRUE(it1.second); 238 | auto it2 = hs.emplace(1); 239 | EXPECT_FALSE(it2.second); 240 | auto it3 = hs.emplace(2); 241 | EXPECT_TRUE(it3.second); 242 | 243 | EXPECT_TRUE(hs.has(1)); 244 | EXPECT_TRUE(hs.has(2)); 245 | EXPECT_FALSE(hs.has(3)); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /Images/ClearAndInsertRnd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/b99119acdf2669c7e4bf071d8fd9046a94c9c2d6/Images/ClearAndInsertRnd.png -------------------------------------------------------------------------------- /Images/ClearAndInsertSeq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/b99119acdf2669c7e4bf071d8fd9046a94c9c2d6/Images/ClearAndInsertSeq.png -------------------------------------------------------------------------------- /Images/CtorDtor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/b99119acdf2669c7e4bf071d8fd9046a94c9c2d6/Images/CtorDtor.png -------------------------------------------------------------------------------- /Images/CtorSingleEmplaceDtor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/b99119acdf2669c7e4bf071d8fd9046a94c9c2d6/Images/CtorSingleEmplaceDtor.png -------------------------------------------------------------------------------- /Images/InsertAccessWithProbability10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/b99119acdf2669c7e4bf071d8fd9046a94c9c2d6/Images/InsertAccessWithProbability10.png -------------------------------------------------------------------------------- /Images/InsertAccessWithProbability50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/b99119acdf2669c7e4bf071d8fd9046a94c9c2d6/Images/InsertAccessWithProbability50.png -------------------------------------------------------------------------------- /Images/InsertRndAndRemove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/b99119acdf2669c7e4bf071d8fd9046a94c9c2d6/Images/InsertRndAndRemove.png -------------------------------------------------------------------------------- /Images/InsertRndClearAndReInsert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/b99119acdf2669c7e4bf071d8fd9046a94c9c2d6/Images/InsertRndClearAndReInsert.png -------------------------------------------------------------------------------- /Images/SearchExisting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/b99119acdf2669c7e4bf071d8fd9046a94c9c2d6/Images/SearchExisting.png -------------------------------------------------------------------------------- /Images/SearchNonExisting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/b99119acdf2669c7e4bf071d8fd9046a94c9c2d6/Images/SearchNonExisting.png -------------------------------------------------------------------------------- /Images/amd_summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/b99119acdf2669c7e4bf071d8fd9046a94c9c2d6/Images/amd_summary.png -------------------------------------------------------------------------------- /Images/intel_summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/b99119acdf2669c7e4bf071d8fd9046a94c9c2d6/Images/intel_summary.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sergey Makeev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExcaliburHash 2 | 3 | [![Actions Status](https://github.com/SergeyMakeev/ExcaliburHash/workflows/build/badge.svg)](https://github.com/SergeyMakeev/ExcaliburHash/actions) 4 | [![codecov](https://codecov.io/gh/SergeyMakeev/ExcaliburHash/branch/main/graph/badge.svg?token=8YKFZPXMEE)](https://codecov.io/gh/SergeyMakeev/ExcaliburHash) 5 | [![Build status](https://ci.appveyor.com/api/projects/status/vsdtgfr4jubgj2hi?svg=true)](https://ci.appveyor.com/project/SergeyMakeev/excaliburhash) 6 | ![MIT](https://img.shields.io/badge/license-MIT-blue.svg) 7 | 8 | ## About 9 | 10 | Excalibur Hash is a high-speed hash map and hash set, ideal for performance-critical uses like video games. 11 | Its design focuses on being friendly to the CPU cache, making it very efficient and fast. 12 | It uses an open addressing hash table and manages removed items with a method called tombstones. 13 | 14 | Engineered for ease of use, Excalibur Hash, in a vast majority of cases (99%), serves as a seamless, drop-in alternative to std::unordered_map. However, it's important to note that Excalibur Hash does not guarantee stable addressing. 15 | So, if your project needs to hold direct pointers to the keys or values, Excalibur Hash might not work as you expect. This aside, its design and efficiency make it a great choice for applications where speed is crucial. 16 | 17 | ## Features 18 | 19 | 1. Extremely fast (see Performance section for details) 20 | 2. CPU cache friendly 21 | 3. Built-in configurable inline storage 22 | 4. Can either work as a map (key, value) or as a set (keys only) 23 | 24 | 25 | ## Performance 26 | 27 | In this section, you can see a performance comparison against a few popular hash table implementations. 28 | This comparison will show their speed and efficiency, helping you understand which hash table might work best for your project. 29 | 30 | 31 | Unless otherwise stated, all tests were run using the following configuration 32 | ``` 33 | OS: Windows 11 Pro (22621.3155) 34 | CPU: Intel i9-12900K 35 | RAM: 128Gb 36 | Compiled in the Release mode using VS2022 (19.39.33520) 37 | ``` 38 | 39 | [Performance test repositoty](https://github.com/SergeyMakeev/SimpleHashTest) 40 | 41 | 42 | [absl::flat_hash_map](https://github.com/abseil/abseil-cpp/tree/2be67701e7a33b45d322064349827e1155953338) 43 | [boost::unordered_map](https://github.com/boostorg/unordered/tree/67c5cdb3a69f0b92d2779880ce9aa1d46e54cf7b) 44 | [boost::unordered_flat_map](https://github.com/boostorg/unordered/tree/67c5cdb3a69f0b92d2779880ce9aa1d46e54cf7b) 45 | [ska::flat_hash_map](https://github.com/skarupke/flat_hash_map/tree/2c4687431f978f02a3780e24b8b701d22aa32d9c) 46 | [ska::unordered_map](https://github.com/skarupke/flat_hash_map/tree/2c4687431f978f02a3780e24b8b701d22aa32d9c) 47 | [folly::F14ValueMap](https://github.com/facebook/folly/tree/4a2f1aaa23d3a4c755b5dc500360ce1011b2e149) 48 | [llvm::DenseMap](https://github.com/llvm/llvm-project/tree/2521e9785dd640920d97b110a8e5b6886e09b851) 49 | [Luau::DenseHashMap](https://github.com/luau-lang/luau/tree/cdd1a380dbf768f168910317e7576210afcd9552) 50 | [phmap::flat_hash_map](https://github.com/greg7mdp/parallel-hashmap/tree/946ebad67a21212d11a0dd4deb7cdedc297d47bc) 51 | [tsl::robin_map](https://github.com/Tessil/robin-map/tree/f45ebce73b3631fdfb8205e2ba700b726ff0c34f) 52 | [google::dense_hash_map](https://github.com/sparsehash/sparsehash/tree/1dffea3d917445d70d33d0c7492919fc4408fe5c) 53 | [std::unordered_map](https://github.com/microsoft/STL) 54 | [Excalibur::HashTable](https://github.com/SergeyMakeev/ExcaliburHash/tree/60be6e673a37317904150c402d43c70801cdbd95) 55 | 56 | 57 | 58 | ### CtorDtor 59 | 60 | Create and immediatly delete 300,000 hash tables on the stack, using a 'heavyweight' object as the value. 61 | This test quickly shows which hash map implementations 'cheat' by creating many key/value pairs in advance. 62 | 63 | ![Performance comparison](https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/master/Images/CtorDtor.png) 64 | 65 | 66 | ### ClearAndInsertSeq 67 | 68 | 1. Create a hash table 69 | 2. Clear the hash table 70 | 3. Insert 599,999 sequential values 71 | 4. Repeat steps 2-3 (25 times) 72 | 5. Destroy the hash table 73 | 74 | ![Performance comparison](https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/master/Images/ClearAndInsertSeq.png) 75 | 76 | ### InsertRndClearAndReInsert 77 | 78 | 1. Create a hash table 79 | 2. Insert 1,000,000 unique random int numbers 80 | 3. Clear the hash table 81 | 4. Reinsert 1,000,000 unique random int numbers into the same cleared map. 82 | 5. Destroy the hash table 83 | 6. Repeat steps 1-5 (10 times) 84 | 85 | ![Performance comparison](https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/master/Images/InsertRndClearAndReInsert.png) 86 | 87 | 88 | ### InsertRndAndRemove 89 | 90 | 1. Create a hash table 91 | 2. Insert 1,000,000 unique random int numbers 92 | 3. Remove all of the inserted entries one by one until the map is empty again. 93 | 4. Destroy the hash table 94 | 5. Repeat steps 1-4 (10 times) 95 | 96 | ![Performance comparison](https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/master/Images/InsertRndAndRemove.png) 97 | 98 | ### CtorSingleEmplaceDtor 99 | 1. Create a hash table 100 | 2. Insert a single key/value int the hash table 101 | 3. Destroy the hash table 102 | 4. Repeat steps 1-3 (300,000 times) 103 | 104 | ![Performance comparison](https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/master/Images/CtorSingleEmplaceDtor.png) 105 | 106 | 107 | ### InsertAccessWithProbability10 108 | 1. Create a hash table 109 | 2. Insert or increment 1,000,000 values where 10% of keys are duplicates 110 | (10% of operations will be modifications and 90% will be insertions) 111 | 3. Destroy the hash table 112 | 4. Repeat steps 1-3 (8 times) 113 | 114 | ![Performance comparison](https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/master/Images/InsertAccessWithProbability10.png) 115 | 116 | 117 | ### InsertAccessWithProbability50 118 | 1. Create a hash table 119 | 2. Insert or increment 1,000,000 values where 50% of keys are duplicates 120 | (50% of operations will be modifications and 50% will be insertions) 121 | 3. Destroy the hash table 122 | 4. Repeat steps 1-3 (8 times) 123 | 124 | ![Performance comparison](https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/master/Images/InsertAccessWithProbability50.png) 125 | 126 | ### SearchNonExisting 127 | 1. Create a hash table 128 | 2. Insert 1,000,000 unique random int numbers 129 | 3. Search for non existent keys (10,000,000 times) 130 | 4. Destroy the hash table 131 | 132 | ![Performance comparison](https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/master/Images/SearchNonExisting.png) 133 | 134 | 135 | ### SearchExisting 136 | 1. Create a hash table 137 | 2. Insert 1,000,000 unique random int numbers 138 | 3. Search for existent keys (10,000,000 times) 139 | 4. Destroy the hash table 140 | 141 | 142 | ![Performance comparison](https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/master/Images/SearchExisting.png) 143 | 144 | ### ClearAndInsertRnd 145 | 146 | 1. Create a hash table 147 | 2. Insert 1,000,000 unique random int numbers 148 | 3. Destroy the hash table 149 | 4. Repeat steps 1-3 (25 times) 150 | 151 | ![Performance comparison](https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/master/Images/ClearAndInsertRnd.png) 152 | 153 | ### Summary 154 | 155 | Below, you can find all the tests combined into a single table using normalized timings where 1.0 is the fastest implementation, 2.0 is twice as slow as the fastest configuration, and so on. 156 | 157 | ``` 158 | OS: Windows 11 Pro (22621.2861) 159 | CPU: Intel i9-12900K 160 | RAM: 128Gb 161 | ``` 162 | ![Intel Summary](https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/master/Images/intel_summary.png) 163 | 164 | 165 | ``` 166 | OS: Windows 11 Home (22631.2861) 167 | CPU: AMD Ryzen5 2600 168 | RAM: 16Gb 169 | ``` 170 | ![AMD Summary](https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/master/Images/amd_summary.png) 171 | 172 | 173 | ## Usage 174 | 175 | Here is the simplest usage example. 176 | 177 | ```cpp 178 | #include "ExcaliburHash.h" 179 | 180 | int main() 181 | { 182 | // use hash table as a hash map (key/value) 183 | Excalibur::HashTable hashMap; 184 | hashMap.emplace(13, 6); 185 | 186 | // use hash table as a hash set (key only) 187 | Excalibur::HashTable hashSet; 188 | hashSet.emplace(13); 189 | 190 | return 0; 191 | } 192 | ``` 193 | 194 | 195 | More detailed examples can be found in the unit tests folder. 196 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{branch}-ci-{build}" 2 | image: Visual Studio 2019 3 | 4 | build: msvc 5 | platform: x86 6 | configuration: Debug 7 | 8 | install: 9 | - choco install opencppcoverage codecov 10 | - set PATH=C:\Program Files\OpenCppCoverage;%PATH% 11 | 12 | build_script: 13 | - git submodule update --init --recursive 14 | - mkdir build 15 | - cd build 16 | - cmake .. 17 | - cd .. 18 | - cmake --build .\build\ --config Debug 19 | - OpenCppCoverage.exe --export_type=cobertura:cobertura.xml --stop_on_assert --sources ExcaliburHashTest*.* --sources ExcaliburHash.h --excluded_sources *googletest* --modules *.exe -- .\build\Debug\ExcaliburHashTest.exe 20 | - codecov -f cobertura.xml --root %APPVEYOR_BUILD_FOLDER% -t 6ff3ea6c-1bf4-47b4-b046-bd204afef71f 21 | -------------------------------------------------------------------------------- /codecov19.cmd: -------------------------------------------------------------------------------- 1 | if exist ./codecov_report/ (rd /S /Q ./codecov_report/) 2 | rem Install OpenCppCoverage from here https://github.com/OpenCppCoverage/OpenCppCoverage/releases 3 | OpenCppCoverage.exe --export_type html:codecov_report --stop_on_assert --sources ExcaliburHashTest*.* --sources ExcaliburHash.h --excluded_sources *googletest* --modules *.exe -- .\build2019\Debug\ExcaliburHashTest.exe -------------------------------------------------------------------------------- /data/random.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SergeyMakeev/ExcaliburHash/b99119acdf2669c7e4bf071d8fd9046a94c9c2d6/data/random.bin -------------------------------------------------------------------------------- /gen_vs19.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | set builddir=build2019 3 | if not exist %builddir% goto GENERATE 4 | del %builddir% /S /Q 5 | :GENERATE 6 | mkdir %builddir% 7 | cd %builddir% 8 | cmake -G "Visual Studio 16 2019" ../ 9 | cd .. 10 | 11 | -------------------------------------------------------------------------------- /gen_vs19_x86.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | set builddir=build2019_32 3 | if not exist %builddir% goto GENERATE 4 | del %builddir% /S /Q 5 | :GENERATE 6 | mkdir %builddir% 7 | cd %builddir% 8 | cmake -G "Visual Studio 16 2019" -A Win32 ../ 9 | cd .. 10 | 11 | -------------------------------------------------------------------------------- /gen_vs22.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | set builddir=build2022 3 | if not exist %builddir% goto GENERATE 4 | del %builddir% /S /Q 5 | :GENERATE 6 | mkdir %builddir% 7 | cd %builddir% 8 | cmake -G "Visual Studio 17 2022" ../ 9 | cd .. 10 | 11 | --------------------------------------------------------------------------------