// Copyright 2018 The Abseil Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "absl/container/internal/raw_hash_set.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/base/attributes.h" #include "absl/base/config.h" #include "absl/base/internal/cycleclock.h" #include "absl/base/prefetch.h" #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/container/internal/container_memory.h" #include "absl/container/internal/hash_function_defaults.h" #include "absl/container/internal/hash_policy_testing.h" #include "absl/container/internal/hashtable_debug.h" #include "absl/container/internal/hashtablez_sampler.h" #include "absl/container/internal/test_allocator.h" #include "absl/container/internal/test_instance_tracker.h" #include "absl/container/node_hash_set.h" #include "absl/functional/function_ref.h" #include "absl/hash/hash.h" #include "absl/log/check.h" #include "absl/log/log.h" #include "absl/memory/memory.h" #include "absl/meta/type_traits.h" #include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" #include "absl/types/optional.h" namespace absl { ABSL_NAMESPACE_BEGIN namespace container_internal { struct RawHashSetTestOnlyAccess { template static auto GetCommon(const C& c) -> decltype(c.common()) { return c.common(); } template static auto GetSlots(const C& c) -> decltype(c.slot_array()) { return c.slot_array(); } template static size_t CountTombstones(const C& c) { return c.common().TombstonesCount(); } }; namespace { using ::testing::ElementsAre; using ::testing::ElementsAreArray; using ::testing::Eq; using ::testing::Ge; using ::testing::Lt; using ::testing::Pair; using ::testing::UnorderedElementsAre; // Convenience function to static cast to ctrl_t. ctrl_t CtrlT(int i) { return static_cast(i); } TEST(GrowthInfoTest, GetGrowthLeft) { GrowthInfo gi; gi.InitGrowthLeftNoDeleted(5); EXPECT_EQ(gi.GetGrowthLeft(), 5); gi.OverwriteFullAsDeleted(); EXPECT_EQ(gi.GetGrowthLeft(), 5); } TEST(GrowthInfoTest, HasNoDeleted) { GrowthInfo gi; gi.InitGrowthLeftNoDeleted(5); EXPECT_TRUE(gi.HasNoDeleted()); gi.OverwriteFullAsDeleted(); EXPECT_FALSE(gi.HasNoDeleted()); // After reinitialization we have no deleted slots. gi.InitGrowthLeftNoDeleted(5); EXPECT_TRUE(gi.HasNoDeleted()); } TEST(GrowthInfoTest, HasNoDeletedAndGrowthLeft) { GrowthInfo gi; gi.InitGrowthLeftNoDeleted(5); EXPECT_TRUE(gi.HasNoDeletedAndGrowthLeft()); gi.OverwriteFullAsDeleted(); EXPECT_FALSE(gi.HasNoDeletedAndGrowthLeft()); gi.InitGrowthLeftNoDeleted(0); EXPECT_FALSE(gi.HasNoDeletedAndGrowthLeft()); gi.OverwriteFullAsDeleted(); EXPECT_FALSE(gi.HasNoDeletedAndGrowthLeft()); // After reinitialization we have no deleted slots. gi.InitGrowthLeftNoDeleted(5); EXPECT_TRUE(gi.HasNoDeletedAndGrowthLeft()); } TEST(GrowthInfoTest, HasNoGrowthLeftAndNoDeleted) { GrowthInfo gi; gi.InitGrowthLeftNoDeleted(1); EXPECT_FALSE(gi.HasNoGrowthLeftAndNoDeleted()); gi.OverwriteEmptyAsFull(); EXPECT_TRUE(gi.HasNoGrowthLeftAndNoDeleted()); gi.OverwriteFullAsDeleted(); EXPECT_FALSE(gi.HasNoGrowthLeftAndNoDeleted()); gi.OverwriteFullAsEmpty(); EXPECT_FALSE(gi.HasNoGrowthLeftAndNoDeleted()); gi.InitGrowthLeftNoDeleted(0); EXPECT_TRUE(gi.HasNoGrowthLeftAndNoDeleted()); gi.OverwriteFullAsEmpty(); EXPECT_FALSE(gi.HasNoGrowthLeftAndNoDeleted()); } TEST(GrowthInfoTest, OverwriteFullAsEmpty) { GrowthInfo gi; gi.InitGrowthLeftNoDeleted(5); gi.OverwriteFullAsEmpty(); EXPECT_EQ(gi.GetGrowthLeft(), 6); gi.OverwriteFullAsDeleted(); EXPECT_EQ(gi.GetGrowthLeft(), 6); gi.OverwriteFullAsEmpty(); EXPECT_EQ(gi.GetGrowthLeft(), 7); EXPECT_FALSE(gi.HasNoDeleted()); } TEST(GrowthInfoTest, OverwriteEmptyAsFull) { GrowthInfo gi; gi.InitGrowthLeftNoDeleted(5); gi.OverwriteEmptyAsFull(); EXPECT_EQ(gi.GetGrowthLeft(), 4); gi.OverwriteFullAsDeleted(); EXPECT_EQ(gi.GetGrowthLeft(), 4); gi.OverwriteEmptyAsFull(); EXPECT_EQ(gi.GetGrowthLeft(), 3); EXPECT_FALSE(gi.HasNoDeleted()); } TEST(GrowthInfoTest, OverwriteControlAsFull) { GrowthInfo gi; gi.InitGrowthLeftNoDeleted(5); gi.OverwriteControlAsFull(ctrl_t::kEmpty); EXPECT_EQ(gi.GetGrowthLeft(), 4); gi.OverwriteControlAsFull(ctrl_t::kDeleted); EXPECT_EQ(gi.GetGrowthLeft(), 4); gi.OverwriteFullAsDeleted(); gi.OverwriteControlAsFull(ctrl_t::kDeleted); // We do not count number of deleted, so the bit sticks till the next rehash. EXPECT_FALSE(gi.HasNoDeletedAndGrowthLeft()); EXPECT_FALSE(gi.HasNoDeleted()); } TEST(Util, NormalizeCapacity) { EXPECT_EQ(1, NormalizeCapacity(0)); EXPECT_EQ(1, NormalizeCapacity(1)); EXPECT_EQ(3, NormalizeCapacity(2)); EXPECT_EQ(3, NormalizeCapacity(3)); EXPECT_EQ(7, NormalizeCapacity(4)); EXPECT_EQ(7, NormalizeCapacity(7)); EXPECT_EQ(15, NormalizeCapacity(8)); EXPECT_EQ(15, NormalizeCapacity(15)); EXPECT_EQ(15 * 2 + 1, NormalizeCapacity(15 + 1)); EXPECT_EQ(15 * 2 + 1, NormalizeCapacity(15 + 2)); } TEST(Util, GrowthAndCapacity) { // Verify that GrowthToCapacity gives the minimum capacity that has enough // growth. for (size_t growth = 0; growth < 10000; ++growth) { SCOPED_TRACE(growth); size_t capacity = NormalizeCapacity(GrowthToLowerboundCapacity(growth)); // The capacity is large enough for `growth`. EXPECT_THAT(CapacityToGrowth(capacity), Ge(growth)); // For (capacity+1) < kWidth, growth should equal capacity. if (capacity + 1 < Group::kWidth) { EXPECT_THAT(CapacityToGrowth(capacity), Eq(capacity)); } else { EXPECT_THAT(CapacityToGrowth(capacity), Lt(capacity)); } if (growth != 0 && capacity > 1) { // There is no smaller capacity that works. EXPECT_THAT(CapacityToGrowth(capacity / 2), Lt(growth)); } } for (size_t capacity = Group::kWidth - 1; capacity < 10000; capacity = 2 * capacity + 1) { SCOPED_TRACE(capacity); size_t growth = CapacityToGrowth(capacity); EXPECT_THAT(growth, Lt(capacity)); EXPECT_LE(GrowthToLowerboundCapacity(growth), capacity); EXPECT_EQ(NormalizeCapacity(GrowthToLowerboundCapacity(growth)), capacity); } } TEST(Util, probe_seq) { probe_seq<16> seq(0, 127); auto gen = [&]() { size_t res = seq.offset(); seq.next(); return res; }; std::vector offsets(8); std::generate_n(offsets.begin(), 8, gen); EXPECT_THAT(offsets, ElementsAre(0, 16, 48, 96, 32, 112, 80, 64)); seq = probe_seq<16>(128, 127); std::generate_n(offsets.begin(), 8, gen); EXPECT_THAT(offsets, ElementsAre(0, 16, 48, 96, 32, 112, 80, 64)); } TEST(BitMask, Smoke) { EXPECT_FALSE((BitMask(0))); EXPECT_TRUE((BitMask(5))); EXPECT_THAT((BitMask(0)), ElementsAre()); EXPECT_THAT((BitMask(0x1)), ElementsAre(0)); EXPECT_THAT((BitMask(0x2)), ElementsAre(1)); EXPECT_THAT((BitMask(0x3)), ElementsAre(0, 1)); EXPECT_THAT((BitMask(0x4)), ElementsAre(2)); EXPECT_THAT((BitMask(0x5)), ElementsAre(0, 2)); EXPECT_THAT((BitMask(0x55)), ElementsAre(0, 2, 4, 6)); EXPECT_THAT((BitMask(0xAA)), ElementsAre(1, 3, 5, 7)); } TEST(BitMask, WithShift_MatchPortable) { // See the non-SSE version of Group for details on what this math is for. uint64_t ctrl = 0x1716151413121110; uint64_t hash = 0x12; constexpr uint64_t lsbs = 0x0101010101010101ULL; auto x = ctrl ^ (lsbs * hash); uint64_t mask = (x - lsbs) & ~x & kMsbs8Bytes; EXPECT_EQ(0x0000000080800000, mask); BitMask b(mask); EXPECT_EQ(*b, 2); } constexpr uint64_t kSome8BytesMask = /* */ 0x8000808080008000ULL; constexpr uint64_t kSome8BytesMaskAllOnes = 0xff00ffffff00ff00ULL; constexpr auto kSome8BytesMaskBits = std::array{1, 3, 4, 5, 7}; TEST(BitMask, WithShift_FullMask) { EXPECT_THAT((BitMask(kMsbs8Bytes)), ElementsAre(0, 1, 2, 3, 4, 5, 6, 7)); EXPECT_THAT( (BitMask(kMsbs8Bytes)), ElementsAre(0, 1, 2, 3, 4, 5, 6, 7)); EXPECT_THAT( (BitMask(~uint64_t{0})), ElementsAre(0, 1, 2, 3, 4, 5, 6, 7)); } TEST(BitMask, WithShift_EmptyMask) { EXPECT_THAT((BitMask(0)), ElementsAre()); EXPECT_THAT((BitMask(0)), ElementsAre()); } TEST(BitMask, WithShift_SomeMask) { EXPECT_THAT((BitMask(kSome8BytesMask)), ElementsAreArray(kSome8BytesMaskBits)); EXPECT_THAT((BitMask( kSome8BytesMask)), ElementsAreArray(kSome8BytesMaskBits)); EXPECT_THAT((BitMask( kSome8BytesMaskAllOnes)), ElementsAreArray(kSome8BytesMaskBits)); } TEST(BitMask, WithShift_SomeMaskExtraBitsForNullify) { // Verify that adding extra bits into non zero bytes is fine. uint64_t extra_bits = 77; for (int i = 0; i < 100; ++i) { // Add extra bits, but keep zero bytes untouched. uint64_t extra_mask = extra_bits & kSome8BytesMaskAllOnes; EXPECT_THAT((BitMask( kSome8BytesMask | extra_mask)), ElementsAreArray(kSome8BytesMaskBits)) << i << " " << extra_mask; extra_bits = (extra_bits + 1) * 3; } } TEST(BitMask, LeadingTrailing) { EXPECT_EQ((BitMask(0x00001a40).LeadingZeros()), 3); EXPECT_EQ((BitMask(0x00001a40).TrailingZeros()), 6); EXPECT_EQ((BitMask(0x00000001).LeadingZeros()), 15); EXPECT_EQ((BitMask(0x00000001).TrailingZeros()), 0); EXPECT_EQ((BitMask(0x00008000).LeadingZeros()), 0); EXPECT_EQ((BitMask(0x00008000).TrailingZeros()), 15); EXPECT_EQ((BitMask(0x0000008080808000).LeadingZeros()), 3); EXPECT_EQ((BitMask(0x0000008080808000).TrailingZeros()), 1); EXPECT_EQ((BitMask(0x0000000000000080).LeadingZeros()), 7); EXPECT_EQ((BitMask(0x0000000000000080).TrailingZeros()), 0); EXPECT_EQ((BitMask(0x8000000000000000).LeadingZeros()), 0); EXPECT_EQ((BitMask(0x8000000000000000).TrailingZeros()), 7); } TEST(Group, EmptyGroup) { for (h2_t h = 0; h != 128; ++h) EXPECT_FALSE(Group{EmptyGroup()}.Match(h)); } TEST(Group, Match) { if (Group::kWidth == 16) { ctrl_t group[] = {ctrl_t::kEmpty, CtrlT(1), ctrl_t::kDeleted, CtrlT(3), ctrl_t::kEmpty, CtrlT(5), ctrl_t::kSentinel, CtrlT(7), CtrlT(7), CtrlT(5), CtrlT(3), CtrlT(1), CtrlT(1), CtrlT(1), CtrlT(1), CtrlT(1)}; EXPECT_THAT(Group{group}.Match(0), ElementsAre()); EXPECT_THAT(Group{group}.Match(1), ElementsAre(1, 11, 12, 13, 14, 15)); EXPECT_THAT(Group{group}.Match(3), ElementsAre(3, 10)); EXPECT_THAT(Group{group}.Match(5), ElementsAre(5, 9)); EXPECT_THAT(Group{group}.Match(7), ElementsAre(7, 8)); } else if (Group::kWidth == 8) { ctrl_t group[] = {ctrl_t::kEmpty, CtrlT(1), CtrlT(2), ctrl_t::kDeleted, CtrlT(2), CtrlT(1), ctrl_t::kSentinel, CtrlT(1)}; EXPECT_THAT(Group{group}.Match(0), ElementsAre()); EXPECT_THAT(Group{group}.Match(1), ElementsAre(1, 5, 7)); EXPECT_THAT(Group{group}.Match(2), ElementsAre(2, 4)); } else { FAIL() << "No test coverage for Group::kWidth==" << Group::kWidth; } } TEST(Group, MaskEmpty) { if (Group::kWidth == 16) { ctrl_t group[] = {ctrl_t::kEmpty, CtrlT(1), ctrl_t::kDeleted, CtrlT(3), ctrl_t::kEmpty, CtrlT(5), ctrl_t::kSentinel, CtrlT(7), CtrlT(7), CtrlT(5), CtrlT(3), CtrlT(1), CtrlT(1), CtrlT(1), CtrlT(1), CtrlT(1)}; EXPECT_THAT(Group{group}.MaskEmpty().LowestBitSet(), 0); EXPECT_THAT(Group{group}.MaskEmpty().HighestBitSet(), 4); } else if (Group::kWidth == 8) { ctrl_t group[] = {ctrl_t::kEmpty, CtrlT(1), CtrlT(2), ctrl_t::kDeleted, CtrlT(2), CtrlT(1), ctrl_t::kSentinel, CtrlT(1)}; EXPECT_THAT(Group{group}.MaskEmpty().LowestBitSet(), 0); EXPECT_THAT(Group{group}.MaskEmpty().HighestBitSet(), 0); } else { FAIL() << "No test coverage for Group::kWidth==" << Group::kWidth; } } TEST(Group, MaskFull) { if (Group::kWidth == 16) { ctrl_t group[] = { ctrl_t::kEmpty, CtrlT(1), ctrl_t::kDeleted, CtrlT(3), ctrl_t::kEmpty, CtrlT(5), ctrl_t::kSentinel, CtrlT(7), CtrlT(7), CtrlT(5), ctrl_t::kDeleted, CtrlT(1), CtrlT(1), ctrl_t::kSentinel, ctrl_t::kEmpty, CtrlT(1)}; EXPECT_THAT(Group{group}.MaskFull(), ElementsAre(1, 3, 5, 7, 8, 9, 11, 12, 15)); } else if (Group::kWidth == 8) { ctrl_t group[] = {ctrl_t::kEmpty, CtrlT(1), ctrl_t::kEmpty, ctrl_t::kDeleted, CtrlT(2), ctrl_t::kSentinel, ctrl_t::kSentinel, CtrlT(1)}; EXPECT_THAT(Group{group}.MaskFull(), ElementsAre(1, 4, 7)); } else { FAIL() << "No test coverage for Group::kWidth==" << Group::kWidth; } } TEST(Group, MaskNonFull) { if (Group::kWidth == 16) { ctrl_t group[] = { ctrl_t::kEmpty, CtrlT(1), ctrl_t::kDeleted, CtrlT(3), ctrl_t::kEmpty, CtrlT(5), ctrl_t::kSentinel, CtrlT(7), CtrlT(7), CtrlT(5), ctrl_t::kDeleted, CtrlT(1), CtrlT(1), ctrl_t::kSentinel, ctrl_t::kEmpty, CtrlT(1)}; EXPECT_THAT(Group{group}.MaskNonFull(), ElementsAre(0, 2, 4, 6, 10, 13, 14)); } else if (Group::kWidth == 8) { ctrl_t group[] = {ctrl_t::kEmpty, CtrlT(1), ctrl_t::kEmpty, ctrl_t::kDeleted, CtrlT(2), ctrl_t::kSentinel, ctrl_t::kSentinel, CtrlT(1)}; EXPECT_THAT(Group{group}.MaskNonFull(), ElementsAre(0, 2, 3, 5, 6)); } else { FAIL() << "No test coverage for Group::kWidth==" << Group::kWidth; } } TEST(Group, MaskEmptyOrDeleted) { if (Group::kWidth == 16) { ctrl_t group[] = {ctrl_t::kEmpty, CtrlT(1), ctrl_t::kEmpty, CtrlT(3), ctrl_t::kDeleted, CtrlT(5), ctrl_t::kSentinel, CtrlT(7), CtrlT(7), CtrlT(5), CtrlT(3), CtrlT(1), CtrlT(1), CtrlT(1), CtrlT(1), CtrlT(1)}; EXPECT_THAT(Group{group}.MaskEmptyOrDeleted().LowestBitSet(), 0); EXPECT_THAT(Group{group}.MaskEmptyOrDeleted().HighestBitSet(), 4); } else if (Group::kWidth == 8) { ctrl_t group[] = {ctrl_t::kEmpty, CtrlT(1), CtrlT(2), ctrl_t::kDeleted, CtrlT(2), CtrlT(1), ctrl_t::kSentinel, CtrlT(1)}; EXPECT_THAT(Group{group}.MaskEmptyOrDeleted().LowestBitSet(), 0); EXPECT_THAT(Group{group}.MaskEmptyOrDeleted().HighestBitSet(), 3); } else { FAIL() << "No test coverage for Group::kWidth==" << Group::kWidth; } } TEST(Batch, DropDeletes) { constexpr size_t kCapacity = 63; constexpr size_t kGroupWidth = container_internal::Group::kWidth; std::vector ctrl(kCapacity + 1 + kGroupWidth); ctrl[kCapacity] = ctrl_t::kSentinel; std::vector pattern = { ctrl_t::kEmpty, CtrlT(2), ctrl_t::kDeleted, CtrlT(2), ctrl_t::kEmpty, CtrlT(1), ctrl_t::kDeleted}; for (size_t i = 0; i != kCapacity; ++i) { ctrl[i] = pattern[i % pattern.size()]; if (i < kGroupWidth - 1) ctrl[i + kCapacity + 1] = pattern[i % pattern.size()]; } ConvertDeletedToEmptyAndFullToDeleted(ctrl.data(), kCapacity); ASSERT_EQ(ctrl[kCapacity], ctrl_t::kSentinel); for (size_t i = 0; i < kCapacity + kGroupWidth; ++i) { ctrl_t expected = pattern[i % (kCapacity + 1) % pattern.size()]; if (i == kCapacity) expected = ctrl_t::kSentinel; if (expected == ctrl_t::kDeleted) expected = ctrl_t::kEmpty; if (IsFull(expected)) expected = ctrl_t::kDeleted; EXPECT_EQ(ctrl[i], expected) << i << " " << static_cast(pattern[i % pattern.size()]); } } TEST(Group, CountLeadingEmptyOrDeleted) { const std::vector empty_examples = {ctrl_t::kEmpty, ctrl_t::kDeleted}; const std::vector full_examples = { CtrlT(0), CtrlT(1), CtrlT(2), CtrlT(3), CtrlT(5), CtrlT(9), CtrlT(127), ctrl_t::kSentinel}; for (ctrl_t empty : empty_examples) { std::vector e(Group::kWidth, empty); EXPECT_EQ(Group::kWidth, Group{e.data()}.CountLeadingEmptyOrDeleted()); for (ctrl_t full : full_examples) { for (size_t i = 0; i != Group::kWidth; ++i) { std::vector f(Group::kWidth, empty); f[i] = full; EXPECT_EQ(i, Group{f.data()}.CountLeadingEmptyOrDeleted()); } std::vector f(Group::kWidth, empty); f[Group::kWidth * 2 / 3] = full; f[Group::kWidth / 2] = full; EXPECT_EQ(Group::kWidth / 2, Group{f.data()}.CountLeadingEmptyOrDeleted()); } } } template struct ValuePolicy { using slot_type = T; using key_type = T; using init_type = T; template static void construct(Allocator* alloc, slot_type* slot, Args&&... args) { absl::allocator_traits::construct(*alloc, slot, std::forward(args)...); } template static void destroy(Allocator* alloc, slot_type* slot) { absl::allocator_traits::destroy(*alloc, slot); } template static std::integral_constant transfer( Allocator* alloc, slot_type* new_slot, slot_type* old_slot) { construct(alloc, new_slot, std::move(*old_slot)); destroy(alloc, old_slot); return {}; } static T& element(slot_type* slot) { return *slot; } template static decltype(absl::container_internal::DecomposeValue( std::declval(), std::declval()...)) apply(F&& f, Args&&... args) { return absl::container_internal::DecomposeValue( std::forward(f), std::forward(args)...); } template static constexpr HashSlotFn get_hash_slot_fn() { return nullptr; } static constexpr bool soo_enabled() { return kSoo; } }; using IntPolicy = ValuePolicy; using Uint8Policy = ValuePolicy; using TranferableIntPolicy = ValuePolicy; // For testing SOO. template class SizedValue { public: SizedValue(int64_t v) { // NOLINT vals_[0] = v; } SizedValue() : SizedValue(0) {} SizedValue(const SizedValue&) = default; SizedValue& operator=(const SizedValue&) = default; int64_t operator*() const { // Suppress erroneous uninitialized memory errors on GCC. #if !defined(__clang__) && defined(__GNUC__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wmaybe-uninitialized" #endif return vals_[0]; #if !defined(__clang__) && defined(__GNUC__) #pragma GCC diagnostic pop #endif } explicit operator int() const { return **this; } explicit operator int64_t() const { return **this; } template friend H AbslHashValue(H h, SizedValue sv) { return H::combine(std::move(h), *sv); } bool operator==(const SizedValue& rhs) const { return **this == *rhs; } private: int64_t vals_[N / sizeof(int64_t)]; }; template using SizedValuePolicy = ValuePolicy, /*kTransferable=*/true, kSoo>; class StringPolicy { template ::value>::type> decltype(std::declval()( std::declval(), std::piecewise_construct, std::declval>(), std::declval())) static apply_impl(F&& f, std::pair, V> p) { const absl::string_view& key = std::get<0>(p.first); return std::forward(f)(key, std::piecewise_construct, std::move(p.first), std::move(p.second)); } public: struct slot_type { struct ctor {}; template explicit slot_type(ctor, Ts&&... ts) : pair(std::forward(ts)...) {} std::pair pair; }; using key_type = std::string; using init_type = std::pair; template static void construct(allocator_type* alloc, slot_type* slot, Args... args) { std::allocator_traits::construct( *alloc, slot, typename slot_type::ctor(), std::forward(args)...); } template static void destroy(allocator_type* alloc, slot_type* slot) { std::allocator_traits::destroy(*alloc, slot); } template static void transfer(allocator_type* alloc, slot_type* new_slot, slot_type* old_slot) { construct(alloc, new_slot, std::move(old_slot->pair)); destroy(alloc, old_slot); } static std::pair& element(slot_type* slot) { return slot->pair; } template static auto apply(F&& f, Args&&... args) -> decltype(apply_impl(std::forward(f), PairArgs(std::forward(args)...))) { return apply_impl(std::forward(f), PairArgs(std::forward(args)...)); } template static constexpr HashSlotFn get_hash_slot_fn() { return nullptr; } }; struct StringHash : absl::Hash { using is_transparent = void; }; struct StringEq : std::equal_to { using is_transparent = void; }; struct StringTable : raw_hash_set> { using Base = typename StringTable::raw_hash_set; StringTable() = default; using Base::Base; }; template struct ValueTable : raw_hash_set, hash_default_hash, std::equal_to, std::allocator> { using Base = typename ValueTable::raw_hash_set; using Base::Base; }; using IntTable = ValueTable; using Uint8Table = ValueTable; using TransferableIntTable = ValueTable; constexpr size_t kNonSooSize = sizeof(HeapOrSoo) + 8; static_assert(sizeof(SizedValue) >= kNonSooSize, "too small"); using NonSooIntTable = ValueTable>; using SooIntTable = ValueTable; template struct CustomAlloc : std::allocator { CustomAlloc() = default; template explicit CustomAlloc(const CustomAlloc& /*other*/) {} template struct rebind { using other = CustomAlloc; }; }; struct CustomAllocIntTable : raw_hash_set, std::equal_to, CustomAlloc> { using Base = typename CustomAllocIntTable::raw_hash_set; using Base::Base; }; struct MinimumAlignmentUint8Table : raw_hash_set, std::equal_to, MinimumAlignmentAlloc> { using Base = typename MinimumAlignmentUint8Table::raw_hash_set; using Base::Base; }; // Allows for freezing the allocator to expect no further allocations. template struct FreezableAlloc : std::allocator { explicit FreezableAlloc(bool* f) : frozen(f) {} template explicit FreezableAlloc(const FreezableAlloc& other) : frozen(other.frozen) {} template struct rebind { using other = FreezableAlloc; }; T* allocate(size_t n) { EXPECT_FALSE(*frozen); return std::allocator::allocate(n); } bool* frozen; }; template struct FreezableSizedValueSooTable : raw_hash_set, container_internal::hash_default_hash>, std::equal_to>, FreezableAlloc>> { using Base = typename FreezableSizedValueSooTable::raw_hash_set; using Base::Base; }; struct BadFastHash { template size_t operator()(const T&) const { return 0; } }; struct BadHashFreezableIntTable : raw_hash_set, FreezableAlloc> { using Base = typename BadHashFreezableIntTable::raw_hash_set; using Base::Base; }; struct BadTable : raw_hash_set, std::allocator> { using Base = typename BadTable::raw_hash_set; BadTable() = default; using Base::Base; }; TEST(Table, EmptyFunctorOptimization) { static_assert(std::is_empty>::value, ""); static_assert(std::is_empty>::value, ""); struct MockTable { void* ctrl; void* slots; size_t size; size_t capacity; }; struct StatelessHash { size_t operator()(absl::string_view) const { return 0; } }; struct StatefulHash : StatelessHash { size_t dummy; }; struct GenerationData { size_t reserved_growth; size_t reservation_size; GenerationType* generation; }; // Ignore unreachable-code warning. Compiler thinks one branch of each ternary // conditional is unreachable. #if defined(__clang__) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunreachable-code" #endif constexpr size_t mock_size = sizeof(MockTable); constexpr size_t generation_size = SwisstableGenerationsEnabled() ? sizeof(GenerationData) : 0; #if defined(__clang__) #pragma clang diagnostic pop #endif EXPECT_EQ( mock_size + generation_size, sizeof( raw_hash_set, std::allocator>)); EXPECT_EQ( mock_size + sizeof(StatefulHash) + generation_size, sizeof( raw_hash_set, std::allocator>)); } template class SooTest : public testing::Test {}; using SooTableTypes = ::testing::Types; TYPED_TEST_SUITE(SooTest, SooTableTypes); TYPED_TEST(SooTest, Empty) { TypeParam t; EXPECT_EQ(0, t.size()); EXPECT_TRUE(t.empty()); } TYPED_TEST(SooTest, LookupEmpty) { TypeParam t; auto it = t.find(0); EXPECT_TRUE(it == t.end()); } TYPED_TEST(SooTest, Insert1) { TypeParam t; EXPECT_TRUE(t.find(0) == t.end()); auto res = t.emplace(0); EXPECT_TRUE(res.second); EXPECT_THAT(*res.first, 0); EXPECT_EQ(1, t.size()); EXPECT_THAT(*t.find(0), 0); } TYPED_TEST(SooTest, Insert2) { TypeParam t; EXPECT_TRUE(t.find(0) == t.end()); auto res = t.emplace(0); EXPECT_TRUE(res.second); EXPECT_THAT(*res.first, 0); EXPECT_EQ(1, t.size()); EXPECT_TRUE(t.find(1) == t.end()); res = t.emplace(1); EXPECT_TRUE(res.second); EXPECT_THAT(*res.first, 1); EXPECT_EQ(2, t.size()); EXPECT_THAT(*t.find(0), 0); EXPECT_THAT(*t.find(1), 1); } TEST(Table, InsertCollision) { BadTable t; EXPECT_TRUE(t.find(1) == t.end()); auto res = t.emplace(1); EXPECT_TRUE(res.second); EXPECT_THAT(*res.first, 1); EXPECT_EQ(1, t.size()); EXPECT_TRUE(t.find(2) == t.end()); res = t.emplace(2); EXPECT_THAT(*res.first, 2); EXPECT_TRUE(res.second); EXPECT_EQ(2, t.size()); EXPECT_THAT(*t.find(1), 1); EXPECT_THAT(*t.find(2), 2); } // Test that we do not add existent element in case we need to search through // many groups with deleted elements TEST(Table, InsertCollisionAndFindAfterDelete) { BadTable t; // all elements go to the same group. // Have at least 2 groups with Group::kWidth collisions // plus some extra collisions in the last group. constexpr size_t kNumInserts = Group::kWidth * 2 + 5; for (size_t i = 0; i < kNumInserts; ++i) { auto res = t.emplace(i); EXPECT_TRUE(res.second); EXPECT_THAT(*res.first, i); EXPECT_EQ(i + 1, t.size()); } // Remove elements one by one and check // that we still can find all other elements. for (size_t i = 0; i < kNumInserts; ++i) { EXPECT_EQ(1, t.erase(i)) << i; for (size_t j = i + 1; j < kNumInserts; ++j) { EXPECT_THAT(*t.find(j), j); auto res = t.emplace(j); EXPECT_FALSE(res.second) << i << " " << j; EXPECT_THAT(*res.first, j); EXPECT_EQ(kNumInserts - i - 1, t.size()); } } EXPECT_TRUE(t.empty()); } TYPED_TEST(SooTest, EraseInSmallTables) { for (int64_t size = 0; size < 64; ++size) { TypeParam t; for (int64_t i = 0; i < size; ++i) { t.insert(i); } for (int64_t i = 0; i < size; ++i) { t.erase(i); EXPECT_EQ(t.size(), size - i - 1); for (int64_t j = i + 1; j < size; ++j) { EXPECT_THAT(*t.find(j), j); } } EXPECT_TRUE(t.empty()); } } TYPED_TEST(SooTest, InsertWithinCapacity) { TypeParam t; t.reserve(10); const size_t original_capacity = t.capacity(); const auto addr = [&](int i) { return reinterpret_cast(&*t.find(i)); }; // Inserting an element does not change capacity. t.insert(0); EXPECT_THAT(t.capacity(), original_capacity); const uintptr_t original_addr_0 = addr(0); // Inserting another element does not rehash. t.insert(1); EXPECT_THAT(t.capacity(), original_capacity); EXPECT_THAT(addr(0), original_addr_0); // Inserting lots of duplicate elements does not rehash. for (int i = 0; i < 100; ++i) { t.insert(i % 10); } EXPECT_THAT(t.capacity(), original_capacity); EXPECT_THAT(addr(0), original_addr_0); // Inserting a range of duplicate elements does not rehash. std::vector dup_range; for (int i = 0; i < 100; ++i) { dup_range.push_back(i % 10); } t.insert(dup_range.begin(), dup_range.end()); EXPECT_THAT(t.capacity(), original_capacity); EXPECT_THAT(addr(0), original_addr_0); } template class SmallTableResizeTest : public testing::Test {}; using SmallTableTypes = ::testing::Types; TYPED_TEST_SUITE(SmallTableResizeTest, SmallTableTypes); TYPED_TEST(SmallTableResizeTest, InsertIntoSmallTable) { TypeParam t; for (int i = 0; i < 32; ++i) { t.insert(i); ASSERT_EQ(t.size(), i + 1); for (int j = 0; j < i + 1; ++j) { EXPECT_TRUE(t.find(j) != t.end()); EXPECT_EQ(*t.find(j), j); } } } TYPED_TEST(SmallTableResizeTest, ResizeGrowSmallTables) { for (size_t source_size = 0; source_size < 32; ++source_size) { for (size_t target_size = source_size; target_size < 32; ++target_size) { for (bool rehash : {false, true}) { TypeParam t; for (size_t i = 0; i < source_size; ++i) { t.insert(static_cast(i)); } if (rehash) { t.rehash(target_size); } else { t.reserve(target_size); } for (size_t i = 0; i < source_size; ++i) { EXPECT_TRUE(t.find(static_cast(i)) != t.end()); EXPECT_EQ(*t.find(static_cast(i)), static_cast(i)); } } } } } TYPED_TEST(SmallTableResizeTest, ResizeReduceSmallTables) { for (size_t source_size = 0; source_size < 32; ++source_size) { for (size_t target_size = 0; target_size <= source_size; ++target_size) { TypeParam t; size_t inserted_count = std::min(source_size, 5); for (size_t i = 0; i < inserted_count; ++i) { t.insert(static_cast(i)); } const size_t minimum_capacity = t.capacity(); t.reserve(source_size); t.rehash(target_size); if (target_size == 0) { EXPECT_EQ(t.capacity(), minimum_capacity) << "rehash(0) must resize to the minimum capacity"; } for (size_t i = 0; i < inserted_count; ++i) { EXPECT_TRUE(t.find(static_cast(i)) != t.end()); EXPECT_EQ(*t.find(static_cast(i)), static_cast(i)); } } } } TEST(Table, LazyEmplace) { StringTable t; bool called = false; auto it = t.lazy_emplace("abc", [&](const StringTable::constructor& f) { called = true; f("abc", "ABC"); }); EXPECT_TRUE(called); EXPECT_THAT(*it, Pair("abc", "ABC")); called = false; it = t.lazy_emplace("abc", [&](const StringTable::constructor& f) { called = true; f("abc", "DEF"); }); EXPECT_FALSE(called); EXPECT_THAT(*it, Pair("abc", "ABC")); } TYPED_TEST(SooTest, ContainsEmpty) { TypeParam t; EXPECT_FALSE(t.contains(0)); } TYPED_TEST(SooTest, Contains1) { TypeParam t; EXPECT_TRUE(t.insert(0).second); EXPECT_TRUE(t.contains(0)); EXPECT_FALSE(t.contains(1)); EXPECT_EQ(1, t.erase(0)); EXPECT_FALSE(t.contains(0)); } TYPED_TEST(SooTest, Contains2) { TypeParam t; EXPECT_TRUE(t.insert(0).second); EXPECT_TRUE(t.contains(0)); EXPECT_FALSE(t.contains(1)); t.clear(); EXPECT_FALSE(t.contains(0)); } int decompose_constructed; int decompose_copy_constructed; int decompose_copy_assigned; int decompose_move_constructed; int decompose_move_assigned; struct DecomposeType { DecomposeType(int i = 0) : i(i) { // NOLINT ++decompose_constructed; } explicit DecomposeType(const char* d) : DecomposeType(*d) {} DecomposeType(const DecomposeType& other) : i(other.i) { ++decompose_copy_constructed; } DecomposeType& operator=(const DecomposeType& other) { ++decompose_copy_assigned; i = other.i; return *this; } DecomposeType(DecomposeType&& other) : i(other.i) { ++decompose_move_constructed; } DecomposeType& operator=(DecomposeType&& other) { ++decompose_move_assigned; i = other.i; return *this; } int i; }; struct DecomposeHash { using is_transparent = void; size_t operator()(const DecomposeType& a) const { return a.i; } size_t operator()(int a) const { return a; } size_t operator()(const char* a) const { return *a; } }; struct DecomposeEq { using is_transparent = void; bool operator()(const DecomposeType& a, const DecomposeType& b) const { return a.i == b.i; } bool operator()(const DecomposeType& a, int b) const { return a.i == b; } bool operator()(const DecomposeType& a, const char* b) const { return a.i == *b; } }; struct DecomposePolicy { using slot_type = DecomposeType; using key_type = DecomposeType; using init_type = DecomposeType; template static void construct(void*, DecomposeType* slot, T&& v) { ::new (slot) DecomposeType(std::forward(v)); } static void destroy(void*, DecomposeType* slot) { slot->~DecomposeType(); } static DecomposeType& element(slot_type* slot) { return *slot; } template static auto apply(F&& f, const T& x) -> decltype(std::forward(f)(x, x)) { return std::forward(f)(x, x); } template static constexpr HashSlotFn get_hash_slot_fn() { return nullptr; } }; template void TestDecompose(bool construct_three) { DecomposeType elem{0}; const int one = 1; const char* three_p = "3"; const auto& three = three_p; const int elem_vector_count = 256; std::vector elem_vector(elem_vector_count, DecomposeType{0}); std::iota(elem_vector.begin(), elem_vector.end(), 0); using DecomposeSet = raw_hash_set>; DecomposeSet set1; decompose_constructed = 0; int expected_constructed = 0; EXPECT_EQ(expected_constructed, decompose_constructed); set1.insert(elem); EXPECT_EQ(expected_constructed, decompose_constructed); set1.insert(1); EXPECT_EQ(++expected_constructed, decompose_constructed); set1.emplace("3"); EXPECT_EQ(++expected_constructed, decompose_constructed); EXPECT_EQ(expected_constructed, decompose_constructed); { // insert(T&&) set1.insert(1); EXPECT_EQ(expected_constructed, decompose_constructed); } { // insert(const T&) set1.insert(one); EXPECT_EQ(expected_constructed, decompose_constructed); } { // insert(hint, T&&) set1.insert(set1.begin(), 1); EXPECT_EQ(expected_constructed, decompose_constructed); } { // insert(hint, const T&) set1.insert(set1.begin(), one); EXPECT_EQ(expected_constructed, decompose_constructed); } { // emplace(...) set1.emplace(1); EXPECT_EQ(expected_constructed, decompose_constructed); set1.emplace("3"); expected_constructed += construct_three; EXPECT_EQ(expected_constructed, decompose_constructed); set1.emplace(one); EXPECT_EQ(expected_constructed, decompose_constructed); set1.emplace(three); expected_constructed += construct_three; EXPECT_EQ(expected_constructed, decompose_constructed); } { // emplace_hint(...) set1.emplace_hint(set1.begin(), 1); EXPECT_EQ(expected_constructed, decompose_constructed); set1.emplace_hint(set1.begin(), "3"); expected_constructed += construct_three; EXPECT_EQ(expected_constructed, decompose_constructed); set1.emplace_hint(set1.begin(), one); EXPECT_EQ(expected_constructed, decompose_constructed); set1.emplace_hint(set1.begin(), three); expected_constructed += construct_three; EXPECT_EQ(expected_constructed, decompose_constructed); } decompose_copy_constructed = 0; decompose_copy_assigned = 0; decompose_move_constructed = 0; decompose_move_assigned = 0; int expected_copy_constructed = 0; int expected_move_constructed = 0; { // raw_hash_set(first, last) with random-access iterators DecomposeSet set2(elem_vector.begin(), elem_vector.end()); // Expect exactly one copy-constructor call for each element if no // rehashing is done. expected_copy_constructed += elem_vector_count; EXPECT_EQ(expected_copy_constructed, decompose_copy_constructed); EXPECT_EQ(expected_move_constructed, decompose_move_constructed); EXPECT_EQ(0, decompose_move_assigned); EXPECT_EQ(0, decompose_copy_assigned); } { // raw_hash_set(first, last) with forward iterators std::list elem_list(elem_vector.begin(), elem_vector.end()); expected_copy_constructed = decompose_copy_constructed; DecomposeSet set2(elem_list.begin(), elem_list.end()); // Expect exactly N elements copied into set, expect at most 2*N elements // moving internally for all resizing needed (for a growth factor of 2). expected_copy_constructed += elem_vector_count; EXPECT_EQ(expected_copy_constructed, decompose_copy_constructed); expected_move_constructed += elem_vector_count; EXPECT_LT(expected_move_constructed, decompose_move_constructed); expected_move_constructed += elem_vector_count; EXPECT_GE(expected_move_constructed, decompose_move_constructed); EXPECT_EQ(0, decompose_move_assigned); EXPECT_EQ(0, decompose_copy_assigned); expected_copy_constructed = decompose_copy_constructed; expected_move_constructed = decompose_move_constructed; } { // insert(first, last) DecomposeSet set2; set2.insert(elem_vector.begin(), elem_vector.end()); // Expect exactly N elements copied into set, expect at most 2*N elements // moving internally for all resizing needed (for a growth factor of 2). const int expected_new_elements = elem_vector_count; const int expected_max_element_moves = 2 * elem_vector_count; expected_copy_constructed += expected_new_elements; EXPECT_EQ(expected_copy_constructed, decompose_copy_constructed); expected_move_constructed += expected_max_element_moves; EXPECT_GE(expected_move_constructed, decompose_move_constructed); EXPECT_EQ(0, decompose_move_assigned); EXPECT_EQ(0, decompose_copy_assigned); expected_copy_constructed = decompose_copy_constructed; expected_move_constructed = decompose_move_constructed; } } TEST(Table, Decompose) { if (SwisstableGenerationsEnabled()) { GTEST_SKIP() << "Generations being enabled causes extra rehashes."; } TestDecompose(false); struct TransparentHashIntOverload { size_t operator()(const DecomposeType& a) const { return a.i; } size_t operator()(int a) const { return a; } }; struct TransparentEqIntOverload { bool operator()(const DecomposeType& a, const DecomposeType& b) const { return a.i == b.i; } bool operator()(const DecomposeType& a, int b) const { return a.i == b; } }; TestDecompose(true); TestDecompose(true); TestDecompose(true); } // Returns the largest m such that a table with m elements has the same number // of buckets as a table with n elements. size_t MaxDensitySize(size_t n) { IntTable t; t.reserve(n); for (size_t i = 0; i != n; ++i) t.emplace(i); const size_t c = t.bucket_count(); while (c == t.bucket_count()) t.emplace(n++); return t.size() - 1; } struct Modulo1000Hash { size_t operator()(int64_t x) const { return static_cast(x) % 1000; } }; struct Modulo1000HashTable : public raw_hash_set, std::allocator> {}; // Test that rehash with no resize happen in case of many deleted slots. TEST(Table, RehashWithNoResize) { if (SwisstableGenerationsEnabled()) { GTEST_SKIP() << "Generations being enabled causes extra rehashes."; } Modulo1000HashTable t; // Adding the same length (and the same hash) strings // to have at least kMinFullGroups groups // with Group::kWidth collisions. Then fill up to MaxDensitySize; const size_t kMinFullGroups = 7; std::vector keys; for (size_t i = 0; i < MaxDensitySize(Group::kWidth * kMinFullGroups); ++i) { int k = i * 1000; t.emplace(k); keys.push_back(k); } const size_t capacity = t.capacity(); // Remove elements from all groups except the first and the last one. // All elements removed from full groups will be marked as ctrl_t::kDeleted. const size_t erase_begin = Group::kWidth / 2; const size_t erase_end = (t.size() / Group::kWidth - 1) * Group::kWidth; for (size_t i = erase_begin; i < erase_end; ++i) { EXPECT_EQ(1, t.erase(keys[i])) << i; } keys.erase(keys.begin() + erase_begin, keys.begin() + erase_end); auto last_key = keys.back(); size_t last_key_num_probes = GetHashtableDebugNumProbes(t, last_key); // Make sure that we have to make a lot of probes for last key. ASSERT_GT(last_key_num_probes, kMinFullGroups); int x = 1; // Insert and erase one element, before inplace rehash happen. while (last_key_num_probes == GetHashtableDebugNumProbes(t, last_key)) { t.emplace(x); ASSERT_EQ(capacity, t.capacity()); // All elements should be there. ASSERT_TRUE(t.find(x) != t.end()) << x; for (const auto& k : keys) { ASSERT_TRUE(t.find(k) != t.end()) << k; } t.erase(x); ++x; } } TYPED_TEST(SooTest, InsertEraseStressTest) { TypeParam t; const size_t kMinElementCount = 250; std::deque keys; size_t i = 0; for (; i < MaxDensitySize(kMinElementCount); ++i) { t.emplace(i); keys.push_back(i); } const size_t kNumIterations = 1000000; for (; i < kNumIterations; ++i) { ASSERT_EQ(1, t.erase(keys.front())); keys.pop_front(); t.emplace(i); keys.push_back(i); } } TEST(Table, InsertOverloads) { StringTable t; // These should all trigger the insert(init_type) overload. t.insert({{}, {}}); t.insert({"ABC", {}}); t.insert({"DEF", "!!!"}); EXPECT_THAT(t, UnorderedElementsAre(Pair("", ""), Pair("ABC", ""), Pair("DEF", "!!!"))); } TYPED_TEST(SooTest, LargeTable) { TypeParam t; for (int64_t i = 0; i != 100000; ++i) t.emplace(i << 40); for (int64_t i = 0; i != 100000; ++i) ASSERT_EQ(i << 40, static_cast(*t.find(i << 40))); } // Timeout if copy is quadratic as it was in Rust. TYPED_TEST(SooTest, EnsureNonQuadraticAsInRust) { static const size_t kLargeSize = 1 << 15; TypeParam t; for (size_t i = 0; i != kLargeSize; ++i) { t.insert(i); } // If this is quadratic, the test will timeout. TypeParam t2; for (const auto& entry : t) t2.insert(entry); } TYPED_TEST(SooTest, ClearBug) { if (SwisstableGenerationsEnabled()) { GTEST_SKIP() << "Generations being enabled causes extra rehashes."; } TypeParam t; constexpr size_t capacity = container_internal::Group::kWidth - 1; constexpr size_t max_size = capacity / 2 + 1; for (size_t i = 0; i < max_size; ++i) { t.insert(i); } ASSERT_EQ(capacity, t.capacity()); intptr_t original = reinterpret_cast(&*t.find(2)); t.clear(); ASSERT_EQ(capacity, t.capacity()); for (size_t i = 0; i < max_size; ++i) { t.insert(i); } ASSERT_EQ(capacity, t.capacity()); intptr_t second = reinterpret_cast(&*t.find(2)); // We are checking that original and second are close enough to each other // that they are probably still in the same group. This is not strictly // guaranteed. EXPECT_LT(static_cast(std::abs(original - second)), capacity * sizeof(typename TypeParam::value_type)); } TYPED_TEST(SooTest, Erase) { TypeParam t; EXPECT_TRUE(t.find(0) == t.end()); auto res = t.emplace(0); EXPECT_TRUE(res.second); EXPECT_EQ(1, t.size()); t.erase(res.first); EXPECT_EQ(0, t.size()); EXPECT_TRUE(t.find(0) == t.end()); } TYPED_TEST(SooTest, EraseMaintainsValidIterator) { TypeParam t; const int kNumElements = 100; for (int i = 0; i < kNumElements; i++) { EXPECT_TRUE(t.emplace(i).second); } EXPECT_EQ(t.size(), kNumElements); int num_erase_calls = 0; auto it = t.begin(); while (it != t.end()) { t.erase(it++); num_erase_calls++; } EXPECT_TRUE(t.empty()); EXPECT_EQ(num_erase_calls, kNumElements); } TYPED_TEST(SooTest, EraseBeginEnd) { TypeParam t; for (int i = 0; i < 10; ++i) t.insert(i); EXPECT_EQ(t.size(), 10); t.erase(t.begin(), t.end()); EXPECT_EQ(t.size(), 0); } // Collect N bad keys by following algorithm: // 1. Create an empty table and reserve it to 2 * N. // 2. Insert N random elements. // 3. Take first Group::kWidth - 1 to bad_keys array. // 4. Clear the table without resize. // 5. Go to point 2 while N keys not collected std::vector CollectBadMergeKeys(size_t N) { static constexpr int kGroupSize = Group::kWidth - 1; auto topk_range = [](size_t b, size_t e, IntTable* t) -> std::vector { for (size_t i = b; i != e; ++i) { t->emplace(i); } std::vector res; res.reserve(kGroupSize); auto it = t->begin(); for (size_t i = b; i != e && i != b + kGroupSize; ++i, ++it) { res.push_back(*it); } return res; }; std::vector bad_keys; bad_keys.reserve(N); IntTable t; t.reserve(N * 2); for (size_t b = 0; bad_keys.size() < N; b += N) { auto keys = topk_range(b, b + N, &t); bad_keys.insert(bad_keys.end(), keys.begin(), keys.end()); t.erase(t.begin(), t.end()); EXPECT_TRUE(t.empty()); } return bad_keys; } struct ProbeStats { // Number of elements with specific probe length over all tested tables. std::vector all_probes_histogram; // Ratios total_probe_length/size for every tested table. std::vector single_table_ratios; // Average ratio total_probe_length/size over tables. double AvgRatio() const { return std::accumulate(single_table_ratios.begin(), single_table_ratios.end(), 0.0) / single_table_ratios.size(); } // Maximum ratio total_probe_length/size over tables. double MaxRatio() const { return *std::max_element(single_table_ratios.begin(), single_table_ratios.end()); } // Percentile ratio total_probe_length/size over tables. double PercentileRatio(double Percentile = 0.95) const { auto r = single_table_ratios; auto mid = r.begin() + static_cast(r.size() * Percentile); if (mid != r.end()) { std::nth_element(r.begin(), mid, r.end()); return *mid; } else { return MaxRatio(); } } // Maximum probe length over all elements and all tables. size_t MaxProbe() const { return all_probes_histogram.size(); } // Fraction of elements with specified probe length. std::vector ProbeNormalizedHistogram() const { double total_elements = std::accumulate(all_probes_histogram.begin(), all_probes_histogram.end(), 0ull); std::vector res; for (size_t p : all_probes_histogram) { res.push_back(p / total_elements); } return res; } size_t PercentileProbe(double Percentile = 0.99) const { size_t idx = 0; for (double p : ProbeNormalizedHistogram()) { if (Percentile > p) { Percentile -= p; ++idx; } else { return idx; } } return idx; } friend std::ostream& operator<<(std::ostream& out, const ProbeStats& s) { out << "{AvgRatio:" << s.AvgRatio() << ", MaxRatio:" << s.MaxRatio() << ", PercentileRatio:" << s.PercentileRatio() << ", MaxProbe:" << s.MaxProbe() << ", Probes=["; for (double p : s.ProbeNormalizedHistogram()) { out << p << ","; } out << "]}"; return out; } }; struct ExpectedStats { double avg_ratio; double max_ratio; std::vector> pecentile_ratios; std::vector> pecentile_probes; friend std::ostream& operator<<(std::ostream& out, const ExpectedStats& s) { out << "{AvgRatio:" << s.avg_ratio << ", MaxRatio:" << s.max_ratio << ", PercentileRatios: ["; for (auto el : s.pecentile_ratios) { out << el.first << ":" << el.second << ", "; } out << "], PercentileProbes: ["; for (auto el : s.pecentile_probes) { out << el.first << ":" << el.second << ", "; } out << "]}"; return out; } }; void VerifyStats(size_t size, const ExpectedStats& exp, const ProbeStats& stats) { EXPECT_LT(stats.AvgRatio(), exp.avg_ratio) << size << " " << stats; EXPECT_LT(stats.MaxRatio(), exp.max_ratio) << size << " " << stats; for (auto pr : exp.pecentile_ratios) { EXPECT_LE(stats.PercentileRatio(pr.first), pr.second) << size << " " << pr.first << " " << stats; } for (auto pr : exp.pecentile_probes) { EXPECT_LE(stats.PercentileProbe(pr.first), pr.second) << size << " " << pr.first << " " << stats; } } using ProbeStatsPerSize = std::map; // Collect total ProbeStats on num_iters iterations of the following algorithm: // 1. Create new table and reserve it to keys.size() * 2 // 2. Insert all keys xored with seed // 3. Collect ProbeStats from final table. ProbeStats CollectProbeStatsOnKeysXoredWithSeed( const std::vector& keys, size_t num_iters) { const size_t reserve_size = keys.size() * 2; ProbeStats stats; int64_t seed = 0x71b1a19b907d6e33; while (num_iters--) { seed = static_cast(static_cast(seed) * 17 + 13); IntTable t1; t1.reserve(reserve_size); for (const auto& key : keys) { t1.emplace(key ^ seed); } auto probe_histogram = GetHashtableDebugNumProbesHistogram(t1); stats.all_probes_histogram.resize( std::max(stats.all_probes_histogram.size(), probe_histogram.size())); std::transform(probe_histogram.begin(), probe_histogram.end(), stats.all_probes_histogram.begin(), stats.all_probes_histogram.begin(), std::plus()); size_t total_probe_seq_length = 0; for (size_t i = 0; i < probe_histogram.size(); ++i) { total_probe_seq_length += i * probe_histogram[i]; } stats.single_table_ratios.push_back(total_probe_seq_length * 1.0 / keys.size()); t1.erase(t1.begin(), t1.end()); } return stats; } ExpectedStats XorSeedExpectedStats() { constexpr bool kRandomizesInserts = #ifdef NDEBUG false; #else // NDEBUG true; #endif // NDEBUG // The effective load factor is larger in non-opt mode because we insert // elements out of order. switch (container_internal::Group::kWidth) { case 8: if (kRandomizesInserts) { return {0.05, 1.0, {{0.95, 0.5}}, {{0.95, 0}, {0.99, 2}, {0.999, 4}, {0.9999, 10}}}; } else { return {0.05, 2.0, {{0.95, 0.1}}, {{0.95, 0}, {0.99, 2}, {0.999, 4}, {0.9999, 10}}}; } case 16: if (kRandomizesInserts) { return {0.1, 2.0, {{0.95, 0.1}}, {{0.95, 0}, {0.99, 1}, {0.999, 8}, {0.9999, 15}}}; } else { return {0.05, 1.0, {{0.95, 0.05}}, {{0.95, 0}, {0.99, 1}, {0.999, 4}, {0.9999, 10}}}; } } LOG(FATAL) << "Unknown Group width"; return {}; } // TODO(b/80415403): Figure out why this test is so flaky, esp. on MSVC TEST(Table, DISABLED_EnsureNonQuadraticTopNXorSeedByProbeSeqLength) { ProbeStatsPerSize stats; std::vector sizes = {Group::kWidth << 5, Group::kWidth << 10}; for (size_t size : sizes) { stats[size] = CollectProbeStatsOnKeysXoredWithSeed(CollectBadMergeKeys(size), 200); } auto expected = XorSeedExpectedStats(); for (size_t size : sizes) { auto& stat = stats[size]; VerifyStats(size, expected, stat); LOG(INFO) << size << " " << stat; } } // Collect total ProbeStats on num_iters iterations of the following algorithm: // 1. Create new table // 2. Select 10% of keys and insert 10 elements key * 17 + j * 13 // 3. Collect ProbeStats from final table ProbeStats CollectProbeStatsOnLinearlyTransformedKeys( const std::vector& keys, size_t num_iters) { ProbeStats stats; std::random_device rd; std::mt19937 rng(rd()); auto linear_transform = [](size_t x, size_t y) { return x * 17 + y * 13; }; std::uniform_int_distribution dist(0, keys.size() - 1); while (num_iters--) { IntTable t1; size_t num_keys = keys.size() / 10; size_t start = dist(rng); for (size_t i = 0; i != num_keys; ++i) { for (size_t j = 0; j != 10; ++j) { t1.emplace(linear_transform(keys[(i + start) % keys.size()], j)); } } auto probe_histogram = GetHashtableDebugNumProbesHistogram(t1); stats.all_probes_histogram.resize( std::max(stats.all_probes_histogram.size(), probe_histogram.size())); std::transform(probe_histogram.begin(), probe_histogram.end(), stats.all_probes_histogram.begin(), stats.all_probes_histogram.begin(), std::plus()); size_t total_probe_seq_length = 0; for (size_t i = 0; i < probe_histogram.size(); ++i) { total_probe_seq_length += i * probe_histogram[i]; } stats.single_table_ratios.push_back(total_probe_seq_length * 1.0 / t1.size()); t1.erase(t1.begin(), t1.end()); } return stats; } ExpectedStats LinearTransformExpectedStats() { constexpr bool kRandomizesInserts = #ifdef NDEBUG false; #else // NDEBUG true; #endif // NDEBUG // The effective load factor is larger in non-opt mode because we insert // elements out of order. switch (container_internal::Group::kWidth) { case 8: if (kRandomizesInserts) { return {0.1, 0.5, {{0.95, 0.3}}, {{0.95, 0}, {0.99, 1}, {0.999, 8}, {0.9999, 15}}}; } else { return {0.4, 0.6, {{0.95, 0.5}}, {{0.95, 1}, {0.99, 14}, {0.999, 23}, {0.9999, 26}}}; } case 16: if (kRandomizesInserts) { return {0.1, 0.4, {{0.95, 0.3}}, {{0.95, 1}, {0.99, 2}, {0.999, 9}, {0.9999, 15}}}; } else { return {0.05, 0.2, {{0.95, 0.1}}, {{0.95, 0}, {0.99, 1}, {0.999, 6}, {0.9999, 10}}}; } } LOG(FATAL) << "Unknown Group width"; return {}; } // TODO(b/80415403): Figure out why this test is so flaky. TEST(Table, DISABLED_EnsureNonQuadraticTopNLinearTransformByProbeSeqLength) { ProbeStatsPerSize stats; std::vector sizes = {Group::kWidth << 5, Group::kWidth << 10}; for (size_t size : sizes) { stats[size] = CollectProbeStatsOnLinearlyTransformedKeys( CollectBadMergeKeys(size), 300); } auto expected = LinearTransformExpectedStats(); for (size_t size : sizes) { auto& stat = stats[size]; VerifyStats(size, expected, stat); LOG(INFO) << size << " " << stat; } } TEST(Table, EraseCollision) { BadTable t; // 1 2 3 t.emplace(1); t.emplace(2); t.emplace(3); EXPECT_THAT(*t.find(1), 1); EXPECT_THAT(*t.find(2), 2); EXPECT_THAT(*t.find(3), 3); EXPECT_EQ(3, t.size()); // 1 DELETED 3 t.erase(t.find(2)); EXPECT_THAT(*t.find(1), 1); EXPECT_TRUE(t.find(2) == t.end()); EXPECT_THAT(*t.find(3), 3); EXPECT_EQ(2, t.size()); // DELETED DELETED 3 t.erase(t.find(1)); EXPECT_TRUE(t.find(1) == t.end()); EXPECT_TRUE(t.find(2) == t.end()); EXPECT_THAT(*t.find(3), 3); EXPECT_EQ(1, t.size()); // DELETED DELETED DELETED t.erase(t.find(3)); EXPECT_TRUE(t.find(1) == t.end()); EXPECT_TRUE(t.find(2) == t.end()); EXPECT_TRUE(t.find(3) == t.end()); EXPECT_EQ(0, t.size()); } TEST(Table, EraseInsertProbing) { BadTable t(100); // 1 2 3 4 t.emplace(1); t.emplace(2); t.emplace(3); t.emplace(4); // 1 DELETED 3 DELETED t.erase(t.find(2)); t.erase(t.find(4)); // 1 10 3 11 12 t.emplace(10); t.emplace(11); t.emplace(12); EXPECT_EQ(5, t.size()); EXPECT_THAT(t, UnorderedElementsAre(1, 10, 3, 11, 12)); } TEST(Table, GrowthInfoDeletedBit) { BadTable t; EXPECT_TRUE( RawHashSetTestOnlyAccess::GetCommon(t).growth_info().HasNoDeleted()); int64_t init_count = static_cast( CapacityToGrowth(NormalizeCapacity(Group::kWidth + 1))); for (int64_t i = 0; i < init_count; ++i) { t.insert(i); } EXPECT_TRUE( RawHashSetTestOnlyAccess::GetCommon(t).growth_info().HasNoDeleted()); t.erase(0); EXPECT_EQ(RawHashSetTestOnlyAccess::CountTombstones(t), 1); EXPECT_FALSE( RawHashSetTestOnlyAccess::GetCommon(t).growth_info().HasNoDeleted()); t.rehash(0); EXPECT_EQ(RawHashSetTestOnlyAccess::CountTombstones(t), 0); EXPECT_TRUE( RawHashSetTestOnlyAccess::GetCommon(t).growth_info().HasNoDeleted()); } TYPED_TEST(SooTest, Clear) { TypeParam t; EXPECT_TRUE(t.find(0) == t.end()); t.clear(); EXPECT_TRUE(t.find(0) == t.end()); auto res = t.emplace(0); EXPECT_TRUE(res.second); EXPECT_EQ(1, t.size()); t.clear(); EXPECT_EQ(0, t.size()); EXPECT_TRUE(t.find(0) == t.end()); } TYPED_TEST(SooTest, Swap) { TypeParam t; EXPECT_TRUE(t.find(0) == t.end()); auto res = t.emplace(0); EXPECT_TRUE(res.second); EXPECT_EQ(1, t.size()); TypeParam u; t.swap(u); EXPECT_EQ(0, t.size()); EXPECT_EQ(1, u.size()); EXPECT_TRUE(t.find(0) == t.end()); EXPECT_THAT(*u.find(0), 0); } TYPED_TEST(SooTest, Rehash) { TypeParam t; EXPECT_TRUE(t.find(0) == t.end()); t.emplace(0); t.emplace(1); EXPECT_EQ(2, t.size()); t.rehash(128); EXPECT_EQ(2, t.size()); EXPECT_THAT(*t.find(0), 0); EXPECT_THAT(*t.find(1), 1); } TYPED_TEST(SooTest, RehashDoesNotRehashWhenNotNecessary) { TypeParam t; t.emplace(0); t.emplace(1); auto* p = &*t.find(0); t.rehash(1); EXPECT_EQ(p, &*t.find(0)); } // Following two tests use non-SOO table because they test for 0 capacity. TEST(Table, RehashZeroDoesNotAllocateOnEmptyTable) { NonSooIntTable t; t.rehash(0); EXPECT_EQ(0, t.bucket_count()); } TEST(Table, RehashZeroDeallocatesEmptyTable) { NonSooIntTable t; t.emplace(0); t.clear(); EXPECT_NE(0, t.bucket_count()); t.rehash(0); EXPECT_EQ(0, t.bucket_count()); } TYPED_TEST(SooTest, RehashZeroForcesRehash) { TypeParam t; t.emplace(0); t.emplace(1); auto* p = &*t.find(0); t.rehash(0); EXPECT_NE(p, &*t.find(0)); } TEST(Table, ConstructFromInitList) { using P = std::pair; struct Q { operator P() const { return {}; } // NOLINT }; StringTable t = {P(), Q(), {}, {{}, {}}}; } TYPED_TEST(SooTest, CopyConstruct) { TypeParam t; t.emplace(0); EXPECT_EQ(1, t.size()); { TypeParam u(t); EXPECT_EQ(1, u.size()); EXPECT_THAT(*u.find(0), 0); } { TypeParam u{t}; EXPECT_EQ(1, u.size()); EXPECT_THAT(*u.find(0), 0); } { TypeParam u = t; EXPECT_EQ(1, u.size()); EXPECT_THAT(*u.find(0), 0); } } TYPED_TEST(SooTest, CopyDifferentSizes) { TypeParam t; for (int i = 0; i < 100; ++i) { t.emplace(i); TypeParam c = t; for (int j = 0; j <= i; ++j) { ASSERT_TRUE(c.find(j) != c.end()) << "i=" << i << " j=" << j; } // Testing find miss to verify that table is not full. ASSERT_TRUE(c.find(-1) == c.end()); } } TYPED_TEST(SooTest, CopyDifferentCapacities) { for (int cap = 1; cap < 100; cap = cap * 2 + 1) { TypeParam t; t.reserve(static_cast(cap)); for (int i = 0; i <= cap; ++i) { t.emplace(i); if (i != cap && i % 5 != 0) { continue; } TypeParam c = t; for (int j = 0; j <= i; ++j) { ASSERT_TRUE(c.find(j) != c.end()) << "cap=" << cap << " i=" << i << " j=" << j; } // Testing find miss to verify that table is not full. ASSERT_TRUE(c.find(-1) == c.end()); } } } TEST(Table, CopyConstructWithAlloc) { StringTable t; t.emplace("a", "b"); EXPECT_EQ(1, t.size()); StringTable u(t, Alloc>()); EXPECT_EQ(1, u.size()); EXPECT_THAT(*u.find("a"), Pair("a", "b")); } struct ExplicitAllocIntTable : raw_hash_set, std::equal_to, Alloc> { ExplicitAllocIntTable() = default; }; TEST(Table, AllocWithExplicitCtor) { ExplicitAllocIntTable t; EXPECT_EQ(0, t.size()); } TEST(Table, MoveConstruct) { { StringTable t; t.emplace("a", "b"); EXPECT_EQ(1, t.size()); StringTable u(std::move(t)); EXPECT_EQ(1, u.size()); EXPECT_THAT(*u.find("a"), Pair("a", "b")); } { StringTable t; t.emplace("a", "b"); EXPECT_EQ(1, t.size()); StringTable u{std::move(t)}; EXPECT_EQ(1, u.size()); EXPECT_THAT(*u.find("a"), Pair("a", "b")); } { StringTable t; t.emplace("a", "b"); EXPECT_EQ(1, t.size()); StringTable u = std::move(t); EXPECT_EQ(1, u.size()); EXPECT_THAT(*u.find("a"), Pair("a", "b")); } } TEST(Table, MoveConstructWithAlloc) { StringTable t; t.emplace("a", "b"); EXPECT_EQ(1, t.size()); StringTable u(std::move(t), Alloc>()); EXPECT_EQ(1, u.size()); EXPECT_THAT(*u.find("a"), Pair("a", "b")); } TEST(Table, CopyAssign) { StringTable t; t.emplace("a", "b"); EXPECT_EQ(1, t.size()); StringTable u; u = t; EXPECT_EQ(1, u.size()); EXPECT_THAT(*u.find("a"), Pair("a", "b")); } TEST(Table, CopySelfAssign) { StringTable t; t.emplace("a", "b"); EXPECT_EQ(1, t.size()); t = *&t; EXPECT_EQ(1, t.size()); EXPECT_THAT(*t.find("a"), Pair("a", "b")); } TEST(Table, MoveAssign) { StringTable t; t.emplace("a", "b"); EXPECT_EQ(1, t.size()); StringTable u; u = std::move(t); EXPECT_EQ(1, u.size()); EXPECT_THAT(*u.find("a"), Pair("a", "b")); } TEST(Table, MoveSelfAssign) { StringTable t; t.emplace("a", "b"); EXPECT_EQ(1, t.size()); t = std::move(*&t); if (SwisstableGenerationsEnabled()) { // NOLINTNEXTLINE(bugprone-use-after-move) EXPECT_DEATH_IF_SUPPORTED(t.contains("a"), "self-move-assigned"); } // As long as we don't crash, it's fine. } TEST(Table, Equality) { StringTable t; std::vector> v = {{"a", "b"}, {"aa", "bb"}}; t.insert(std::begin(v), std::end(v)); StringTable u = t; EXPECT_EQ(u, t); } TEST(Table, Equality2) { StringTable t; std::vector> v1 = {{"a", "b"}, {"aa", "bb"}}; t.insert(std::begin(v1), std::end(v1)); StringTable u; std::vector> v2 = {{"a", "a"}, {"aa", "aa"}}; u.insert(std::begin(v2), std::end(v2)); EXPECT_NE(u, t); } TEST(Table, Equality3) { StringTable t; std::vector> v1 = {{"b", "b"}, {"bb", "bb"}}; t.insert(std::begin(v1), std::end(v1)); StringTable u; std::vector> v2 = {{"a", "a"}, {"aa", "aa"}}; u.insert(std::begin(v2), std::end(v2)); EXPECT_NE(u, t); } TYPED_TEST(SooTest, NumDeletedRegression) { TypeParam t; t.emplace(0); t.erase(t.find(0)); // construct over a deleted slot. t.emplace(0); t.clear(); } TYPED_TEST(SooTest, FindFullDeletedRegression) { TypeParam t; for (int i = 0; i < 1000; ++i) { t.emplace(i); t.erase(t.find(i)); } EXPECT_EQ(0, t.size()); } TYPED_TEST(SooTest, ReplacingDeletedSlotDoesNotRehash) { // We need to disable hashtablez to avoid issues related to SOO and sampling. SetHashtablezEnabled(false); size_t n; { // Compute n such that n is the maximum number of elements before rehash. TypeParam t; t.emplace(0); size_t c = t.bucket_count(); for (n = 1; c == t.bucket_count(); ++n) t.emplace(n); --n; } TypeParam t; t.rehash(n); const size_t c = t.bucket_count(); for (size_t i = 0; i != n; ++i) t.emplace(i); EXPECT_EQ(c, t.bucket_count()) << "rehashing threshold = " << n; t.erase(0); t.emplace(0); EXPECT_EQ(c, t.bucket_count()) << "rehashing threshold = " << n; } TEST(Table, NoThrowMoveConstruct) { ASSERT_TRUE( std::is_nothrow_copy_constructible>::value); ASSERT_TRUE(std::is_nothrow_copy_constructible< std::equal_to>::value); ASSERT_TRUE(std::is_nothrow_copy_constructible>::value); EXPECT_TRUE(std::is_nothrow_move_constructible::value); } TEST(Table, NoThrowMoveAssign) { ASSERT_TRUE( std::is_nothrow_move_assignable>::value); ASSERT_TRUE( std::is_nothrow_move_assignable>::value); ASSERT_TRUE(std::is_nothrow_move_assignable>::value); ASSERT_TRUE( absl::allocator_traits>::is_always_equal::value); EXPECT_TRUE(std::is_nothrow_move_assignable::value); } TEST(Table, NoThrowSwappable) { ASSERT_TRUE( container_internal::IsNoThrowSwappable>()); ASSERT_TRUE(container_internal::IsNoThrowSwappable< std::equal_to>()); ASSERT_TRUE(container_internal::IsNoThrowSwappable>()); EXPECT_TRUE(container_internal::IsNoThrowSwappable()); } TEST(Table, HeterogeneousLookup) { struct Hash { size_t operator()(int64_t i) const { return i; } size_t operator()(double i) const { ADD_FAILURE(); return i; } }; struct Eq { bool operator()(int64_t a, int64_t b) const { return a == b; } bool operator()(double a, int64_t b) const { ADD_FAILURE(); return a == b; } bool operator()(int64_t a, double b) const { ADD_FAILURE(); return a == b; } bool operator()(double a, double b) const { ADD_FAILURE(); return a == b; } }; struct THash { using is_transparent = void; size_t operator()(int64_t i) const { return i; } size_t operator()(double i) const { return i; } }; struct TEq { using is_transparent = void; bool operator()(int64_t a, int64_t b) const { return a == b; } bool operator()(double a, int64_t b) const { return a == b; } bool operator()(int64_t a, double b) const { return a == b; } bool operator()(double a, double b) const { return a == b; } }; raw_hash_set> s{0, 1, 2}; // It will convert to int64_t before the query. EXPECT_EQ(1, *s.find(double{1.1})); raw_hash_set> ts{0, 1, 2}; // It will try to use the double, and fail to find the object. EXPECT_TRUE(ts.find(1.1) == ts.end()); } template using CallFind = decltype(std::declval().find(17)); template using CallErase = decltype(std::declval().erase(17)); template using CallExtract = decltype(std::declval().extract(17)); template using CallPrefetch = decltype(std::declval().prefetch(17)); template using CallCount = decltype(std::declval().count(17)); template