From b016d9e010064a247eaa764b2b9d659513e841b1 Mon Sep 17 00:00:00 2001 From: Grey Golla Date: Wed, 10 Jul 2024 13:24:19 -0700 Subject: [PATCH] Add runtime benchmarks for `Fixed*Map` data structures This adds a new set of tests tagged "benchmark" in bazel, which are filtered out of the default `test_suite` target. This means that they will only be built/run manually. --- BUILD.bazel | 65 ++++++++++++++---- CMakeLists.txt | 9 ++- test/benchmarks/map_clear.cpp | 113 ++++++++++++++++++++++++++++++++ test/benchmarks/map_copy.cpp | 116 +++++++++++++++++++++++++++++++++ test/benchmarks/map_lookup.cpp | 116 +++++++++++++++++++++++++++++++++ test/benchmarks/map_utils.hpp | 90 +++++++++++++++++++++++++ test/fixed_map_perf_test.cpp | 89 ------------------------- test/fixed_map_test.cpp | 73 ++++++++++++++++++--- 8 files changed, 557 insertions(+), 114 deletions(-) create mode 100644 test/benchmarks/map_clear.cpp create mode 100644 test/benchmarks/map_copy.cpp create mode 100644 test/benchmarks/map_lookup.cpp create mode 100644 test/benchmarks/map_utils.hpp delete mode 100644 test/fixed_map_perf_test.cpp diff --git a/BUILD.bazel b/BUILD.bazel index e308ef58..7fddec97 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -1157,20 +1157,6 @@ cc_test( copts = ["-std=c++20"], ) -cc_test( - name = "fixed_map_perf_test", - srcs = ["test/fixed_map_perf_test.cpp"], - deps = [ - ":consteval_compare", - ":fixed_index_based_storage", - ":fixed_map", - ":fixed_red_black_tree", - "@com_google_googletest//:gtest_main", - "@com_google_benchmark//:benchmark_main", - ], - copts = ["-std=c++20"], -) - cc_test( name = "fixed_list_test", srcs = ["test/fixed_list_test.cpp"], @@ -1621,6 +1607,57 @@ cc_test( copts = ["-std=c++20"], ) +cc_library( + name = "benchmark_map_utils", + hdrs = ["test/benchmarks/map_utils.hpp"], +) + +cc_test( + name = "map_copy_bench", + srcs = ["test/benchmarks/map_copy.cpp"], + deps = [ + ":consteval_compare", + ":fixed_map", + ":fixed_unordered_map", + ":mock_testing_types", + ":benchmark_map_utils", + "@com_google_benchmark//:benchmark_main", + ], + copts = ["-std=c++20"], + tags = ["benchmark"], +) + +cc_test( + name = "map_lookup_bench", + srcs = ["test/benchmarks/map_lookup.cpp"], + deps = [ + ":consteval_compare", + ":fixed_map", + ":fixed_unordered_map", + ":mock_testing_types", + ":benchmark_map_utils", + "@com_google_benchmark//:benchmark_main", + ], + copts = ["-std=c++20"], + tags = ["benchmark"], +) + +cc_test( + name = "map_clear_bench", + srcs = ["test/benchmarks/map_clear.cpp"], + deps = [ + ":consteval_compare", + ":fixed_map", + ":fixed_unordered_map", + ":mock_testing_types", + ":benchmark_map_utils", + "@com_google_benchmark//:benchmark_main", + ], + copts = ["-std=c++20"], + tags = ["benchmark"], +) + test_suite( name = "all_tests", + tags = ["-benchmark"], ) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6cadad41..151e96ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -171,8 +171,6 @@ if(BUILD_TESTS) add_test_dependencies(fixed_list_test) add_executable(fixed_map_test test/fixed_map_test.cpp) add_test_dependencies(fixed_map_test) - add_executable(fixed_map_perf_test test/fixed_map_perf_test.cpp) - add_test_dependencies(fixed_map_perf_test) add_executable(fixed_red_black_tree_test test/fixed_red_black_tree_test.cpp) add_test_dependencies(fixed_red_black_tree_test) add_executable(fixed_red_black_tree_view_test test/fixed_red_black_tree_view_test.cpp) @@ -231,6 +229,13 @@ if(BUILD_TESTS) add_test_dependencies(tuples_test) add_executable(type_name_test test/type_name_test.cpp) add_test_dependencies(type_name_test) + + add_executable(map_copy_bench test/benchmarks/map_copy.cpp) + add_test_dependencies(map_copy_bench) + add_executable(map_lookup_bench test/benchmarks/map_lookup.cpp) + add_test_dependencies(map_lookup_bench) + add_executable(map_clear_bench test/benchmarks/map_clear.cpp) + add_test_dependencies(map_clear_bench) endif() option(FIXED_CONTAINERS_OPT_INSTALL "Enable install target" ${PROJECT_IS_TOP_LEVEL}) diff --git a/test/benchmarks/map_clear.cpp b/test/benchmarks/map_clear.cpp new file mode 100644 index 00000000..b913ac7e --- /dev/null +++ b/test/benchmarks/map_clear.cpp @@ -0,0 +1,113 @@ +#include "fixed_containers/fixed_map.hpp" +#include "fixed_containers/fixed_unordered_map.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace fixed_containers +{ + +namespace +{ +template +void benchmark_map_copy(benchmark::State& state) +{ + const int64_t nelem = state.range(0); + MapType instance = {}; + + using KeyType = typename MapType::key_type; + for (int64_t i = 0; i < nelem; i++) + { + instance.try_emplace(static_cast(i)); + } + + for (const auto& _ : state) + { + MapType instance2{instance}; + benchmark::DoNotOptimize(instance2); + } +} + +template +void benchmark_map_copy_then_clear(benchmark::State& state) +{ + using KeyType = typename MapType::key_type; + MapType instance{}; + const int64_t nelem = state.range(0); + for (int64_t i = 0; i < nelem; i++) + { + instance.try_emplace(static_cast(i)); + } + + for (const auto& _ : state) + { + MapType instance2{instance}; + instance2.clear(); + benchmark::DoNotOptimize(instance2); + } +} + +template +void benchmark_map_copy_then_reconstruct(benchmark::State& state) +{ + using KeyType = typename MapType::key_type; + MapType instance{}; + const int64_t nelem = state.range(0); + for (int64_t i = 0; i < nelem; i++) + { + instance.try_emplace(static_cast(i)); + } + + for (const auto& _ : state) + { + MapType instance2{instance}; + instance2 = {}; + benchmark::DoNotOptimize(instance2); + } +} + +template +void benchmark_array_clear(benchmark::State& state) +{ + ArrType instance{}; + + for (const auto& _ : state) + { + instance.fill(0); + benchmark::DoNotOptimize(instance); + } +} +} // namespace + +constexpr std::size_t MAX_SIZE = 8 << 13; + +BENCHMARK(benchmark_map_copy>)->Range(16, MAX_SIZE); +BENCHMARK(benchmark_map_copy_then_clear>)->Range(16, MAX_SIZE); +BENCHMARK(benchmark_map_copy_then_reconstruct>)->Range(16, MAX_SIZE); + +BENCHMARK(benchmark_map_copy>)->Range(16, MAX_SIZE); +BENCHMARK(benchmark_map_copy_then_clear>)->Range(16, MAX_SIZE); +BENCHMARK(benchmark_map_copy_then_reconstruct>)->Range(16, MAX_SIZE); + +BENCHMARK(benchmark_map_copy>)->Range(16, MAX_SIZE); +BENCHMARK(benchmark_map_copy_then_clear>)->Range(16, MAX_SIZE); +BENCHMARK(benchmark_map_copy_then_reconstruct>)->Range(16, MAX_SIZE); + +BENCHMARK(benchmark_map_copy>)->Range(16, MAX_SIZE); +BENCHMARK(benchmark_map_copy_then_clear>) + ->Range(16, MAX_SIZE); +BENCHMARK(benchmark_map_copy_then_reconstruct>) + ->Range(16, MAX_SIZE); + +// more-or-less the theoretical best performance we could possibly get for a full FixedUnorderedMap +// (just 0 out every bucket) +BENCHMARK(benchmark_array_clear>); + +} // namespace fixed_containers + +BENCHMARK_MAIN(); diff --git a/test/benchmarks/map_copy.cpp b/test/benchmarks/map_copy.cpp new file mode 100644 index 00000000..097ac1b9 --- /dev/null +++ b/test/benchmarks/map_copy.cpp @@ -0,0 +1,116 @@ +#include "map_utils.hpp" + +#include "../mock_testing_types.hpp" +#include "fixed_containers/fixed_unordered_map.hpp" + +#include + +#include + +namespace fixed_containers +{ + +namespace +{ +template +void benchmark_map_copy_fresh(benchmark::State& state) +{ + const int64_t nelem = state.range(0); + MapType instance = {}; + + using KeyType = typename MapType::key_type; + for (int64_t i = 0; i < nelem; i++) + { + instance.try_emplace(static_cast(i)); + } + + for (const auto& _ : state) + { + MapType instance2{instance}; + benchmark::DoNotOptimize(instance2); + } +} + +template +void benchmark_map_iterate_copy_fresh(benchmark::State& state) +{ + const int64_t nelem = state.range(0); + MapType instance = {}; + + using KeyType = typename MapType::key_type; + for (int64_t i = 0; i < nelem; i++) + { + instance.try_emplace(static_cast(i)); + } + + for (const auto& _ : state) + { + MapType instance2{}; + for (const auto& elem : instance) + { + instance2.try_emplace(elem.first, elem.second); + } + benchmark::DoNotOptimize(instance2); + } +} + +template +void benchmark_map_copy_shuffled(benchmark::State& state) +{ + const int64_t nelem = state.range(0); + auto instance = map_benchmarks::make_shuffled_map(); + + using KeyType = typename MapType::key_type; + for (int64_t i = 0; i < nelem; i++) + { + instance.try_emplace(static_cast(i)); + } + + for (const auto& _ : state) + { + MapType instance2{instance}; + benchmark::DoNotOptimize(instance2); + } +} + +template +void benchmark_map_iterate_copy_shuffled(benchmark::State& state) +{ + const int64_t nelem = state.range(0); + auto instance = map_benchmarks::make_shuffled_map(); + + using KeyType = typename MapType::key_type; + for (int64_t i = 0; i < nelem; i++) + { + instance.try_emplace(static_cast(i)); + } + + for (const auto& _ : state) + { + MapType instance2{}; + for (const auto& elem : instance) + { + instance2.try_emplace(elem.first, elem.second); + } + benchmark::DoNotOptimize(instance2); + } +} +} // namespace + +BENCHMARK( + benchmark_map_copy_fresh>) + ->DenseRange(1024, 8 << 14, 1024); +BENCHMARK(benchmark_map_iterate_copy_fresh< + FixedUnorderedMap>) + ->DenseRange(1024, 8 << 14, 1024); + +BENCHMARK( + benchmark_map_copy_shuffled>) + ->DenseRange(1024, 8 << 14, 1024); +BENCHMARK(benchmark_map_iterate_copy_shuffled< + FixedUnorderedMap>) + ->DenseRange(1024, 8 << 14, 1024); + +} // namespace fixed_containers + +BENCHMARK_MAIN(); diff --git a/test/benchmarks/map_lookup.cpp b/test/benchmarks/map_lookup.cpp new file mode 100644 index 00000000..32fdf3e6 --- /dev/null +++ b/test/benchmarks/map_lookup.cpp @@ -0,0 +1,116 @@ +#include "map_utils.hpp" + +#include "fixed_containers/fixed_map.hpp" +#include "fixed_containers/fixed_unordered_map.hpp" + +#include + +#include +#include +#include + +namespace fixed_containers +{ +namespace +{ +template +void benchmark_map_lookup_fresh(benchmark::State& state) +{ + using KeyType = typename MapType::key_type; + MapType instance{}; + const int64_t nelem = state.range(0); + for (int64_t i = 0; i < nelem; i++) + { + instance.try_emplace(static_cast(i)); + } + + for (const auto& _ : state) + { + for (int64_t i = 0; i < nelem; i += nelem / 8) + { + auto& entry = instance.at(static_cast(i)); + benchmark::DoNotOptimize(entry); + } + } +} + +template +void benchmark_map_lookup_shuffled(benchmark::State& state) +{ + using KeyType = typename MapType::key_type; + auto instance = map_benchmarks::make_shuffled_map(); + const int64_t nelem = state.range(0); + for (int64_t i = 0; i < nelem; i++) + { + instance.try_emplace(static_cast(i)); + } + + for (const auto& _ : state) + { + for (int64_t i = 0; i < nelem; i += nelem / 8) + { + auto& entry = instance.at(static_cast(i)); + benchmark::DoNotOptimize(entry); + } + } +} + +template +void benchmark_map_iterate_fresh(benchmark::State& state) +{ + using KeyType = typename MapType::key_type; + MapType instance{}; + const int64_t nelem = state.range(0); + for (int64_t i = 0; i < nelem; i++) + { + instance.try_emplace(static_cast(i)); + } + + for (const auto& _ : state) + { + for (auto pair : instance) + { + benchmark::DoNotOptimize(pair.second); + } + } +} + +template +void benchmark_map_iterate_shuffled(benchmark::State& state) +{ + using KeyType = typename MapType::key_type; + auto instance = map_benchmarks::make_shuffled_map(); + const int64_t nelem = state.range(0); + for (int64_t i = 0; i < nelem; i++) + { + instance.try_emplace(static_cast(i)); + } + + for (const auto& _ : state) + { + for (auto pair : instance) + { + benchmark::DoNotOptimize(pair.second); + } + } +} +} // namespace + +BENCHMARK(benchmark_map_lookup_fresh>)->Range(256, 8 << 14); +BENCHMARK(benchmark_map_lookup_fresh>)->Range(256, 8 << 14); +BENCHMARK(benchmark_map_lookup_fresh>)->Range(256, 8 << 14); +BENCHMARK(benchmark_map_lookup_fresh>)->Range(256, 8 << 14); +BENCHMARK(benchmark_map_lookup_shuffled>)->Range(256, 8 << 14); +BENCHMARK(benchmark_map_lookup_shuffled>)->Range(256, 8 << 14); + +BENCHMARK(benchmark_map_iterate_fresh>)->Range(256, 8 << 14); +BENCHMARK(benchmark_map_iterate_fresh>)->Range(256, 8 << 14); +BENCHMARK(benchmark_map_iterate_fresh>)->Range(256, 8 << 14); +BENCHMARK(benchmark_map_iterate_fresh>)->Range(256, 8 << 14); +BENCHMARK(benchmark_map_iterate_shuffled>)->Range(256, 8 << 14); +BENCHMARK(benchmark_map_iterate_shuffled>) + ->Range(256, 8 << 14); + +} // namespace fixed_containers + +BENCHMARK_MAIN(); diff --git a/test/benchmarks/map_utils.hpp b/test/benchmarks/map_utils.hpp new file mode 100644 index 00000000..52df78b0 --- /dev/null +++ b/test/benchmarks/map_utils.hpp @@ -0,0 +1,90 @@ +#pragma once + +#include +#include + +namespace fixed_containers::map_benchmarks +{ + +template +[[maybe_unused]] static void del(MapType& map, int64_t divisor) +{ + auto iter = map.begin(); + while (iter != map.end()) + { + if (iter->first % divisor == 0) + { + iter = map.erase(iter); + } + else + { + iter++; + } + } +} + +template +[[maybe_unused]] static void replace_low(MapType& map, std::size_t divisor) +{ + using KeyType = typename MapType::key_type; + for (std::size_t i = 0; i < map.max_size(); i += divisor) + { + map.try_emplace(static_cast(i)); + } +} + +template +[[maybe_unused]] static void replace_high(MapType& map, std::size_t divisor) +{ + using KeyType = typename MapType::key_type; + // find the largest multiple smaller than `n` + const std::size_t start = ((map.max_size() - 1) / divisor) * divisor; + + for (std::size_t i = start; i > 0; i -= divisor) + { + map.try_emplace(static_cast(i)); + } +} + +// create a "well-used" map, so that new elements will be inserted into dispersed spots in the map +// instead of spots with good memory locality +template +[[maybe_unused]] static MapType make_shuffled_map() +{ + using KeyType = typename MapType::key_type; + MapType instance{}; + // fill the map completely + for (std::size_t i = 0; i < instance.max_size(); i++) + { + instance.try_emplace(static_cast(i)); + } + + // delete and replace chunks of the map + del(instance, 2); + del(instance, 5); + del(instance, 227); + replace_low(instance, 5); + replace_high(instance, 2); + replace_low(instance, 227); + del(instance, 13); + del(instance, 21); + del(instance, 31); + replace_high(instance, 21); + replace_low(instance, 13); + replace_high(instance, 31); + del(instance, 3); + del(instance, 7); + replace_low(instance, 3); + replace_high(instance, 7); + + // clear the map + del(instance, 997); + del(instance, 333); + del(instance, 1023); + del(instance, 15); + del(instance, 1); + + return instance; +} + +} // namespace fixed_containers::map_benchmarks diff --git a/test/fixed_map_perf_test.cpp b/test/fixed_map_perf_test.cpp deleted file mode 100644 index 5ede12cc..00000000 --- a/test/fixed_map_perf_test.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "fixed_containers/consteval_compare.hpp" -#include "fixed_containers/fixed_index_based_storage.hpp" -#include "fixed_containers/fixed_map.hpp" -#include "fixed_containers/fixed_red_black_tree_nodes.hpp" - -#include - -#include -#include -#include -#include -#include - -namespace fixed_containers -{ -namespace -{ -using V = std::array, 30>; -constexpr std::size_t CAP = 130; - -template -using CompactPoolFixedMap = - FixedMap, - fixed_red_black_tree_detail::RedBlackTreeNodeColorCompactness::EMBEDDED_COLOR, - FixedIndexBasedPoolStorage>; -static_assert(std::is_same_v, CompactPoolFixedMap>); - -template -using CompactContiguousFixedMap = - FixedMap, - fixed_red_black_tree_detail::RedBlackTreeNodeColorCompactness::EMBEDDED_COLOR, - FixedIndexBasedContiguousStorage>; - -template -using DedicatedColorBitPoolFixedMap = - FixedMap, - fixed_red_black_tree_detail::RedBlackTreeNodeColorCompactness::DEDICATED_COLOR, - FixedIndexBasedContiguousStorage>; - -template -using DedicatedColorBitContiguousFixedMap = - FixedMap, - fixed_red_black_tree_detail::RedBlackTreeNodeColorCompactness::DEDICATED_COLOR, - FixedIndexBasedContiguousStorage>; - -// The reference boost-based fixed_map (with an array-backed pool-allocator) was at 51000 -// at the time of writing. -static_assert(consteval_compare::equal<50992, sizeof(FixedMap)>); -static_assert(consteval_compare::equal<50992, sizeof(CompactPoolFixedMap)>); -static_assert(consteval_compare::equal<50992, sizeof(CompactContiguousFixedMap)>); -static_assert(consteval_compare::equal<52032, sizeof(DedicatedColorBitPoolFixedMap)>); -static_assert( - consteval_compare::equal<52032, sizeof(DedicatedColorBitContiguousFixedMap)>); - -template -void benchmark_map_lookup(benchmark::State& state) -{ - using KeyType = typename MapType::key_type; - MapType instance{}; - for (std::size_t i = 0; i < 100; i++) - { - instance.try_emplace(static_cast(i)); - } - - for (auto _ : state) - { - auto& entry = instance.at(7); - benchmark::DoNotOptimize(entry); - } -} - -BENCHMARK(benchmark_map_lookup>); -BENCHMARK(benchmark_map_lookup>); -} // namespace -} // namespace fixed_containers - -BENCHMARK_MAIN(); diff --git a/test/fixed_map_test.cpp b/test/fixed_map_test.cpp index c4c5722c..390eac49 100644 --- a/test/fixed_map_test.cpp +++ b/test/fixed_map_test.cpp @@ -8,6 +8,8 @@ #include "fixed_containers/assert_or_abort.hpp" #include "fixed_containers/concepts.hpp" #include "fixed_containers/consteval_compare.hpp" +#include "fixed_containers/fixed_index_based_storage.hpp" +#include "fixed_containers/fixed_red_black_tree_nodes.hpp" #include "fixed_containers/max_size.hpp" #include "fixed_containers/memory.hpp" @@ -78,6 +80,59 @@ static_assert(ranges::bidirectional_iterator); } // namespace +// verify that the FixedMap takes the expected amount of memory +namespace +{ +using V = std::array, 30>; +constexpr std::size_t CAP = 130; + +template +using CompactPoolFixedMap = + FixedMap, + fixed_red_black_tree_detail::RedBlackTreeNodeColorCompactness::EMBEDDED_COLOR, + FixedIndexBasedPoolStorage>; +static_assert(std::is_same_v, CompactPoolFixedMap>); + +template +using CompactContiguousFixedMap = + FixedMap, + fixed_red_black_tree_detail::RedBlackTreeNodeColorCompactness::EMBEDDED_COLOR, + FixedIndexBasedContiguousStorage>; + +template +using DedicatedColorBitPoolFixedMap = + FixedMap, + fixed_red_black_tree_detail::RedBlackTreeNodeColorCompactness::DEDICATED_COLOR, + FixedIndexBasedContiguousStorage>; + +template +using DedicatedColorBitContiguousFixedMap = + FixedMap, + fixed_red_black_tree_detail::RedBlackTreeNodeColorCompactness::DEDICATED_COLOR, + FixedIndexBasedContiguousStorage>; + +// The reference boost-based fixed_map (with an array-backed pool-allocator) was at 51000 +// at the time of writing. +static_assert(consteval_compare::equal<50992, sizeof(FixedMap)>); +static_assert(consteval_compare::equal<50992, sizeof(CompactPoolFixedMap)>); +static_assert(consteval_compare::equal<50992, sizeof(CompactContiguousFixedMap)>); +static_assert(consteval_compare::equal<52032, sizeof(DedicatedColorBitPoolFixedMap)>); +static_assert( + consteval_compare::equal<52032, sizeof(DedicatedColorBitContiguousFixedMap)>); +} // namespace + TEST(FixedMap, DefaultConstructor) { constexpr FixedMap VAL1{}; @@ -1520,7 +1575,7 @@ TEST(FixedMap, ComplexNontrivialCopies) } auto map_2{map_1}; - for(const auto& pair : map_1) + for (const auto& pair : map_1) { EXPECT_TRUE(map_2.contains(pair.first)); } @@ -1531,7 +1586,7 @@ TEST(FixedMap, ComplexNontrivialCopies) map_2.try_emplace(i + 100); } auto map_3{map_1}; - for(const auto& pair : map_1) + for (const auto& pair : map_1) { EXPECT_TRUE(map_3.contains(pair.first)); } @@ -1542,20 +1597,20 @@ TEST(FixedMap, ComplexNontrivialCopies) map_3.try_emplace(i + 100); } auto map_4{map_1}; - for(const auto& pair : map_1) + for (const auto& pair : map_1) { EXPECT_TRUE(map_4.contains(pair.first)); } EXPECT_EQ(map_4.size(), map_1.size()); map_1 = map_2; - for(const auto& pair : map_2) + for (const auto& pair : map_2) { EXPECT_TRUE(map_1.contains(pair.first)); } map_1.clear(); map_1 = map_3; - for(const auto& pair : map_3) + for (const auto& pair : map_3) { EXPECT_TRUE(map_1.contains(pair.first)); } @@ -1572,7 +1627,7 @@ TEST(FixedMap, ComplexNontrivialCopies) map_1.clear(); map_1 = map_4; - for(const auto& pair : map_4) + for (const auto& pair : map_4) { EXPECT_TRUE(map_1.contains(pair.first)); } @@ -1591,7 +1646,7 @@ TEST(FixedUnorderedMap, ComplexNontrivialMoves) } FM map_2{std::move(map_1)}; - for(const auto& pair : map_1_orig) + for (const auto& pair : map_1_orig) { EXPECT_TRUE(map_2.contains(pair.first)); } @@ -1612,13 +1667,13 @@ TEST(FixedUnorderedMap, ComplexNontrivialMoves) } map_1 = std::move(map_2); - for(const auto& pair : map_2_orig) + for (const auto& pair : map_2_orig) { EXPECT_TRUE(map_1.contains(pair.first)); } map_1.clear(); map_1 = std::move(map_3); - for(const auto& pair : map_3_orig) + for (const auto& pair : map_3_orig) { EXPECT_TRUE(map_1.contains(pair.first)); }