Use TinyUnorderedMap for render tile sets (#1792)
Co-authored-by: Bart Louwers <bart@emeel.net>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
199
include/mbgl/util/tiny_unordered_map.hpp
Normal file
199
include/mbgl/util/tiny_unordered_map.hpp
Normal file
@@ -0,0 +1,199 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
|
||||
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 Key,
|
||||
typename T,
|
||||
std::size_t LinearThreshold,
|
||||
typename Hash = std::hash<Key>,
|
||||
typename KeyEqual = std::equal_to<Key>,
|
||||
typename Allocator = std::allocator<std::pair<const Key, T>>>
|
||||
struct TinyUnorderedMap : private std::unordered_map<Key, T, Hash, KeyEqual, Allocator> {
|
||||
using Super = std::unordered_map<Key, T, Hash, KeyEqual, Allocator>;
|
||||
|
||||
public:
|
||||
using value_type = typename Super::value_type;
|
||||
|
||||
TinyUnorderedMap() = default;
|
||||
|
||||
/// Construct from a range of key-value pairs
|
||||
template <typename InputIterator>
|
||||
TinyUnorderedMap(InputIterator first, InputIterator last)
|
||||
: TinyUnorderedMap(first, last, [](auto& x) { return x; }) {}
|
||||
|
||||
/// Construct from a generator
|
||||
template <typename InputIterator, typename GeneratorFunction>
|
||||
TinyUnorderedMap(InputIterator first, InputIterator last, GeneratorFunction gen) {
|
||||
assign(first, last, gen);
|
||||
}
|
||||
|
||||
/// Construct from a range of keys and a range of values.
|
||||
template <typename KeyInputIterator, typename ValueInputIterator>
|
||||
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<decltype(n)>(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 <typename KeyInput, typename ValueInput>
|
||||
TinyUnorderedMap(std::initializer_list<KeyInput> keys_, std::initializer_list<ValueInput> values_)
|
||||
: TinyUnorderedMap(keys_.begin(), keys_.end(), values_.begin(), values_.end()) {}
|
||||
|
||||
TinyUnorderedMap(std::initializer_list<value_type> 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 <typename InputIterator, typename GeneratorFunction>
|
||||
TinyUnorderedMap& assign(InputIterator first, InputIterator last, GeneratorFunction gen) {
|
||||
const auto n = std::distance(first, last);
|
||||
if (n <= static_cast<decltype(n)>(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<std::reference_wrapper<T>> find(const Key& key) noexcept {
|
||||
return TinyUnorderedMap::find<TinyUnorderedMap, T>(*this, key);
|
||||
}
|
||||
std::optional<std::reference_wrapper<const T>> find(const Key& key) const noexcept {
|
||||
return TinyUnorderedMap::find<const TinyUnorderedMap, const T>(*this, key);
|
||||
}
|
||||
|
||||
std::optional<std::reference_wrapper<T>> operator[](const Key& key) noexcept {
|
||||
return TinyUnorderedMap::find<TinyUnorderedMap, T>(*this, key);
|
||||
}
|
||||
std::optional<std::reference_wrapper<const T>> operator[](const Key& key) const noexcept {
|
||||
return TinyUnorderedMap::find<const TinyUnorderedMap, const T>(*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<std::optional<Key>, LinearThreshold> keys;
|
||||
std::array<std::optional<T>, 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 <typename TThis, typename TRet>
|
||||
static std::optional<std::reference_wrapper<TRet>> 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
|
||||
@@ -113,8 +113,9 @@ void FillBucket::addFeature(const GeometryTileFeature& feature,
|
||||
const auto triangleIndex = static_cast<uint16_t>(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<uint16_t>(triangleIndex + indices[i]),
|
||||
static_cast<uint16_t>(triangleIndex + indices[i + 1]),
|
||||
static_cast<uint16_t>(triangleIndex + indices[i + 2]));
|
||||
}
|
||||
|
||||
triangleSegment.vertexLength += totalVertices;
|
||||
|
||||
@@ -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<uint16_t>(flatIndices[indices[i]]),
|
||||
static_cast<uint16_t>(flatIndices[indices[i + 2]]),
|
||||
static_cast<uint16_t>(flatIndices[indices[i + 1]]));
|
||||
}
|
||||
|
||||
triangleSegment.vertexLength += totalVertices;
|
||||
|
||||
@@ -186,34 +186,30 @@ std::size_t RenderLayer::removeAllDrawables() {
|
||||
}
|
||||
|
||||
void RenderLayer::updateRenderTileIDs() {
|
||||
const auto oldMap = std::move(renderTileIDs);
|
||||
renderTileIDs = std::unordered_map<OverscaledTileID, util::SimpleIdentity>{};
|
||||
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;
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
#if MLN_DRAWABLE_RENDERER
|
||||
#include <mbgl/gfx/drawable.hpp>
|
||||
#include <mbgl/renderer/change_request.hpp>
|
||||
|
||||
#include <unordered_map>
|
||||
#include <mbgl/util/tiny_unordered_map.hpp>
|
||||
#endif // MLN_DRAWABLE_RENDERER
|
||||
|
||||
#include <list>
|
||||
@@ -261,9 +260,13 @@ protected:
|
||||
// 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<OverscaledTileID, util::SimpleIdentity> 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<OverscaledTileID, util::SimpleIdentity, LinearTileIDs>;
|
||||
RenderTileIDMap renderTileIDs;
|
||||
RenderTileIDMap newRenderTileIDs;
|
||||
#endif
|
||||
|
||||
// Current layer index as specified by the layerIndexChanged event
|
||||
|
||||
@@ -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
|
||||
|
||||
455
test/util/tiny_map.test.cpp
Normal file
455
test/util/tiny_map.test.cpp
Normal file
@@ -0,0 +1,455 @@
|
||||
#include <mbgl/test/util.hpp>
|
||||
#include <mbgl/tile/tile_id.hpp>
|
||||
#include <mbgl/util/chrono.hpp>
|
||||
#include <mbgl/util/logging.hpp>
|
||||
#include <mbgl/util/string.hpp>
|
||||
#include <mbgl/util/tiny_unordered_map.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <numeric>
|
||||
#include <random>
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace mbgl;
|
||||
using namespace mbgl::util;
|
||||
|
||||
template <typename TMapIter, typename TMap2>
|
||||
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 <typename TMap1, typename TMap2>
|
||||
// void testSetDiff(TMap1 map1, TMap2& map2) {
|
||||
// testSetDiff(map1.begin(), map1.end(), map2);
|
||||
// testSetDiff(map2.begin(), map2.end(), map1);
|
||||
//}
|
||||
|
||||
template <std::size_t Threshold>
|
||||
void testInit() {
|
||||
testing::ScopedTrace trace(__FILE__, __LINE__, Threshold);
|
||||
|
||||
// Construct from separate initializer lists
|
||||
const auto map = TinyUnorderedMap<int, int, Threshold>{{1, 2, 3}, {3, 2, 1}};
|
||||
EXPECT_EQ(3, map.size());
|
||||
|
||||
// Construct from list of pairs
|
||||
const auto map2 = TinyUnorderedMap<int, int, Threshold>{{{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<int, int, Threshold>{{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 <std::size_t Threshold>
|
||||
void testCopy() {
|
||||
testing::ScopedTrace trace(__FILE__, __LINE__, Threshold);
|
||||
|
||||
const auto map = TinyUnorderedMap<int, int, Threshold>{{1, 2, 3}, {3, 2, 1}};
|
||||
decltype(map) map2(map);
|
||||
|
||||
std::remove_const_t<decltype(map)> 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 <std::size_t Threshold>
|
||||
void testGenInit() {
|
||||
testing::ScopedTrace trace(__FILE__, __LINE__, Threshold);
|
||||
|
||||
const auto input = std::vector<int>{1, 2, 3};
|
||||
const auto map = TinyUnorderedMap<int, int, Threshold>{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 <std::size_t Threshold>
|
||||
void testMove() {
|
||||
testing::ScopedTrace trace(__FILE__, __LINE__, Threshold);
|
||||
|
||||
const auto map = TinyUnorderedMap<int, int, Threshold>{{1, 2, 3}, {3, 2, 1}};
|
||||
std::remove_const_t<decltype(map)> 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 <std::size_t Threshold>
|
||||
void testConstLookup() {
|
||||
testing::ScopedTrace trace(__FILE__, __LINE__, Threshold);
|
||||
|
||||
const auto map = TinyUnorderedMap<int, int, Threshold>{{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 <std::size_t Threshold>
|
||||
void testMutableLookup() {
|
||||
testing::ScopedTrace trace(__FILE__, __LINE__, Threshold);
|
||||
|
||||
auto map = TinyUnorderedMap<int, int, Threshold>{{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 <std::size_t Threshold>
|
||||
void testTileIDKey() {
|
||||
testing::ScopedTrace trace(__FILE__, __LINE__, Threshold);
|
||||
|
||||
const std::vector<OverscaledTileID> 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<std::size_t> values(keys.size(), 0);
|
||||
const auto map = TinyUnorderedMap<OverscaledTileID, std::size_t, Threshold>{
|
||||
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 <std::size_t Threshold>
|
||||
void testCustomComp() {
|
||||
testing::ScopedTrace trace(__FILE__, __LINE__, Threshold);
|
||||
|
||||
const auto map = TinyUnorderedMap<int, int, Threshold>{{1, 2, 3}, {3, 2, 1}};
|
||||
const auto map2 = TinyUnorderedMap<int, int, Threshold, std::hash<int>, 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 <typename T>
|
||||
using TIter = typename std::vector<T>::const_iterator;
|
||||
template <typename TMap, typename TKey>
|
||||
using TMakeMap = std::function<TMap(TIter<TKey>, TIter<TKey>, TIter<std::size_t>, TIter<std::size_t>)>;
|
||||
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<OverscaledTileID()> makeGenTileID() {
|
||||
auto tid = std::make_shared<OverscaledTileID>(0, 0, 0, 0, 0);
|
||||
return [=] {
|
||||
auto v = *tid;
|
||||
incTileID(*tid);
|
||||
return v;
|
||||
};
|
||||
}
|
||||
|
||||
std::function<std::size_t()> makeGenIntID() {
|
||||
static std::size_t n = 0;
|
||||
return [=] {
|
||||
return ++n;
|
||||
};
|
||||
}
|
||||
|
||||
std::function<std::string()> makeGenStringID() {
|
||||
static int n = 0;
|
||||
return [=] {
|
||||
return "someprefix_" + std::to_string(++n);
|
||||
};
|
||||
}
|
||||
|
||||
static volatile int do_not_optimize_away = 0;
|
||||
|
||||
template <typename TKey, // key type
|
||||
typename TMap, // map type
|
||||
std::size_t Threshold = 0>
|
||||
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<TMap, TKey> make,
|
||||
const std::function<TKey()> 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<TKey> keys;
|
||||
keys.reserve(max);
|
||||
std::generate_n(std::back_inserter(keys), max, std::move(generate));
|
||||
|
||||
// build values
|
||||
std::vector<size_t> values(keys.size());
|
||||
std::iota(values.begin(), values.end(), 0);
|
||||
|
||||
using SteadyClock = std::chrono::steady_clock;
|
||||
using Usec = std::chrono::microseconds;
|
||||
std::vector<double> 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<TKey> buildKeys(keys.begin(), keys.begin() + i);
|
||||
std::vector<TKey> 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<Usec>(SteadyClock::now() - startTime);
|
||||
const auto elapsed = static_cast<double>(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<std::size_t, std::unordered_map<std::size_t, size_t>>(
|
||||
"int:stl",
|
||||
20,
|
||||
lookups,
|
||||
10,
|
||||
[&](auto kb, auto ke, auto vb, auto) {
|
||||
std::unordered_map<std::size_t, size_t> 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<std::string, std::unordered_map<std::string, size_t>>(
|
||||
"str:stl",
|
||||
10,
|
||||
lookups,
|
||||
10,
|
||||
[&](auto kb, auto ke, auto vb, auto) {
|
||||
std::unordered_map<std::string, size_t> m;
|
||||
m.reserve(std::distance(kb, ke));
|
||||
while (kb != ke) m.insert(std::make_pair(*kb++, *vb++));
|
||||
return m;
|
||||
},
|
||||
makeGenStringID());
|
||||
|
||||
benchmark<OverscaledTileID, std::unordered_map<OverscaledTileID, size_t>>(
|
||||
"tile:stl",
|
||||
20,
|
||||
lookups,
|
||||
10,
|
||||
[&](auto kb, auto ke, auto vb, auto) {
|
||||
std::unordered_map<OverscaledTileID, size_t> m;
|
||||
m.reserve(std::distance(kb, ke));
|
||||
while (kb != ke) m.insert(std::make_pair(*kb++, *vb++));
|
||||
return m;
|
||||
},
|
||||
makeGenTileID());
|
||||
}
|
||||
|
||||
template <std::size_t Threshold>
|
||||
void testBenchmarkTinyMap() {
|
||||
testing::ScopedTrace trace(__FILE__, __LINE__, Threshold);
|
||||
|
||||
// TinyMap with int keys
|
||||
benchmark<std::size_t, TinyUnorderedMap<std::size_t, size_t, Threshold>, Threshold>(
|
||||
"int:tiny",
|
||||
20,
|
||||
lookups,
|
||||
10,
|
||||
[&](auto kb, auto ke, auto vb, auto ve) {
|
||||
return TinyUnorderedMap<std::size_t, size_t, Threshold>{kb, ke, vb, ve};
|
||||
},
|
||||
makeGenIntID());
|
||||
|
||||
// TinyMap with string keys
|
||||
benchmark<std::string, TinyUnorderedMap<std::string, size_t, Threshold>, Threshold>(
|
||||
"str:tiny",
|
||||
10,
|
||||
lookups,
|
||||
10,
|
||||
[&](auto kb, auto ke, auto vb, auto ve) {
|
||||
return TinyUnorderedMap<std::string, size_t, Threshold>{kb, ke, vb, ve};
|
||||
},
|
||||
makeGenStringID());
|
||||
|
||||
benchmark<OverscaledTileID, TinyUnorderedMap<OverscaledTileID, size_t, Threshold>, Threshold>(
|
||||
"tile:tiny",
|
||||
20,
|
||||
lookups,
|
||||
10,
|
||||
[&](auto kb, auto ke, auto vb, auto ve) {
|
||||
return TinyUnorderedMap<OverscaledTileID, size_t, Threshold>{kb, ke, vb, ve};
|
||||
},
|
||||
makeGenTileID());
|
||||
}
|
||||
TEST(TinyMap, TEST_REQUIRES_ACCURATE_TIMING(BenchmarkTinyMap)) {
|
||||
testBenchmarkTinyMap<4>();
|
||||
testBenchmarkTinyMap<8>();
|
||||
testBenchmarkTinyMap<16>();
|
||||
}
|
||||
Reference in New Issue
Block a user