diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7b513443..28dbe352 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.x' - - run: ./scripts/lint/lint-all.py + - run: ./scripts/lint/lint-version.py linux: runs-on: ubuntu-latest diff --git a/CMakeLists.txt b/CMakeLists.txt index 742ee7fa..ac6a90f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.12) project("unordered_dense" - VERSION 4.1.2 + VERSION 4.2.0 DESCRIPTION "A fast & densely stored hashmap and hashset based on robin-hood backward shift deletion" HOMEPAGE_URL "https://github.com/martinus/unordered_dense") diff --git a/include/ankerl/unordered_dense.h b/include/ankerl/unordered_dense.h index e50931b3..ac874a24 100644 --- a/include/ankerl/unordered_dense.h +++ b/include/ankerl/unordered_dense.h @@ -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.1.2 +// Version 4.2.0 // https://github.com/martinus/unordered_dense // // Licensed under the MIT License . @@ -31,8 +31,8 @@ // 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 1 // NOLINT(cppcoreguidelines-macro-usage) backwards compatible functionality -#define ANKERL_UNORDERED_DENSE_VERSION_PATCH 2 // NOLINT(cppcoreguidelines-macro-usage) backwards compatible bug fixes +#define ANKERL_UNORDERED_DENSE_VERSION_MINOR 2 // 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/ @@ -767,7 +767,7 @@ class table : public std::conditional_t, base_table_type_map, bas typename std::allocator_traits::template rebind_alloc; using bucket_alloc_traits = std::allocator_traits; - static constexpr uint8_t initial_shifts = 64 - 3; // 2^(64-m_shift) number of buckets + static constexpr uint8_t initial_shifts = 64 - 2; // 2^(64-m_shift) number of buckets static constexpr float default_max_load_factor = 0.8F; public: @@ -893,7 +893,12 @@ class table : public std::conditional_t, base_table_type_map, bas // assumes m_values has data, m_buckets=m_buckets_end=nullptr, m_shifts is INITIAL_SHIFTS void copy_buckets(table const& other) { - if (!empty()) { + // assumes m_values has already the correct data copied over. + if (empty()) { + // when empty, at least allocate an initial buckets and clear them. + allocate_buckets_from_shift(); + clear_buckets(); + } else { m_shifts = other.m_shifts; allocate_buckets_from_shift(); std::memcpy(m_buckets, other.m_buckets, sizeof(Bucket) * bucket_count()); @@ -904,7 +909,7 @@ class table : public std::conditional_t, base_table_type_map, bas * True when no element can be added any more without increasing the size */ [[nodiscard]] auto is_full() const -> bool { - return size() >= m_max_bucket_capacity; + return size() > m_max_bucket_capacity; } void deallocate_buckets() { @@ -948,7 +953,9 @@ class table : public std::conditional_t, base_table_type_map, bas } void increase_size() { - if (ANKERL_UNORDERED_DENSE_UNLIKELY(m_max_bucket_capacity == max_bucket_count())) { + if (m_max_bucket_capacity == max_bucket_count()) { + // remove the value again, we can't add it! + m_values.pop_back(); on_error_bucket_overflow(); } --m_shifts; @@ -1018,27 +1025,26 @@ class table : public std::conditional_t, base_table_type_map, bas return it_isinserted; } - template - auto do_place_element(dist_and_fingerprint_type dist_and_fingerprint, value_idx_type bucket_idx, K&& key, Args&&... args) + template + auto do_place_element(dist_and_fingerprint_type dist_and_fingerprint, value_idx_type bucket_idx, Args&&... args) -> std::pair { // emplace the new value. If that throws an exception, no harm done; index is still in a valid state - m_values.emplace_back(std::piecewise_construct, - std::forward_as_tuple(std::forward(key)), - std::forward_as_tuple(std::forward(args)...)); + m_values.emplace_back(std::forward(args)...); - // place element and shift up until we find an empty spot auto value_idx = static_cast(m_values.size() - 1); - place_and_shift_up({dist_and_fingerprint, value_idx}, bucket_idx); + if (ANKERL_UNORDERED_DENSE_UNLIKELY(is_full())) { + increase_size(); + } else { + place_and_shift_up({dist_and_fingerprint, value_idx}, bucket_idx); + } + + // place element and shift up until we find an empty spot return {begin() + static_cast(value_idx), true}; } template auto do_try_emplace(K&& key, Args&&... args) -> std::pair { - if (ANKERL_UNORDERED_DENSE_UNLIKELY(is_full())) { - increase_size(); - } - auto hash = mixed_hash(key); auto dist_and_fingerprint = dist_and_fingerprint_from_hash(hash); auto bucket_idx = bucket_idx_from_hash(hash); @@ -1050,7 +1056,11 @@ class table : public std::conditional_t, base_table_type_map, bas return {begin() + static_cast(bucket->m_value_idx), false}; } } else if (dist_and_fingerprint > bucket->m_dist_and_fingerprint) { - return do_place_element(dist_and_fingerprint, bucket_idx, std::forward(key), std::forward(args)...); + return do_place_element(dist_and_fingerprint, + bucket_idx, + std::piecewise_construct, + std::forward_as_tuple(std::forward(key)), + std::forward_as_tuple(std::forward(args)...)); } dist_and_fingerprint = dist_inc(dist_and_fingerprint); bucket_idx = next(bucket_idx); @@ -1116,9 +1126,6 @@ class table : public std::conditional_t, base_table_type_map, bas } public: - table() - : table(0) {} - explicit table(size_t bucket_count, Hash const& hash = Hash(), KeyEqual const& equal = KeyEqual(), @@ -1128,9 +1135,15 @@ class table : public std::conditional_t, base_table_type_map, bas , m_equal(equal) { if (0 != bucket_count) { reserve(bucket_count); + } else { + allocate_buckets_from_shift(); + clear_buckets(); } } + table() + : table(0) {} + table(size_t bucket_count, allocator_type const& alloc) : table(bucket_count, Hash(), KeyEqual(), alloc) {} @@ -1213,9 +1226,9 @@ class table : public std::conditional_t, base_table_type_map, bas return *this; } - auto operator=(table&& other) noexcept( - noexcept(std::is_nothrow_move_assignable_v&& std::is_nothrow_move_assignable_v&& - std::is_nothrow_move_assignable_v)) -> table& { + auto operator=(table&& other) noexcept(noexcept(std::is_nothrow_move_assignable_v && + std::is_nothrow_move_assignable_v && + std::is_nothrow_move_assignable_v)) -> table& { if (&other != this) { deallocate_buckets(); // deallocate before m_values is set (might have another allocator) m_values = std::move(other.m_values); @@ -1230,6 +1243,8 @@ class table : public std::conditional_t, base_table_type_map, bas m_max_load_factor = std::exchange(other.m_max_load_factor, default_max_load_factor); m_hash = std::exchange(other.m_hash, {}); m_equal = std::exchange(other.m_equal, {}); + other.allocate_buckets_from_shift(); + other.clear_buckets(); } else { // set max_load_factor *before* copying the other's buckets, so we have the same // behavior @@ -1453,10 +1468,6 @@ class table : public std::conditional_t, base_table_type_map, bas typename KE = KeyEqual, std::enable_if_t && is_transparent_v, bool> = true> auto emplace(K&& key) -> std::pair { - if (is_full()) { - increase_size(); - } - auto hash = mixed_hash(key); auto dist_and_fingerprint = dist_and_fingerprint_from_hash(hash); auto bucket_idx = bucket_idx_from_hash(hash); @@ -1472,19 +1483,11 @@ class table : public std::conditional_t, base_table_type_map, bas } // value is new, insert element first, so when exception happens we are in a valid state - m_values.emplace_back(std::forward(key)); - // now place the bucket and shift up until we find an empty spot - auto value_idx = static_cast(m_values.size() - 1); - place_and_shift_up({dist_and_fingerprint, value_idx}, bucket_idx); - return {begin() + static_cast(value_idx), true}; + return do_place_element(dist_and_fingerprint, bucket_idx, std::forward(key)); } template auto emplace(Args&&... args) -> std::pair { - if (is_full()) { - increase_size(); - } - // we have to instantiate the value_type to be able to access the key. // 1. emplace_back the object so it is constructed. 2. If the key is already there, pop it later in the loop. auto& key = get_key(m_values.emplace_back(std::forward(args)...)); @@ -1504,8 +1507,13 @@ class table : public std::conditional_t, base_table_type_map, bas // value is new, place the bucket and shift up until we find an empty spot auto value_idx = static_cast(m_values.size() - 1); - place_and_shift_up({dist_and_fingerprint, value_idx}, bucket_idx); - + if (ANKERL_UNORDERED_DENSE_UNLIKELY(is_full())) { + // increase_size just rehashes all the data we have in m_values + increase_size(); + } else { + // place element and shift up until we find an empty spot + place_and_shift_up({dist_and_fingerprint, value_idx}, bucket_idx); + } return {begin() + static_cast(value_idx), true}; } @@ -1609,8 +1617,8 @@ class table : public std::conditional_t, base_table_type_map, bas return do_erase_key(std::forward(key)); } - void swap(table& other) noexcept(noexcept(std::is_nothrow_swappable_v&& - std::is_nothrow_swappable_v&& std::is_nothrow_swappable_v)) { + void swap(table& other) noexcept(noexcept(std::is_nothrow_swappable_v && + std::is_nothrow_swappable_v && std::is_nothrow_swappable_v)) { using std::swap; swap(other, *this); } diff --git a/meson.build b/meson.build index 632798b3..69292873 100644 --- a/meson.build +++ b/meson.build @@ -18,7 +18,7 @@ # project('unordered_dense', 'cpp', - version: '4.1.2', + version: '4.2.0', license: 'MIT', default_options : [ 'cpp_std=c++17', diff --git a/scripts/build.py b/scripts/build.py index 8a34ec85..25e0548b 100755 --- a/scripts/build.py +++ b/scripts/build.py @@ -7,39 +7,39 @@ cmd_and_dir = [ # needs honggfuzz installed - ['env', 'CXX=ccache hfuzz-clang++ -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION', 'meson', 'setup', '--buildtype', 'release', '-Dcpp_std=c++17', 'builddir/hfuzz-clang_cpp17_release'], - ['env', 'CXX=ccache clang++', 'meson', 'setup', '--buildtype', 'release', '-Dcpp_std=c++17', 'builddir/clang_cpp17_release'], - ['env', 'CXX=ccache clang++', 'meson', 'setup', '--buildtype', 'debug', '-Dcpp_std=c++17', 'builddir/clang_cpp17_debug'], - ['env', 'CXX=ccache g++', 'meson', 'setup', '--buildtype', 'release', '-Dcpp_std=c++17', 'builddir/gcc_cpp17_release'], - ['env', 'CXX=ccache g++', 'meson', 'setup', '--buildtype', 'debug', '-Dcpp_std=c++17', 'builddir/gcc_cpp17_debug'], + ['env', 'CXX_LD=mold', 'CXX=ccache g++', 'meson', 'setup', '--buildtype', 'release', '-Dcpp_std=c++17', 'builddir/gcc_cpp17_release'], + ['env', 'CXX_LD=mold', 'CXX=ccache clang++', 'meson', 'setup', '--buildtype', 'release', '-Dcpp_std=c++17', 'builddir/clang_cpp17_release'], + ['env', 'CXX_LD=mold', 'CXX=ccache hfuzz-clang++ -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION', 'meson', 'setup', '--buildtype', 'release', '-Dcpp_std=c++17', 'builddir/hfuzz-clang_cpp17_release'], + ['env', 'CXX_LD=mold', 'CXX=ccache clang++', 'meson', 'setup', '--buildtype', 'debug', '-Dcpp_std=c++17', 'builddir/clang_cpp17_debug'], + ['env', 'CXX_LD=mold', 'CXX=ccache g++', 'meson', 'setup', '--buildtype', 'debug', '-Dcpp_std=c++17', 'builddir/gcc_cpp17_debug'], # 32bit. Install lib32-clang - ['env', 'CXX=ccache g++', 'meson', 'setup', '--buildtype', 'debug', '-Dcpp_std=c++17', '-Dcpp_args=-m32', '-Dcpp_link_args=-m32', '-Dc_args=-m32', '-Dc_link_args=-m32', 'builddir/gcc_cpp17_debug_32'], - ['env', 'CXX=ccache clang++', 'meson', 'setup', '--buildtype', 'debug', '-Dcpp_std=c++17', '-Dcpp_args=-m32', '-Dcpp_link_args=-m32', '-Dc_args=-m32', '-Dc_link_args=-m32', 'builddir/clang_cpp17_debug_32'], + ['env', 'CXX_LD=mold', 'CXX=ccache g++', 'meson', 'setup', '--buildtype', 'debug', '-Dcpp_std=c++17', '-Dcpp_args=-m32', '-Dcpp_link_args=-m32', '-Dc_args=-m32', '-Dc_link_args=-m32', 'builddir/gcc_cpp17_debug_32'], + ['env', 'CXX_LD=mold', 'CXX=ccache clang++', 'meson', 'setup', '--buildtype', 'debug', '-Dcpp_std=c++17', '-Dcpp_args=-m32', '-Dcpp_link_args=-m32', '-Dc_args=-m32', '-Dc_link_args=-m32', 'builddir/clang_cpp17_debug_32'], # c++20 - ['env', 'CXX=ccache clang++', 'meson', 'setup', '--buildtype', 'debug', '-Dcpp_std=c++20', 'builddir/clang_cpp20_debug'], - ['env', 'CXX=ccache g++', 'meson', 'setup', '--buildtype', 'debug', '-Dcpp_std=c++20', 'builddir/gcc_cpp20_debug'], + ['env', 'CXX_LD=mold', 'CXX=ccache clang++', 'meson', 'setup', '--buildtype', 'debug', '-Dcpp_std=c++20', 'builddir/clang_cpp20_debug'], + ['env', 'CXX_LD=mold', 'CXX=ccache g++', 'meson', 'setup', '--buildtype', 'debug', '-Dcpp_std=c++20', 'builddir/gcc_cpp20_debug'], # coverage; use "ninja clean && ninja test && ninja coverage" - ['env', 'CXX=ccache clang++', 'meson', 'setup', '-Db_coverage=true', 'builddir/coverage'], + #['env', 'CXX_LD=mold', 'CXX=ccache clang++', 'meson', 'setup', '-Db_coverage=true', 'builddir/coverage'], # sanitizers # It is not possible to combine more than one of the -fsanitize=address, -fsanitize=thread, and -fsanitize=memory checkers in the same program. # see https://clang.llvm.org/docs/UsersManual.html#controlling-code-generation # # can't use ccache, it doesn't work with the ignorelist.txt - ['env', 'CXX=ccache g++', 'meson', 'setup', '-Db_sanitize=address', 'builddir/gcc_sanitize_address'], - ['env', 'CXX=ccache clang++', 'meson', 'setup', '-Db_sanitize=address', 'builddir/clang_sanitize_address'], + ['env', 'CXX_LD=mold', 'CXX=ccache g++', 'meson', 'setup', '-Db_sanitize=address', 'builddir/gcc_sanitize_address'], + ['env', 'CXX_LD=mold', 'CXX=ccache clang++', 'meson', 'setup', '-Db_sanitize=address', 'builddir/clang_sanitize_address'], - ['env', 'CXX=ccache g++', 'meson', 'setup', '-Db_sanitize=thread', 'builddir/gcc_sanitize_thread'], - ['env', 'CXX=ccache clang++', 'meson', 'setup', '-Db_sanitize=thread', 'builddir/clang_sanitize_thread'], + ['env', 'CXX_LD=mold', 'CXX=ccache g++', 'meson', 'setup', '-Db_sanitize=thread', 'builddir/gcc_sanitize_thread'], + ['env', 'CXX_LD=mold', 'CXX=ccache clang++', 'meson', 'setup', '-Db_sanitize=thread', 'builddir/clang_sanitize_thread'], - # ['env', 'CXX=ccache g++', 'meson', 'setup', '-Db_sanitize=memory', 'builddir/gcc_sanitize_memory'], # doesn't work due to STL, and ignore doesn't work either :-( - # ['env', 'CXX=ccache clang++', 'meson', 'setup', '-Db_sanitize=memory', 'builddir/clang_sanitize_memory'], # doesn't work due to STL, and ignore doesn't work either :-( + # ['env', 'CXX_LD=mold', 'CXX=ccache g++', 'meson', 'setup', '-Db_sanitize=memory', 'builddir/gcc_sanitize_memory'], # doesn't work due to STL, and ignore doesn't work either :-( + # ['env', 'CXX_LD=mold', 'CXX=ccache clang++', 'meson', 'setup', '-Db_sanitize=memory', 'builddir/clang_sanitize_memory'], # doesn't work due to STL, and ignore doesn't work either :-( - ['env', 'CXX=ccache g++', 'meson', 'setup', '-Db_sanitize=undefined', 'builddir/gcc_sanitize_undefined'], - ['env', 'CXX=ccache clang++', 'meson', 'setup', '-Db_sanitize=undefined', 'builddir/clang_sanitize_undefined'], + ['env', 'CXX_LD=mold', 'CXX=ccache g++', 'meson', 'setup', '-Db_sanitize=undefined', 'builddir/gcc_sanitize_undefined'], + ['env', 'CXX_LD=mold', 'CXX=ccache clang++', 'meson', 'setup', '-Db_sanitize=undefined', 'builddir/clang_sanitize_undefined'], ] root_path = Path(__file__).parent.parent diff --git a/scripts/lint/lint-clang-format.py b/scripts/lint/lint-clang-format.py index 563987bb..d10a5c4d 100755 --- a/scripts/lint/lint-clang-format.py +++ b/scripts/lint/lint-clang-format.py @@ -17,8 +17,8 @@ f"{root_path}/test/**/*.cpp", ] exclusions = [ - "nanobench\.h", - "FuzzedDataProvider\.h", + "nanobench\\.h", + "FuzzedDataProvider\\.h", '/third-party/'] files = [] diff --git a/test/bench/game_of_life.cpp b/test/bench/game_of_life.cpp index a005a50f..1e29354c 100644 --- a/test/bench/game_of_life.cpp +++ b/test/bench/game_of_life.cpp @@ -9,7 +9,9 @@ #include #if __has_include("boost/unordered/unordered_flat_map.hpp") -# pragma clang diagnostic ignored "-Wold-style-cast" +# if defined(__clang__) +# pragma clang diagnostic ignored "-Wold-style-cast" +# endif # include "boost/unordered/unordered_flat_map.hpp" # define HAS_BOOST_UNORDERED_FLAT_MAP() 1 // NOLINT(cppcoreguidelines-macro-usage) #else @@ -17,8 +19,10 @@ #endif #if 0 && __has_include("absl/container/flat_hash_map.h") -# pragma clang diagnostic ignored "-Wdeprecated-builtins" -# pragma clang diagnostic ignored "-Wsign-conversion" +# if defined(__clang__) +# pragma clang diagnostic ignored "-Wdeprecated-builtins" +# pragma clang diagnostic ignored "-Wsign-conversion" +# endif # include # define HAS_ABSL() 1 // NOLINT(cppcoreguidelines-macro-usage) #else diff --git a/test/bench/quick_overall_map.cpp b/test/bench/quick_overall_map.cpp index c64ca9a0..50ed5c2c 100644 --- a/test/bench/quick_overall_map.cpp +++ b/test/bench/quick_overall_map.cpp @@ -26,7 +26,7 @@ inline auto init_key() -> K { template inline void randomize_key(ankerl::nanobench::Rng* rng, int n, T* key) { - // we limit ourselfes to 32bit n + // we limit ourselves to 32bit n auto limited = (((*rng)() >> 32U) * static_cast(n)) >> 32U; *key = static_cast(limited); } diff --git a/test/bench/show_allocations.cpp b/test/bench/show_allocations.cpp index 55365a06..2158e192 100644 --- a/test/bench/show_allocations.cpp +++ b/test/bench/show_allocations.cpp @@ -6,8 +6,10 @@ #include #if __has_include("boost/unordered/unordered_flat_map.hpp") -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wold-style-cast" +# if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wold-style-cast" +# endif # include "boost/unordered/unordered_flat_map.hpp" # define HAS_BOOST_UNORDERED_FLAT_MAP() 1 // NOLINT(cppcoreguidelines-macro-usage) #else diff --git a/test/meson.build b/test/meson.build index fedb6a6f..c2c724cd 100644 --- a/test/meson.build +++ b/test/meson.build @@ -88,6 +88,7 @@ foreach arg : [ '-Wno-stringop-overflow', # g++ error in fmtlib '-Warith-conversion', '-Wshadow=global', + '-Wno-array-bounds', # gcc 13 gives incorrect warning # gcc / clang '-Wconversion', @@ -154,6 +155,7 @@ cpp_args += [ link_args += [ #'-fsanitize=undefined,address', #'-fno-sanitize=thread' + #'-Wl,-shuffle-sections' # for benchmarking with mold linker ] test_exe = executable( diff --git a/test/unit/bucket.cpp b/test/unit/bucket.cpp index b862afe9..da6f1047 100644 --- a/test/unit/bucket.cpp +++ b/test/unit/bucket.cpp @@ -56,7 +56,11 @@ TEST_CASE_MAP("bucket_micro", INFO(counts); auto map = map_t(); + INFO("map_t::max_size()=" << map_t::max_size()); for (size_t i = 0; i < map_t::max_size(); ++i) { + if (i == 255) { + INFO("i=" << i); + } auto const r = map.try_emplace({i, counts}, i, counts); REQUIRE(r.second); diff --git a/test/unit/ctors.cpp b/test/unit/ctors.cpp index 42e27906..51ca9691 100644 --- a/test/unit/ctors.cpp +++ b/test/unit/ctors.cpp @@ -73,7 +73,8 @@ TEST_CASE_MAP("ctors_map", counter::obj, counter::obj) { TEST_CASE_MAP("ctor_bucket_count_map", counter::obj, counter::obj) { { auto m = map_t{}; - REQUIRE(m.bucket_count() == 0U); + // depends on initial bucket count, could also be 0 + // REQUIRE(m.bucket_count() == 2U); } { auto m = map_t{150U}; diff --git a/test/unit/namespace.cpp b/test/unit/namespace.cpp index 27c21d03..22ba24aa 100644 --- a/test/unit/namespace.cpp +++ b/test/unit/namespace.cpp @@ -2,7 +2,7 @@ #include -namespace versioned_namespace = ankerl::unordered_dense::v4_1_2; +namespace versioned_namespace = ankerl::unordered_dense::v4_2_0; static_assert(std::is_same_v, ankerl::unordered_dense::map>); static_assert(std::is_same_v, ankerl::unordered_dense::hash>); diff --git a/test/unit/pmr.cpp b/test/unit/pmr.cpp index b7613894..8703fbcb 100644 --- a/test/unit/pmr.cpp +++ b/test/unit/pmr.cpp @@ -223,7 +223,7 @@ TEST_CASE("pmr_move_same_mr") { REQUIRE(map1.find(3) != map1.end()); show(mr1, "mr1"); - REQUIRE(mr1.num_allocs() == 4); + REQUIRE(mr1.num_allocs() == 5); // 5 because of the initial allocation REQUIRE(mr1.num_deallocs() == 2); REQUIRE(mr1.num_is_equals() == 0); }