From 9e7f1cc3a579e096fb2f750c14078c823b2fe545 Mon Sep 17 00:00:00 2001 From: Tim Sylvester Date: Wed, 1 Nov 2023 07:14:01 -0700 Subject: [PATCH] Use `TinyUnorderedMap` for render tile sets (#1792) Co-authored-by: Bart Louwers --- CMakeLists.txt | 1 + bazel/core.bzl | 1 + include/mbgl/util/tiny_unordered_map.hpp | 199 ++++++++ src/mbgl/renderer/buckets/fill_bucket.cpp | 5 +- .../buckets/fill_extrusion_bucket.cpp | 4 +- src/mbgl/renderer/render_layer.cpp | 32 +- src/mbgl/renderer/render_layer.hpp | 13 +- test/CMakeLists.txt | 1 + test/util/tiny_map.test.cpp | 455 ++++++++++++++++++ 9 files changed, 685 insertions(+), 26 deletions(-) create mode 100644 include/mbgl/util/tiny_unordered_map.hpp create mode 100644 test/util/tiny_map.test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 48592b16b6b..09d71e917e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -416,6 +416,7 @@ list(APPEND INCLUDE_FILES ${PROJECT_SOURCE_DIR}/include/mbgl/util/thread.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/util/tileset.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/util/timer.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/util/tiny_unordered_map.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/util/traits.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/util/type_list.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/util/unitbezier.hpp diff --git a/bazel/core.bzl b/bazel/core.bzl index ebc73f9b64e..d8bab43ec59 100644 --- a/bazel/core.bzl +++ b/bazel/core.bzl @@ -831,6 +831,7 @@ MLN_CORE_HEADERS = [ "include/mbgl/util/thread.hpp", "include/mbgl/util/tileset.hpp", "include/mbgl/util/timer.hpp", + "include/mbgl/util/tiny_unordered_map.hpp", "include/mbgl/util/traits.hpp", "include/mbgl/util/type_list.hpp", "include/mbgl/util/unitbezier.hpp", diff --git a/include/mbgl/util/tiny_unordered_map.hpp b/include/mbgl/util/tiny_unordered_map.hpp new file mode 100644 index 00000000000..3fa4b8bf42c --- /dev/null +++ b/include/mbgl/util/tiny_unordered_map.hpp @@ -0,0 +1,199 @@ +#pragma once + +#include +#include +#include +#include + +namespace mbgl { +namespace util { + +/// A wrapper around `unordered_map` which uses linear search below some threshold of size. +/// +/// Currently an immutable map, not allowing insert or delete, but could be extended to allow that, with extra cost when +/// transitioning over the threshold value, of course. The maped values can be modified via reference accessors, just +/// not the set of keys or the keys themselves. Since the small-case elements are stored separately from +/// `unordered_map`, we can't use that class' iterators, and its interface is private. A wrapper iterator might allow +/// for a better and more complete public interface. +/// @tparam Key The key type +/// @tparam T The mapped type +/// @tparam LinearThreshold The count of elements above which a dynamically allocated hashtable is used +/// @tparam Hash The hash implementation +/// @tparam KeyEqual The key equality implementation +/// @tparam Allocator The allocator used for dynamic key-value-pair allocations only +template , + typename KeyEqual = std::equal_to, + typename Allocator = std::allocator>> +struct TinyUnorderedMap : private std::unordered_map { + using Super = std::unordered_map; + +public: + using value_type = typename Super::value_type; + + TinyUnorderedMap() = default; + + /// Construct from a range of key-value pairs + template + TinyUnorderedMap(InputIterator first, InputIterator last) + : TinyUnorderedMap(first, last, [](auto& x) { return x; }) {} + + /// Construct from a generator + template + TinyUnorderedMap(InputIterator first, InputIterator last, GeneratorFunction gen) { + assign(first, last, gen); + } + + /// Construct from a range of keys and a range of values. + template + TinyUnorderedMap(KeyInputIterator firstKey, + KeyInputIterator lastKey, + ValueInputIterator firstValue, + [[maybe_unused]] ValueInputIterator lastValue) { + const auto n = std::distance(firstKey, lastKey); + assert(n == std::distance(firstValue, lastValue)); + if (n <= static_cast(LinearThreshold)) { + for (std::size_t i = 0; firstKey != lastKey; i++) { + keys[i].emplace(*firstKey++); + values[i].emplace(*firstValue++); + } + linearSize = n; + } else { + this->reserve(n); + while (firstKey != lastKey) { + this->Super::operator[](*firstKey++) = *firstValue++; + } + } + } + + /// Construct from initializer lists + template + TinyUnorderedMap(std::initializer_list keys_, std::initializer_list values_) + : TinyUnorderedMap(keys_.begin(), keys_.end(), values_.begin(), values_.end()) {} + + TinyUnorderedMap(std::initializer_list values_) + : TinyUnorderedMap(values_.begin(), values_.end()) {} + + /// Move constructor + TinyUnorderedMap(TinyUnorderedMap&& rhs) + : Super(std::move(rhs)), + linearSize(rhs.linearSize), + keys(std::move(rhs.keys)), + values(std::move(rhs.values)) { + rhs.linearSize = 0; + } + + /// Copy constructor + TinyUnorderedMap(const TinyUnorderedMap& rhs) + : Super(rhs), + linearSize(rhs.linearSize), + keys(rhs.keys), + values(rhs.values) {} + + /// Replace contents from generator + template + TinyUnorderedMap& assign(InputIterator first, InputIterator last, GeneratorFunction gen) { + const auto n = std::distance(first, last); + if (n <= static_cast(LinearThreshold)) { + this->Super::clear(); + for (std::size_t i = 0; first != last; i++) { + auto result = gen(*first++); + keys[i].emplace(std::move(result.first)); + values[i].emplace(std::move(result.second)); + } + linearSize = n; + } else { + linearSize = 0; + this->reserve(n); + while (first != last) { + this->Super::insert(gen(*first++)); + } + } + return *this; + } + + /// Copy assignment + TinyUnorderedMap& operator=(const TinyUnorderedMap& rhs) { + TinyUnorderedMap{rhs}.swap(*this); + return *this; + } + + /// Move assignment + TinyUnorderedMap& operator=(TinyUnorderedMap&& rhs) noexcept { + TinyUnorderedMap{std::move(rhs)}.swap(*this); + return *this; + } + + /// Swap all contents with another instance + TinyUnorderedMap& swap(TinyUnorderedMap& other) { + this->Super::swap(other); + std::swap(keys, other.keys); + std::swap(values, other.values); + std::swap(linearSize, other.linearSize); + return *this; + } + + std::size_t size() const noexcept { return linearSize ? linearSize : Super::size(); } + bool empty() const noexcept { return !size(); } + + std::optional> find(const Key& key) noexcept { + return TinyUnorderedMap::find(*this, key); + } + std::optional> find(const Key& key) const noexcept { + return TinyUnorderedMap::find(*this, key); + } + + std::optional> operator[](const Key& key) noexcept { + return TinyUnorderedMap::find(*this, key); + } + std::optional> operator[](const Key& key) const noexcept { + return TinyUnorderedMap::find(*this, key); + } + + std::size_t count(const Key& key) const noexcept { return find(key) ? 1 : 0; } + + void clear() { + linearSize = 0; + this->Super::clear(); + } + +private: + std::size_t linearSize = 0; + std::array, LinearThreshold> keys; + std::array, LinearThreshold> values; + + auto makeFindPredicate(const Key& key) const noexcept { + return [&, eq = this->Super::key_eq()](const auto& x) { + return eq(key, *x); + }; + } + + /// Search the small-map storage for a key. + /// @return A pair of `bool found` and `size_t index` + auto findLinear(const Key& key) const noexcept { + const auto beg = keys.begin(); + const auto end = beg + linearSize; + const auto hit = std::find_if(beg, end, makeFindPredicate(key)); + return std::make_pair(hit != end, hit != end ? std::distance(beg, hit) : 0); + } + + // templated to provide `iterator` and `const_iterator` return values without duplication + template + static std::optional> find(TThis& map, const Key& key) noexcept { + if (map.linearSize) { + if (const auto result = map.findLinear(key); result.first) { + // Return a reference to the value at the same index + assert(map.values[result.second]); + return *map.values[result.second]; + } + } else if (const auto hit = map.Super::find(key); hit != map.end()) { + return hit->second; + } + return std::nullopt; + } +}; + +} // namespace util +} // namespace mbgl diff --git a/src/mbgl/renderer/buckets/fill_bucket.cpp b/src/mbgl/renderer/buckets/fill_bucket.cpp index c286510aba0..87e2d8a2735 100644 --- a/src/mbgl/renderer/buckets/fill_bucket.cpp +++ b/src/mbgl/renderer/buckets/fill_bucket.cpp @@ -113,8 +113,9 @@ void FillBucket::addFeature(const GeometryTileFeature& feature, const auto triangleIndex = static_cast(triangleSegment.vertexLength); for (std::size_t i = 0; i < nIndicies; i += 3) { - triangles.emplace_back( - triangleIndex + indices[i], triangleIndex + indices[i + 1], triangleIndex + indices[i + 2]); + triangles.emplace_back(static_cast(triangleIndex + indices[i]), + static_cast(triangleIndex + indices[i + 1]), + static_cast(triangleIndex + indices[i + 2])); } triangleSegment.vertexLength += totalVertices; diff --git a/src/mbgl/renderer/buckets/fill_extrusion_bucket.cpp b/src/mbgl/renderer/buckets/fill_extrusion_bucket.cpp index 78224caaa6d..e45ba52a54a 100644 --- a/src/mbgl/renderer/buckets/fill_extrusion_bucket.cpp +++ b/src/mbgl/renderer/buckets/fill_extrusion_bucket.cpp @@ -151,7 +151,9 @@ void FillExtrusionBucket::addFeature(const GeometryTileFeature& feature, for (std::size_t i = 0; i < nIndices; i += 3) { // Counter-Clockwise winding order. - triangles.emplace_back(flatIndices[indices[i]], flatIndices[indices[i + 2]], flatIndices[indices[i + 1]]); + triangles.emplace_back(static_cast(flatIndices[indices[i]]), + static_cast(flatIndices[indices[i + 2]]), + static_cast(flatIndices[indices[i + 1]])); } triangleSegment.vertexLength += totalVertices; diff --git a/src/mbgl/renderer/render_layer.cpp b/src/mbgl/renderer/render_layer.cpp index bdd34185e78..40d5070e59f 100644 --- a/src/mbgl/renderer/render_layer.cpp +++ b/src/mbgl/renderer/render_layer.cpp @@ -186,34 +186,30 @@ std::size_t RenderLayer::removeAllDrawables() { } void RenderLayer::updateRenderTileIDs() { - const auto oldMap = std::move(renderTileIDs); - renderTileIDs = std::unordered_map{}; - if (renderTiles) { - renderTileIDs.reserve(renderTiles->size()); - for (const auto& tile : *renderTiles) { - // If the tile existed previously, retain the mapped value - const auto tileID = tile.get().getOverscaledTileID(); - const auto hit = oldMap.find(tileID); - const auto bucketID = (hit != oldMap.end()) ? hit->second : util::SimpleIdentity::Empty; - [[maybe_unused]] const auto result = renderTileIDs.insert(std::make_pair(tileID, bucketID)); - assert(result.second && "Unexpected duplicate TileID in renderTiles"); - } + if (!renderTiles || renderTiles->empty()) { + renderTileIDs.clear(); + return; } + + newRenderTileIDs.assign(renderTiles->begin(), renderTiles->end(), [&](const auto& tile) { + const auto& tileID = tile.get().getOverscaledTileID(); + return std::make_pair(tileID, getRenderTileBucketID(tileID)); + }); + renderTileIDs.swap(newRenderTileIDs); } bool RenderLayer::hasRenderTile(const OverscaledTileID& tileID) const { - return renderTileIDs.find(tileID) != renderTileIDs.end(); + return renderTileIDs.find(tileID).has_value(); } util::SimpleIdentity RenderLayer::getRenderTileBucketID(const OverscaledTileID& tileID) const { - const auto hit = renderTileIDs.find(tileID); - return (hit != renderTileIDs.end()) ? hit->second : util::SimpleIdentity::Empty; + const auto result = renderTileIDs.find(tileID); + return result.has_value() ? result->get() : util::SimpleIdentity::Empty; } bool RenderLayer::setRenderTileBucketID(const OverscaledTileID& tileID, util::SimpleIdentity bucketID) { - const auto hit = renderTileIDs.find(tileID); - if (hit != renderTileIDs.end() && hit->second != bucketID) { - hit->second = bucketID; + if (auto result = renderTileIDs.find(tileID); result && result->get() != bucketID) { + result->get() = bucketID; return true; } return false; diff --git a/src/mbgl/renderer/render_layer.hpp b/src/mbgl/renderer/render_layer.hpp index 6ad7b0b0245..ed4a509b9d3 100644 --- a/src/mbgl/renderer/render_layer.hpp +++ b/src/mbgl/renderer/render_layer.hpp @@ -9,8 +9,7 @@ #if MLN_DRAWABLE_RENDERER #include #include - -#include +#include #endif // MLN_DRAWABLE_RENDERER #include @@ -261,9 +260,13 @@ class RenderLayer { // An optional tweaker that will update drawables LayerTweakerPtr layerTweaker; - // The set of Tile IDs in `renderTiles`, along with the - // identity of the bucket from which they were built. - std::unordered_map renderTileIDs; + // A sorted set of tile IDs in `renderTiles`, along with + // the identity of the bucket from which they were built. + // We swap between two instances to minimize reallocations. + static constexpr auto LinearTileIDs = 12; // From benchmarking, see #1805 + using RenderTileIDMap = util::TinyUnorderedMap; + RenderTileIDMap renderTileIDs; + RenderTileIDMap newRenderTileIDs; #endif // Current layer index as specified by the layerIndexChanged event diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bd46c6209ef..e53d3929135 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -107,6 +107,7 @@ add_library( ${PROJECT_SOURCE_DIR}/test/util/tile_cover.test.cpp ${PROJECT_SOURCE_DIR}/test/util/tile_range.test.cpp ${PROJECT_SOURCE_DIR}/test/util/timer.test.cpp + ${PROJECT_SOURCE_DIR}/test/util/tiny_map.test.cpp ${PROJECT_SOURCE_DIR}/test/util/token.test.cpp ${PROJECT_SOURCE_DIR}/test/util/url.test.cpp ${PROJECT_SOURCE_DIR}/test/util/tile_server_options.test.cpp diff --git a/test/util/tiny_map.test.cpp b/test/util/tiny_map.test.cpp new file mode 100644 index 00000000000..0fa38e4ecc4 --- /dev/null +++ b/test/util/tiny_map.test.cpp @@ -0,0 +1,455 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace mbgl; +using namespace mbgl::util; + +template +void testSetDiff(TMapIter beg, TMapIter end, TMap2& map2) { + while (beg != end) { + auto item = *beg++; + auto i = map2.find(item.first); + EXPECT_TRUE(i); + EXPECT_EQ(*i, item.second); + } +} +// TODO: need key-value iteration +// template +// void testSetDiff(TMap1 map1, TMap2& map2) { +// testSetDiff(map1.begin(), map1.end(), map2); +// testSetDiff(map2.begin(), map2.end(), map1); +//} + +template +void testInit() { + testing::ScopedTrace trace(__FILE__, __LINE__, Threshold); + + // Construct from separate initializer lists + const auto map = TinyUnorderedMap{{1, 2, 3}, {3, 2, 1}}; + EXPECT_EQ(3, map.size()); + + // Construct from list of pairs + const auto map2 = TinyUnorderedMap{{{1, 3}, {2, 2}, {3, 1}}}; + EXPECT_EQ(map.size(), map2.size()); + // testSetDiff(map, map2); + EXPECT_TRUE(map[1] && map2[1]); + EXPECT_TRUE(map[2] && map2[2]); + EXPECT_TRUE(map[3] && map2[3]); + + // Construct in a different order + const auto map3 = TinyUnorderedMap{{2, 3, 1}, {2, 1, 3}}; + EXPECT_EQ(map.size(), map3.size()); + // testSetDiff(map, map3); + EXPECT_TRUE(map3[1]); + EXPECT_TRUE(map3[2]); + EXPECT_TRUE(map3[3]); +} +TEST(TinyMap, Init) { + testInit<0>(); + testInit<2>(); + testInit<5>(); + testInit<10>(); +} + +template +void testCopy() { + testing::ScopedTrace trace(__FILE__, __LINE__, Threshold); + + const auto map = TinyUnorderedMap{{1, 2, 3}, {3, 2, 1}}; + decltype(map) map2(map); + + std::remove_const_t map3; + map3 = map2; + + // testSetDiff(map, map2); + // testSetDiff(map, map3); + EXPECT_EQ(map.size(), map2.size()); + EXPECT_EQ(map.size(), map3.size()); + EXPECT_TRUE(map[1] && map2[1] && map3[1]); + EXPECT_TRUE(map[2] && map2[2] && map3[2]); + EXPECT_TRUE(map[3] && map2[3] && map3[3]); +} +TEST(TinyMap, Copy) { + testCopy<0>(); + testCopy<2>(); + testCopy<5>(); + testCopy<10>(); +} + +template +void testGenInit() { + testing::ScopedTrace trace(__FILE__, __LINE__, Threshold); + + const auto input = std::vector{1, 2, 3}; + const auto map = TinyUnorderedMap{input.begin(), input.end(), [](const auto& x) { + return std::make_pair(x, 4 - x); + }}; + + EXPECT_EQ(3, map.size()); + EXPECT_EQ(3, map[1]); + EXPECT_EQ(2, map[2]); + EXPECT_EQ(1, map[3]); +} +TEST(TinyMap, GenInit) { + testGenInit<0>(); + testGenInit<2>(); + testGenInit<5>(); + testGenInit<10>(); +} + +template +void testMove() { + testing::ScopedTrace trace(__FILE__, __LINE__, Threshold); + + const auto map = TinyUnorderedMap{{1, 2, 3}, {3, 2, 1}}; + std::remove_const_t map2(map); + + // move constructor + decltype(map2) map3(std::move(map2)); + EXPECT_TRUE(map2.empty()); + // testSetDiff(map, map3); + EXPECT_TRUE(map3[1]); + EXPECT_TRUE(map3[2]); + EXPECT_TRUE(map3[3]); + + // move assignment + decltype(map2) map4; + map4 = std::move(map3); + EXPECT_TRUE(map3.empty()); + // testSetDiff(map, map4); + EXPECT_TRUE(map4[1]); + EXPECT_TRUE(map4[2]); + EXPECT_TRUE(map4[3]); + + map4.clear(); + EXPECT_TRUE(map4.empty()); +} +TEST(TinyMap, Move) { + testMove<0>(); + testMove<2>(); + testMove<5>(); + testMove<10>(); +} + +template +void testConstLookup() { + testing::ScopedTrace trace(__FILE__, __LINE__, Threshold); + + const auto map = TinyUnorderedMap{{2, 4, 6}, {3, 2, 1}}; + + EXPECT_EQ(3, *map.find(2)); + EXPECT_EQ(2, *map.find(4)); + EXPECT_EQ(1, *map.find(6)); +} +TEST(TinyMap, ConstLookup) { + testConstLookup<0>(); + testConstLookup<2>(); + testConstLookup<5>(); + testConstLookup<10>(); +} + +template +void testMutableLookup() { + testing::ScopedTrace trace(__FILE__, __LINE__, Threshold); + + auto map = TinyUnorderedMap{{2, 4, 6}, {3, 2, 1}}; + + EXPECT_EQ(3, *map.find(2)); + EXPECT_EQ(2, *map.find(4)); + EXPECT_EQ(1, *map.find(6)); + + // operator[] doesn't insert... for now + EXPECT_FALSE(map[7]); + EXPECT_FALSE(map[5]); + EXPECT_FALSE(map[0]); + EXPECT_FALSE(map[3]); + EXPECT_EQ(3, map.size()); + + EXPECT_EQ(3, map[2]); + EXPECT_EQ(2, map[4]); + EXPECT_EQ(1, map[6]); +} +TEST(TinyMap, MutableLookup) { + testMutableLookup<0>(); + testMutableLookup<2>(); + testMutableLookup<5>(); + testMutableLookup<10>(); +} + +template +void testTileIDKey() { + testing::ScopedTrace trace(__FILE__, __LINE__, Threshold); + + const std::vector keys = { + {1, 0, {1, 0, 0}}, + {1, 0, {1, 1, 0}}, + {1, 0, {1, 0, 1}}, + {1, 0, {1, 1, 1}}, + + {2, 0, {2, 0, 0}}, + {2, 0, {2, 1, 0}}, + {2, 0, {2, 0, 1}}, + {2, 0, {2, 1, 1}}, + {2, 0, {2, 2, 1}}, + {2, 0, {2, 3, 1}}, + {2, 0, {2, 2, 2}}, + {2, 0, {2, 3, 2}}, + + {0, 0, {0, 0, 0}}, + }; + const std::vector values(keys.size(), 0); + const auto map = TinyUnorderedMap{ + keys.begin(), keys.end(), values.begin(), values.end()}; + + for (const auto& k : keys) { + EXPECT_TRUE(map.find(k)); + EXPECT_FALSE(map.find({3, k.wrap, k.canonical})); + EXPECT_FALSE(map.find({k.overscaledZ, 1, k.canonical})); + } +} +TEST(TinyMap, TileIDKey) { + testTileIDKey<0>(); + testTileIDKey<2>(); + testTileIDKey<5>(); + testTileIDKey<10>(); +} + +template +void testCustomComp() { + testing::ScopedTrace trace(__FILE__, __LINE__, Threshold); + + const auto map = TinyUnorderedMap{{1, 2, 3}, {3, 2, 1}}; + const auto map2 = TinyUnorderedMap, std::equal_to<>>{{2, 3, 1}, {2, 1, 3}}; + // testSetDiff(map, map2); + EXPECT_EQ(map.size(), map2.size()); + EXPECT_TRUE(map2[1]); + EXPECT_TRUE(map2[2]); + EXPECT_TRUE(map2[3]); +} +TEST(TinyMap, CustomComp) { + testCustomComp<0>(); + testCustomComp<2>(); + testCustomComp<5>(); + testCustomComp<10>(); +} + +template +using TIter = typename std::vector::const_iterator; +template +using TMakeMap = std::function, TIter, TIter, TIter)>; +using ssize_t = std::ptrdiff_t; + +void incTileID(OverscaledTileID& tid) { + tid.canonical.y++; + if (tid.canonical.y == 1U << tid.canonical.z) { + tid.canonical.y = 0; + tid.canonical.x++; + } + if (tid.canonical.x == 1U << tid.canonical.z) { + tid.canonical.x = 0; + tid.canonical.z++; + } +} +// return a function that generate successive tile IDs +std::function makeGenTileID() { + auto tid = std::make_shared(0, 0, 0, 0, 0); + return [=] { + auto v = *tid; + incTileID(*tid); + return v; + }; +} + +std::function makeGenIntID() { + static std::size_t n = 0; + return [=] { + return ++n; + }; +} + +std::function makeGenStringID() { + static int n = 0; + return [=] { + return "someprefix_" + std::to_string(++n); + }; +} + +static volatile int do_not_optimize_away = 0; + +template +void benchmark(const std::string_view label, + const std::size_t max, // largest number of elements to consider + const std::size_t lookups, // number of searches for each item (~ read/write ratio) + const std::size_t reports, // number of items reported from `max` (should divide evenly) + const TMakeMap make, + const std::function generate, + const std::size_t seed = 0xf00dLL) { + if ((max / reports) * reports != max) { + assert(false); + return; + } + + std::seed_seq seed_seq{seed}; + std::default_random_engine engine(seed_seq); + + // build keys + std::vector keys; + keys.reserve(max); + std::generate_n(std::back_inserter(keys), max, std::move(generate)); + + // build values + std::vector values(keys.size()); + std::iota(values.begin(), values.end(), 0); + + using SteadyClock = std::chrono::steady_clock; + using Usec = std::chrono::microseconds; + std::vector times(reports); + + constexpr std::size_t timingIterations = 50; + + // test each size from 1 to max + for (std::size_t i = 1; i <= max; ++i) { + // Build and lookup keys in different random orders + std::vector buildKeys(keys.begin(), keys.begin() + i); + std::vector testKeys(keys.begin(), keys.begin() + i); + std::shuffle(buildKeys.begin(), buildKeys.end(), engine); + std::shuffle(testKeys.begin(), testKeys.end(), engine); + + // (build a map and look up the keys many times) many times + const auto startTime = SteadyClock::now(); + for (std::size_t tt = 0; tt < timingIterations; ++tt) { + const auto map = make(buildKeys.begin(), buildKeys.end(), values.begin(), values.begin() + i); + for (std::size_t j = 0; j < lookups; ++j) { + for (const auto& k : testKeys) { + if (map.count(k)) { + do_not_optimize_away++; + } + } + } + } + + if ((i % (max / reports)) == 0) { + const auto totalElapsed = std::chrono::duration_cast(SteadyClock::now() - startTime); + const auto elapsed = static_cast(totalElapsed.count()) / timingIterations; + times[i / (max / reports) - 1] = elapsed; + } + } + + std::stringstream ss; + ss << std::setw(10) << std::left << label; + ss << " threshold=" << std::setw(3) << std::setfill('0') << std::right << Threshold << std::setfill(' '); + ss << " (x" << std::setw(2) << (max / reports) << "): "; + for (std::size_t i = 0; i < reports; ++i) { + ss << std::setw(6) << std::round(times[i] * 10); + } + Log::Info(Event::Timing, ss.str()); +} + +constexpr std::size_t lookups = 100; + +TEST(TinyMap, TEST_REQUIRES_ACCURATE_TIMING(BenchmarkRef)) { +#if defined(DEBUG) + Log::Info(Event::General, "Build Type: Debug"); +#elif defined(NDEBUG) + Log::Info(Event::General, "Build Type: Release"); +#else + Log::Info(Event::General, "Build Type: ?"); +#endif + + // std::unordered_map with size_t keys + benchmark>( + "int:stl", + 20, + lookups, + 10, + [&](auto kb, auto ke, auto vb, auto) { + std::unordered_map m; + m.reserve(std::distance(kb, ke)); + while (kb != ke) m.insert(std::make_pair(*kb++, *vb++)); + return m; + }, + makeGenIntID()); + + // std::unordered_map with string keys + benchmark>( + "str:stl", + 10, + lookups, + 10, + [&](auto kb, auto ke, auto vb, auto) { + std::unordered_map m; + m.reserve(std::distance(kb, ke)); + while (kb != ke) m.insert(std::make_pair(*kb++, *vb++)); + return m; + }, + makeGenStringID()); + + benchmark>( + "tile:stl", + 20, + lookups, + 10, + [&](auto kb, auto ke, auto vb, auto) { + std::unordered_map m; + m.reserve(std::distance(kb, ke)); + while (kb != ke) m.insert(std::make_pair(*kb++, *vb++)); + return m; + }, + makeGenTileID()); +} + +template +void testBenchmarkTinyMap() { + testing::ScopedTrace trace(__FILE__, __LINE__, Threshold); + + // TinyMap with int keys + benchmark, Threshold>( + "int:tiny", + 20, + lookups, + 10, + [&](auto kb, auto ke, auto vb, auto ve) { + return TinyUnorderedMap{kb, ke, vb, ve}; + }, + makeGenIntID()); + + // TinyMap with string keys + benchmark, Threshold>( + "str:tiny", + 10, + lookups, + 10, + [&](auto kb, auto ke, auto vb, auto ve) { + return TinyUnorderedMap{kb, ke, vb, ve}; + }, + makeGenStringID()); + + benchmark, Threshold>( + "tile:tiny", + 20, + lookups, + 10, + [&](auto kb, auto ke, auto vb, auto ve) { + return TinyUnorderedMap{kb, ke, vb, ve}; + }, + makeGenTileID()); +} +TEST(TinyMap, TEST_REQUIRES_ACCURATE_TIMING(BenchmarkTinyMap)) { + testBenchmarkTinyMap<4>(); + testBenchmarkTinyMap<8>(); + testBenchmarkTinyMap<16>(); +}