| // 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. |
| // |
| // Generates probe length statistics for many combinations of key types and key |
| // distributions, all using the default hash function for swisstable. |
| |
| #include <memory> |
| #include <regex> // NOLINT |
| #include <vector> |
| |
| #include "absl/base/no_destructor.h" |
| #include "absl/container/flat_hash_map.h" |
| #include "absl/container/internal/hash_function_defaults.h" |
| #include "absl/container/internal/hashtable_debug.h" |
| #include "absl/container/internal/raw_hash_set.h" |
| #include "absl/random/distributions.h" |
| #include "absl/random/random.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/str_format.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/strings/strip.h" |
| #include "absl/types/optional.h" |
| |
| namespace { |
| |
| enum class OutputStyle { kRegular, kBenchmark }; |
| |
| // The --benchmark command line flag. |
| // This is populated from main(). |
| // When run in "benchmark" mode, we have different output. This allows |
| // A/B comparisons with tools like `benchy`. |
| absl::string_view benchmarks; |
| |
| OutputStyle output() { |
| return !benchmarks.empty() ? OutputStyle::kBenchmark : OutputStyle::kRegular; |
| } |
| |
| template <class T> |
| struct Policy { |
| using slot_type = T; |
| using key_type = T; |
| using init_type = T; |
| |
| template <class allocator_type, class Arg> |
| static void construct(allocator_type* alloc, slot_type* slot, |
| const Arg& arg) { |
| std::allocator_traits<allocator_type>::construct(*alloc, slot, arg); |
| } |
| |
| template <class allocator_type> |
| static void destroy(allocator_type* alloc, slot_type* slot) { |
| std::allocator_traits<allocator_type>::destroy(*alloc, slot); |
| } |
| |
| static slot_type& element(slot_type* slot) { return *slot; } |
| |
| template <class F, class... Args> |
| static auto apply(F&& f, const slot_type& arg) |
| -> decltype(std::forward<F>(f)(arg, arg)) { |
| return std::forward<F>(f)(arg, arg); |
| } |
| |
| template <class Hash> |
| static constexpr auto get_hash_slot_fn() { |
| return nullptr; |
| } |
| }; |
| |
| absl::BitGen& GlobalBitGen() { |
| static absl::NoDestructor<absl::BitGen> value; |
| return *value; |
| } |
| |
| // Keeps a pool of allocations and randomly gives one out. |
| // This introduces more randomization to the addresses given to swisstable and |
| // should help smooth out this factor from probe length calculation. |
| template <class T> |
| class RandomizedAllocator { |
| public: |
| using value_type = T; |
| |
| RandomizedAllocator() = default; |
| template <typename U> |
| RandomizedAllocator(RandomizedAllocator<U>) {} // NOLINT |
| |
| static T* allocate(size_t n) { |
| auto& pointers = GetPointers(n); |
| // Fill the pool |
| while (pointers.size() < kRandomPool) { |
| pointers.push_back(std::allocator<T>{}.allocate(n)); |
| } |
| |
| // Choose a random one. |
| size_t i = absl::Uniform<size_t>(GlobalBitGen(), 0, pointers.size()); |
| T* result = pointers[i]; |
| pointers[i] = pointers.back(); |
| pointers.pop_back(); |
| return result; |
| } |
| |
| static void deallocate(T* p, size_t n) { |
| // Just put it back on the pool. No need to release the memory. |
| GetPointers(n).push_back(p); |
| } |
| |
| private: |
| // We keep at least kRandomPool allocations for each size. |
| static constexpr size_t kRandomPool = 20; |
| |
| static std::vector<T*>& GetPointers(size_t n) { |
| static absl::NoDestructor<absl::flat_hash_map<size_t, std::vector<T*>>> m; |
| return (*m)[n]; |
| } |
| }; |
| |
| template <class T> |
| struct DefaultHash { |
| using type = absl::container_internal::hash_default_hash<T>; |
| }; |
| |
| template <class T> |
| using DefaultHashT = typename DefaultHash<T>::type; |
| |
| template <class T> |
| struct Table : absl::container_internal::raw_hash_set< |
| Policy<T>, DefaultHashT<T>, |
| absl::container_internal::hash_default_eq<T>, |
| RandomizedAllocator<T>> {}; |
| |
| struct LoadSizes { |
| size_t min_load; |
| size_t max_load; |
| }; |
| |
| LoadSizes GetMinMaxLoadSizes() { |
| static const auto sizes = [] { |
| Table<int> t; |
| |
| // First, fill enough to have a good distribution. |
| constexpr size_t kMinSize = 10000; |
| while (t.size() < kMinSize) t.insert(t.size()); |
| |
| const auto reach_min_load_factor = [&] { |
| const double lf = t.load_factor(); |
| while (lf <= t.load_factor()) t.insert(t.size()); |
| }; |
| |
| // Then, insert until we reach min load factor. |
| reach_min_load_factor(); |
| const size_t min_load_size = t.size(); |
| |
| // Keep going until we hit min load factor again, then go back one. |
| t.insert(t.size()); |
| reach_min_load_factor(); |
| |
| return LoadSizes{min_load_size, t.size() - 1}; |
| }(); |
| return sizes; |
| } |
| |
| struct Ratios { |
| double min_load; |
| double avg_load; |
| double max_load; |
| }; |
| |
| // See absl/container/internal/hashtable_debug.h for details on |
| // probe length calculation. |
| template <class ElemFn> |
| Ratios CollectMeanProbeLengths() { |
| const auto min_max_sizes = GetMinMaxLoadSizes(); |
| |
| ElemFn elem; |
| using Key = decltype(elem()); |
| Table<Key> t; |
| |
| Ratios result; |
| while (t.size() < min_max_sizes.min_load) t.insert(elem()); |
| result.min_load = |
| absl::container_internal::GetHashtableDebugProbeSummary(t).mean; |
| |
| while (t.size() < (min_max_sizes.min_load + min_max_sizes.max_load) / 2) |
| t.insert(elem()); |
| result.avg_load = |
| absl::container_internal::GetHashtableDebugProbeSummary(t).mean; |
| |
| while (t.size() < min_max_sizes.max_load) t.insert(elem()); |
| result.max_load = |
| absl::container_internal::GetHashtableDebugProbeSummary(t).mean; |
| |
| return result; |
| } |
| |
| template <int Align> |
| uintptr_t PointerForAlignment() { |
| alignas(Align) static constexpr uintptr_t kInitPointer = 0; |
| return reinterpret_cast<uintptr_t>(&kInitPointer); |
| } |
| |
| // This incomplete type is used for testing hash of pointers of different |
| // alignments. |
| // NOTE: We are generating invalid pointer values on the fly with |
| // reinterpret_cast. There are not "safely derived" pointers so using them is |
| // technically UB. It is unlikely to be a problem, though. |
| template <int Align> |
| struct Ptr; |
| |
| template <int Align> |
| Ptr<Align>* MakePtr(uintptr_t v) { |
| if (sizeof(v) == 8) { |
| constexpr int kCopyBits = 16; |
| // Ensure high bits are all the same. |
| v = static_cast<uintptr_t>(static_cast<intptr_t>(v << kCopyBits) >> |
| kCopyBits); |
| } |
| return reinterpret_cast<Ptr<Align>*>(v); |
| } |
| |
| struct IntIdentity { |
| uint64_t i; |
| friend bool operator==(IntIdentity a, IntIdentity b) { return a.i == b.i; } |
| IntIdentity operator++(int) { return IntIdentity{i++}; } |
| }; |
| |
| template <int Align> |
| struct PtrIdentity { |
| explicit PtrIdentity(uintptr_t val = PointerForAlignment<Align>()) : i(val) {} |
| uintptr_t i; |
| friend bool operator==(PtrIdentity a, PtrIdentity b) { return a.i == b.i; } |
| PtrIdentity operator++(int) { |
| PtrIdentity p(i); |
| i += Align; |
| return p; |
| } |
| }; |
| |
| constexpr char kStringFormat[] = "/path/to/file/name-%07d-of-9999999.txt"; |
| |
| template <bool small> |
| struct String { |
| std::string value; |
| static std::string Make(uint32_t v) { |
| return {small ? absl::StrCat(v) : absl::StrFormat(kStringFormat, v)}; |
| } |
| }; |
| |
| template <> |
| struct DefaultHash<IntIdentity> { |
| struct type { |
| size_t operator()(IntIdentity t) const { return t.i; } |
| }; |
| }; |
| |
| template <int Align> |
| struct DefaultHash<PtrIdentity<Align>> { |
| struct type { |
| size_t operator()(PtrIdentity<Align> t) const { return t.i; } |
| }; |
| }; |
| |
| template <class T> |
| struct Sequential { |
| T operator()() const { return current++; } |
| mutable T current{}; |
| }; |
| |
| template <int Align> |
| struct Sequential<Ptr<Align>*> { |
| Ptr<Align>* operator()() const { |
| auto* result = MakePtr<Align>(current); |
| current += Align; |
| return result; |
| } |
| mutable uintptr_t current = PointerForAlignment<Align>(); |
| }; |
| |
| |
| template <bool small> |
| struct Sequential<String<small>> { |
| std::string operator()() const { return String<small>::Make(current++); } |
| mutable uint32_t current = 0; |
| }; |
| |
| template <class T, class U> |
| struct Sequential<std::pair<T, U>> { |
| mutable Sequential<T> tseq; |
| mutable Sequential<U> useq; |
| |
| using RealT = decltype(tseq()); |
| using RealU = decltype(useq()); |
| |
| mutable std::vector<RealT> ts; |
| mutable std::vector<RealU> us; |
| mutable size_t ti = 0, ui = 0; |
| |
| std::pair<RealT, RealU> operator()() const { |
| std::pair<RealT, RealU> value{get_t(), get_u()}; |
| if (ti == 0) { |
| ti = ui + 1; |
| ui = 0; |
| } else { |
| --ti; |
| ++ui; |
| } |
| return value; |
| } |
| |
| RealT get_t() const { |
| while (ti >= ts.size()) ts.push_back(tseq()); |
| return ts[ti]; |
| } |
| |
| RealU get_u() const { |
| while (ui >= us.size()) us.push_back(useq()); |
| return us[ui]; |
| } |
| }; |
| |
| template <class T, int percent_skip> |
| struct AlmostSequential { |
| mutable Sequential<T> current; |
| |
| auto operator()() const -> decltype(current()) { |
| while (absl::Uniform(GlobalBitGen(), 0.0, 1.0) <= percent_skip / 100.) |
| current(); |
| return current(); |
| } |
| }; |
| |
| struct Uniform { |
| template <typename T> |
| T operator()(T) const { |
| return absl::Uniform<T>(absl::IntervalClosed, GlobalBitGen(), T{0}, ~T{0}); |
| } |
| }; |
| |
| struct Gaussian { |
| template <typename T> |
| T operator()(T) const { |
| double d; |
| do { |
| d = absl::Gaussian<double>(GlobalBitGen(), 1e6, 1e4); |
| } while (d <= 0 || d > std::numeric_limits<T>::max() / 2); |
| return static_cast<T>(d); |
| } |
| }; |
| |
| struct Zipf { |
| template <typename T> |
| T operator()(T) const { |
| return absl::Zipf<T>(GlobalBitGen(), std::numeric_limits<T>::max(), 1.6); |
| } |
| }; |
| |
| template <class T, class Dist> |
| struct Random { |
| T operator()() const { return Dist{}(T{}); } |
| }; |
| |
| template <class Dist, int Align> |
| struct Random<Ptr<Align>*, Dist> { |
| Ptr<Align>* operator()() const { |
| return MakePtr<Align>(Random<uintptr_t, Dist>{}() * Align); |
| } |
| }; |
| |
| template <class Dist> |
| struct Random<IntIdentity, Dist> { |
| IntIdentity operator()() const { |
| return IntIdentity{Random<uint64_t, Dist>{}()}; |
| } |
| }; |
| |
| template <class Dist, int Align> |
| struct Random<PtrIdentity<Align>, Dist> { |
| PtrIdentity<Align> operator()() const { |
| return PtrIdentity<Align>{Random<uintptr_t, Dist>{}() * Align}; |
| } |
| }; |
| |
| template <class Dist, bool small> |
| struct Random<String<small>, Dist> { |
| std::string operator()() const { |
| return String<small>::Make(Random<uint32_t, Dist>{}()); |
| } |
| }; |
| |
| template <class T, class U, class Dist> |
| struct Random<std::pair<T, U>, Dist> { |
| auto operator()() const |
| -> decltype(std::make_pair(Random<T, Dist>{}(), Random<U, Dist>{}())) { |
| return std::make_pair(Random<T, Dist>{}(), Random<U, Dist>{}()); |
| } |
| }; |
| |
| template <typename> |
| std::string Name(); |
| |
| std::string Name(uint32_t*) { return "u32"; } |
| std::string Name(uint64_t*) { return "u64"; } |
| std::string Name(IntIdentity*) { return "IntIdentity"; } |
| |
| template <int Align> |
| std::string Name(Ptr<Align>**) { |
| return absl::StrCat("Ptr", Align); |
| } |
| |
| template <int Align> |
| std::string Name(PtrIdentity<Align>*) { |
| return absl::StrCat("PtrIdentity", Align); |
| } |
| |
| template <bool small> |
| std::string Name(String<small>*) { |
| return small ? "StrS" : "StrL"; |
| } |
| |
| template <class T, class U> |
| std::string Name(std::pair<T, U>*) { |
| if (output() == OutputStyle::kBenchmark) |
| return absl::StrCat("P_", Name<T>(), "_", Name<U>()); |
| return absl::StrCat("P<", Name<T>(), ",", Name<U>(), ">"); |
| } |
| |
| template <class T> |
| std::string Name(Sequential<T>*) { |
| return "Sequential"; |
| } |
| |
| template <class T, int P> |
| std::string Name(AlmostSequential<T, P>*) { |
| return absl::StrCat("AlmostSeq_", P); |
| } |
| |
| template <class T> |
| std::string Name(Random<T, Uniform>*) { |
| return "UnifRand"; |
| } |
| |
| template <class T> |
| std::string Name(Random<T, Gaussian>*) { |
| return "GausRand"; |
| } |
| |
| template <class T> |
| std::string Name(Random<T, Zipf>*) { |
| return "ZipfRand"; |
| } |
| |
| template <typename T> |
| std::string Name() { |
| return Name(static_cast<T*>(nullptr)); |
| } |
| |
| constexpr int kNameWidth = 15; |
| constexpr int kDistWidth = 16; |
| |
| bool CanRunBenchmark(absl::string_view name) { |
| static const absl::NoDestructor<absl::optional<std::regex>> filter([] { |
| return benchmarks.empty() || benchmarks == "all" |
| ? absl::nullopt |
| : absl::make_optional(std::regex(std::string(benchmarks))); |
| }()); |
| return !filter->has_value() || std::regex_search(std::string(name), **filter); |
| } |
| |
| struct Result { |
| std::string name; |
| std::string dist_name; |
| Ratios ratios; |
| }; |
| |
| template <typename T, typename Dist> |
| void RunForTypeAndDistribution(std::vector<Result>& results) { |
| std::string name = absl::StrCat(Name<T>(), "/", Name<Dist>()); |
| // We have to check against all three names (min/avg/max) before we run it. |
| // If any of them is enabled, we run it. |
| if (!CanRunBenchmark(absl::StrCat(name, "/min")) && |
| !CanRunBenchmark(absl::StrCat(name, "/avg")) && |
| !CanRunBenchmark(absl::StrCat(name, "/max"))) { |
| return; |
| } |
| results.push_back({Name<T>(), Name<Dist>(), CollectMeanProbeLengths<Dist>()}); |
| } |
| |
| template <class T> |
| void RunForType(std::vector<Result>& results) { |
| RunForTypeAndDistribution<T, Sequential<T>>(results); |
| RunForTypeAndDistribution<T, AlmostSequential<T, 20>>(results); |
| RunForTypeAndDistribution<T, AlmostSequential<T, 50>>(results); |
| RunForTypeAndDistribution<T, Random<T, Uniform>>(results); |
| #ifdef NDEBUG |
| // Disable these in non-opt mode because they take too long. |
| RunForTypeAndDistribution<T, Random<T, Gaussian>>(results); |
| RunForTypeAndDistribution<T, Random<T, Zipf>>(results); |
| #endif // NDEBUG |
| } |
| |
| } // namespace |
| |
| int main(int argc, char** argv) { |
| // Parse the benchmark flags. Ignore all of them except the regex pattern. |
| for (int i = 1; i < argc; ++i) { |
| absl::string_view arg = argv[i]; |
| const auto next = [&] { return argv[std::min(i + 1, argc - 1)]; }; |
| |
| if (absl::ConsumePrefix(&arg, "--benchmark_filter")) { |
| if (arg == "") { |
| // --benchmark_filter X |
| benchmarks = next(); |
| } else if (absl::ConsumePrefix(&arg, "=")) { |
| // --benchmark_filter=X |
| benchmarks = arg; |
| } |
| } |
| |
| // Any --benchmark flag turns on the mode. |
| if (absl::ConsumePrefix(&arg, "--benchmark")) { |
| if (benchmarks.empty()) benchmarks="all"; |
| } |
| } |
| |
| std::vector<Result> results; |
| RunForType<uint64_t>(results); |
| RunForType<IntIdentity>(results); |
| RunForType<Ptr<8>*>(results); |
| RunForType<Ptr<16>*>(results); |
| RunForType<Ptr<32>*>(results); |
| RunForType<Ptr<64>*>(results); |
| RunForType<PtrIdentity<8>>(results); |
| RunForType<PtrIdentity<16>>(results); |
| RunForType<PtrIdentity<32>>(results); |
| RunForType<PtrIdentity<64>>(results); |
| RunForType<std::pair<uint32_t, uint32_t>>(results); |
| RunForType<String<true>>(results); |
| RunForType<String<false>>(results); |
| RunForType<std::pair<uint64_t, String<true>>>(results); |
| RunForType<std::pair<String<true>, uint64_t>>(results); |
| RunForType<std::pair<uint64_t, String<false>>>(results); |
| RunForType<std::pair<String<false>, uint64_t>>(results); |
| |
| switch (output()) { |
| case OutputStyle::kRegular: |
| absl::PrintF("%-*s%-*s Min Avg Max\n%s\n", kNameWidth, |
| "Type", kDistWidth, "Distribution", |
| std::string(kNameWidth + kDistWidth + 10 * 3, '-')); |
| for (const auto& result : results) { |
| absl::PrintF("%-*s%-*s %8.4f %8.4f %8.4f\n", kNameWidth, result.name, |
| kDistWidth, result.dist_name, result.ratios.min_load, |
| result.ratios.avg_load, result.ratios.max_load); |
| } |
| break; |
| case OutputStyle::kBenchmark: { |
| absl::PrintF("{\n"); |
| absl::PrintF(" \"benchmarks\": [\n"); |
| absl::string_view comma; |
| for (const auto& result : results) { |
| auto print = [&](absl::string_view stat, double Ratios::*val) { |
| std::string name = |
| absl::StrCat(result.name, "/", result.dist_name, "/", stat); |
| // Check the regex again. We might had have enabled only one of the |
| // stats for the benchmark. |
| if (!CanRunBenchmark(name)) return; |
| absl::PrintF(" %s{\n", comma); |
| absl::PrintF(" \"cpu_time\": %f,\n", 1e9 * result.ratios.*val); |
| absl::PrintF(" \"real_time\": %f,\n", 1e9 * result.ratios.*val); |
| absl::PrintF(" \"iterations\": 1,\n"); |
| absl::PrintF(" \"name\": \"%s\",\n", name); |
| absl::PrintF(" \"time_unit\": \"ns\"\n"); |
| absl::PrintF(" }\n"); |
| comma = ","; |
| }; |
| print("min", &Ratios::min_load); |
| print("avg", &Ratios::avg_load); |
| print("max", &Ratios::max_load); |
| } |
| absl::PrintF(" ],\n"); |
| absl::PrintF(" \"context\": {\n"); |
| absl::PrintF(" }\n"); |
| absl::PrintF("}\n"); |
| break; |
| } |
| } |
| |
| return 0; |
| } |