Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide high quality & fast hash implementations for std::tuple and std::pair #101

Merged
merged 2 commits into from
Dec 18, 2023
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
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.12)
project("unordered_dense"
VERSION 4.2.0
VERSION 4.3.0
DESCRIPTION "A fast & densely stored hashmap and hashset based on robin-hood backward shift deletion"
HOMEPAGE_URL "https://github.com/martinus/unordered_dense")

Expand Down
60 changes: 58 additions & 2 deletions include/ankerl/unordered_dense.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
///////////////////////// ankerl::unordered_dense::{map, set} /////////////////////////

// A fast & densely stored hashmap and hashset based on robin-hood backward shift deletion.
// Version 4.2.0
// Version 4.3.0
// https://github.com/martinus/unordered_dense
//
// Licensed under the MIT License <http://opensource.org/licenses/MIT>.
Expand Down Expand Up @@ -31,7 +31,7 @@

// see https://semver.org/spec/v2.0.0.html
#define ANKERL_UNORDERED_DENSE_VERSION_MAJOR 4 // NOLINT(cppcoreguidelines-macro-usage) incompatible API changes
#define ANKERL_UNORDERED_DENSE_VERSION_MINOR 2 // NOLINT(cppcoreguidelines-macro-usage) backwards compatible functionality
#define ANKERL_UNORDERED_DENSE_VERSION_MINOR 3 // NOLINT(cppcoreguidelines-macro-usage) backwards compatible functionality
#define ANKERL_UNORDERED_DENSE_VERSION_PATCH 0 // NOLINT(cppcoreguidelines-macro-usage) backwards compatible bug fixes

// API versioning with inline namespace, see https://www.foonathan.net/2018/11/inline-namespaces/
Expand Down Expand Up @@ -328,6 +328,62 @@ struct hash<Enum, typename std::enable_if<std::is_enum<Enum>::value>::type> {
}
};

template <typename... Args>
struct tuple_hash_helper {
template <typename Arg>
[[nodiscard]] constexpr static auto calc_buf_size() {
if constexpr (std::has_unique_object_representations_v<Arg>) {
return sizeof(Arg);
} else {
return sizeof(hash<Arg>{}(std::declval<Arg>()));
}
}

// Reads data from back to front. We do this so there's no need for bswap when multiple
// bytes are read (on little endian). This should be a tiny bit faster.
template <typename Arg>
[[nodiscard]] constexpr static auto put(std::byte* pos, Arg const& arg) -> std::byte* {
if constexpr (std::has_unique_object_representations_v<Arg>) {
pos -= sizeof(Arg);
std::memcpy(pos, &arg, sizeof(Arg));
return pos;
} else {
auto x = hash<Arg>{}(arg);
pos -= sizeof(x);
std::memcpy(pos, &x, sizeof(x));
return pos;
}
}

// Creates a buffer that holds all the data from each element of the tuple. If possible we memcpy the data directly. If
// not, we hash the object and use this for the array. Size of the array is known at compile time, and memcpy is optimized
// away, so filling the buffer is highly efficient. Finally, call wyhash with this buffer.
template <typename T, std::size_t... Idx>
[[nodiscard]] static auto calc_hash(T const& t, std::index_sequence<Idx...>) noexcept -> uint64_t {
std::array<std::byte, (calc_buf_size<Args>() + ...)> tmp_buffer;
auto* buf_ptr = tmp_buffer.data() + tmp_buffer.size();
((buf_ptr = put(buf_ptr, std::get<Idx>(t))), ...);
// at this point, buf_ptr==tmp_buffer.data()
return ankerl::unordered_dense::detail::wyhash::hash(tmp_buffer.data(), tmp_buffer.size());
}
};

template <typename... Args>
struct hash<std::tuple<Args...>> : tuple_hash_helper<Args...> {
using is_avalanching = void;
auto operator()(std::tuple<Args...> const& t) const noexcept -> uint64_t {
return tuple_hash_helper<Args...>::calc_hash(t, std::index_sequence_for<Args...>{});
}
};

template <typename A, typename B>
struct hash<std::pair<A, B>> : tuple_hash_helper<A, B> {
using is_avalanching = void;
auto operator()(std::pair<A, B> const& t) const noexcept -> uint64_t {
return tuple_hash_helper<A, B>::calc_hash(t, std::index_sequence_for<A, B>{});
}
};

// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
# define ANKERL_UNORDERED_DENSE_HASH_STATICCAST(T) \
template <> \
Expand Down
2 changes: 1 addition & 1 deletion meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
#

project('unordered_dense', 'cpp',
version: '4.2.0',
version: '4.3.0',
license: 'MIT',
default_options : [
'cpp_std=c++17',
Expand Down
1 change: 1 addition & 0 deletions test/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ test_sources = [
'unit/swap.cpp',
'unit/transparent.cpp',
'unit/try_emplace.cpp',
'unit/tuple_hash.cpp',
'unit/unique_ptr.cpp',
'unit/unordered_set.cpp',
'unit/vectorofmaps.cpp',
Expand Down
2 changes: 1 addition & 1 deletion test/unit/namespace.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

#include <doctest.h>

namespace versioned_namespace = ankerl::unordered_dense::v4_2_0;
namespace versioned_namespace = ankerl::unordered_dense::v4_3_0;

static_assert(std::is_same_v<versioned_namespace::map<int, int>, ankerl::unordered_dense::map<int, int>>);
static_assert(std::is_same_v<versioned_namespace::hash<int>, ankerl::unordered_dense::hash<int>>);
Expand Down
26 changes: 26 additions & 0 deletions test/unit/tuple_hash.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#include <ankerl/unordered_dense.h>

#include <app/doctest.h>

TEST_CASE("tuple_hash") {
auto m = ankerl::unordered_dense::map<std::pair<int, std::string>, int>();
auto pair_hash = ankerl::unordered_dense::hash<std::pair<int, std::string>>{};
REQUIRE(pair_hash(std::pair<int, std::string>{1, "a"}) != pair_hash(std::pair<int, std::string>{1, "b"}));

m.try_emplace({1, "a"}, 23);
m.try_emplace({1, "b"}, 42);
REQUIRE(m.size() == 2U);
}

TEST_CASE("good_tuple_hash") {
auto hashes = ankerl::unordered_dense::set<uint64_t>();

auto t = std::tuple<uint8_t, uint8_t, uint8_t>();
for (size_t i = 0; i < 256 * 256; ++i) {
std::get<0>(t) = static_cast<uint8_t>(i);
std::get<2>(t) = static_cast<uint8_t>(i / 256);
hashes.emplace(ankerl::unordered_dense::hash<decltype(t)>{}(t));
}

REQUIRE(hashes.size() == 256 * 256);
}
Loading