Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions .github/workflows/ccpp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:

Expand All @@ -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
ctest --output-on-failure
29 changes: 18 additions & 11 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.6)
cmake_minimum_required(VERSION 3.20)

project(HashMap VERSION 1.0 LANGUAGES CXX)

Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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()
endif()
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion include/rigtorp/HashMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
41 changes: 35 additions & 6 deletions src/HashMapBenchmark.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// © 2017-2020 Erik Rigtorp <erik@rigtorp.se>
// SPDX-License-Identifier: MIT

#if defined(__x86_64__) || defined(_M_X64)
#include <nmmintrin.h> // _mm_crc32_u64
#endif

#include <chrono>
#include <cstdint>
#include <iostream>
#include <random>
#include <unistd.h>
Expand Down Expand Up @@ -50,9 +53,21 @@ template <typename T> 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 <typename T, typename U>
bool operator==(const huge_page_allocator<T> &,
const huge_page_allocator<U> &) noexcept {
return true;
}

template <typename T, typename U>
bool operator!=(const huge_page_allocator<T> &,
const huge_page_allocator<U> &) noexcept {
return false;
}
#else
template <typename T> using huge_page_allocator = std::allocator<T>;
#endif
Expand Down Expand Up @@ -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<size_t>(x);
#endif
}
};

auto b = [&](const char *n, auto &m) {
std::minstd_rand gen(0);
std::uniform_int_distribution<int> ud(2, count);
std::uniform_int_distribution<key> 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, {}});
Expand All @@ -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()) {
Expand Down
38 changes: 38 additions & 0 deletions src/HashMapTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, int> 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<int, int> hm(requested, -1);
const size_t expected = requested <= 4 ? 4 : requested <= 8 ? 8 : 16;
EXPECT(hm.bucket_count() == expected);
}
}

{
// bucket_count()
HashMap<int, int> hm(16, 0);
Expand Down
Loading