diff --git a/.github/workflows/ccpp.yml b/.github/workflows/ccpp.yml index ecef34a..af19e52 100644 --- a/.github/workflows/ccpp.yml +++ b/.github/workflows/ccpp.yml @@ -19,10 +19,7 @@ jobs: build-windows: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [windows-latest, windows-2016] + runs-on: windows-latest steps: - uses: actions/checkout@v1 @@ -32,7 +29,7 @@ jobs: cmake -B build -S . cmake --build build --config Debug cd build - ctest --output-on-failure + ctest -C Debug --output-on-failure build-macos: @@ -46,4 +43,4 @@ jobs: cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-Werror -O2 -fsanitize=address,undefined" cmake --build build cd build - ctest --output-on-failure \ No newline at end of file + ctest --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index 4dda1a1..c72c478 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.6) +cmake_minimum_required(VERSION 3.20) project(HashMap VERSION 1.0 LANGUAGES CXX) @@ -19,15 +19,19 @@ if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) add_compile_options(-Wall -Wextra -Wpedantic) endif() - find_package(absl) - - add_executable(HashMapBenchmark src/HashMapBenchmark.cpp) - target_link_libraries(HashMapBenchmark HashMap) - if (absl_FOUND) - target_link_libraries(HashMapBenchmark absl::flat_hash_map) + if(NOT WIN32) + find_package(absl) + + add_executable(HashMapBenchmark src/HashMapBenchmark.cpp) + target_link_libraries(HashMapBenchmark HashMap) + if(absl_FOUND) + target_link_libraries(HashMapBenchmark absl::flat_hash_map) + endif() + if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$") + target_compile_options(HashMapBenchmark PRIVATE -msse4.2) + endif() + target_compile_features(HashMapBenchmark PRIVATE cxx_std_17) endif() - target_compile_options(HashMapBenchmark PRIVATE -mavx2) - target_compile_features(HashMapBenchmark INTERFACE cxx_std_17) add_executable(HashMapExample src/HashMapExample.cpp) target_link_libraries(HashMapExample HashMap) @@ -36,7 +40,10 @@ if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) target_link_libraries(HashMapTest HashMap) enable_testing() - add_test(HashMapTest HashMapTest) + add_test(NAME HashMapTest COMMAND HashMapTest) + if(TARGET HashMapBenchmark) + add_test(NAME HashMapBenchmarkSmoke COMMAND HashMapBenchmark -t 4 -c 1000 -i 1000) + endif() endif() # Install @@ -76,4 +83,4 @@ if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}" ) -endif() \ No newline at end of file +endif() diff --git a/README.md b/README.md index 7b22d29..cab3367 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,12 @@ The rest of the member functions are implemented as for A benchmark `src/HashMapBenchmark.cpp` is included with the sources. The benchmark simulates a delete heavy workload where items are repeatedly inserted -and deleted. +and deleted. + +The benchmark is built on Linux and macOS; Windows builds the example and tests. +All containers in a run use the same hash: hardware CRC32 on x86-64 and +MurmurHash3's `fmix64` integer mixer on ARM and other architectures. Hash choices +differ across architectures, so compare containers within the same run. I ran this benchmark on the following configuration: diff --git a/include/rigtorp/HashMap.h b/include/rigtorp/HashMap.h index 392b269..01758e8 100644 --- a/include/rigtorp/HashMap.h +++ b/include/rigtorp/HashMap.h @@ -99,7 +99,7 @@ class HashMap { HashMap(size_type bucket_count, key_type empty_key, const allocator_type &alloc = allocator_type()) : empty_key_(empty_key), buckets_(alloc) { - size_t pow2 = 1; + size_t pow2 = 2; while (pow2 < bucket_count) { pow2 <<= 1; } diff --git a/src/HashMapBenchmark.cpp b/src/HashMapBenchmark.cpp index 34df526..1fe0a73 100644 --- a/src/HashMapBenchmark.cpp +++ b/src/HashMapBenchmark.cpp @@ -1,9 +1,12 @@ // © 2017-2020 Erik Rigtorp // SPDX-License-Identifier: MIT +#if defined(__x86_64__) || defined(_M_X64) #include // _mm_crc32_u64 +#endif #include +#include #include #include #include @@ -50,9 +53,21 @@ template struct huge_page_allocator { } void deallocate(T *p, std::size_t n) { - munmap(p, round_to_huge_page_size(n)); + munmap(p, round_to_huge_page_size(n * sizeof(T))); } }; + +template +bool operator==(const huge_page_allocator &, + const huge_page_allocator &) noexcept { + return true; +} + +template +bool operator!=(const huge_page_allocator &, + const huge_page_allocator &) noexcept { + return false; +} #else template using huge_page_allocator = std::allocator; #endif @@ -98,21 +113,35 @@ int main(int argc, char *argv[]) { }; struct hash { - size_t operator()(size_t h) const noexcept { return _mm_crc32_u64(0, h); } + size_t operator()(size_t h) const noexcept { +#if defined(__x86_64__) || defined(_M_X64) + return _mm_crc32_u64(0, h); +#else + // MurmurHash3's public-domain fmix64 finalizer, by Austin Appleby: + // https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp + uint64_t x = h; + x ^= x >> 33; + x *= UINT64_C(0xff51afd7ed558ccd); + x ^= x >> 33; + x *= UINT64_C(0xc4ceb9fe1a85ec53); + x ^= x >> 33; + return static_cast(x); +#endif + } }; auto b = [&](const char *n, auto &m) { std::minstd_rand gen(0); - std::uniform_int_distribution ud(2, count); + std::uniform_int_distribution ud(2, count); for (size_t i = 0; i < count; ++i) { - const int val = ud(gen); + const key val = ud(gen); m.insert({val, {}}); } auto start = steady_clock::now(); for (size_t i = 0; i < iters; ++i) { - const int val = ud(gen); + const key val = ud(gen); const auto it = m.find(val); if (it == m.end()) { m.insert({val, {}}); @@ -125,7 +154,7 @@ int main(int argc, char *argv[]) { nanoseconds max = {}; for (size_t i = 0; i < iters; ++i) { - const int val = ud(gen); + const key val = ud(gen); auto start = steady_clock::now(); const auto it = m.find(val); if (it == m.end()) { diff --git a/src/HashMapTest.cpp b/src/HashMapTest.cpp index 9fad462..12b3f1e 100644 --- a/src/HashMapTest.cpp +++ b/src/HashMapTest.cpp @@ -467,6 +467,44 @@ int main(int argc, char *argv[]) { } // Bucket interface + { + // Small maps can hold their first element without growing. + for (size_t requested : {0, 1, 2}) { + HashMap hm(requested, -1); + EXPECT(hm.empty()); + EXPECT(hm.bucket_count() == 2); + EXPECT(hm.find(0) == hm.end()); + hm.emplace(0, 42); + EXPECT(hm.bucket_count() == 2); + EXPECT(hm.size() == 1); + EXPECT(hm.at(0) == 42); + hm.reserve(1); + EXPECT(hm.bucket_count() == 2); + hm.emplace(1, 43); + EXPECT(hm.bucket_count() == 4); + EXPECT(hm.at(0) == 42); + EXPECT(hm.at(1) == 43); + EXPECT(hm.erase(0) == 1); + EXPECT(hm.at(1) == 43); + hm.clear(); + hm.rehash(0); + EXPECT(hm.empty()); + EXPECT(hm.bucket_count() == 2); + hm.emplace(0, 44); + EXPECT(hm.bucket_count() == 2); + EXPECT(hm.at(0) == 44); + } + } + + { + // Larger requests still round up to the next power of two. + for (size_t requested : {3, 4, 5, 8, 9}) { + HashMap hm(requested, -1); + const size_t expected = requested <= 4 ? 4 : requested <= 8 ? 8 : 16; + EXPECT(hm.bucket_count() == expected); + } + } + { // bucket_count() HashMap hm(16, 0);