Skip to content

Commit

Permalink
Use TinyUnorderedMap for render tile sets (#1792)
Browse files Browse the repository at this point in the history
Co-authored-by: Bart Louwers <[email protected]>
  • Loading branch information
TimSylvester and louwers authored Nov 1, 2023
1 parent 5a17559 commit 9e7f1cc
Show file tree
Hide file tree
Showing 9 changed files with 685 additions and 26 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions bazel/core.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
199 changes: 199 additions & 0 deletions include/mbgl/util/tiny_unordered_map.hpp
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions src/mbgl/renderer/buckets/fill_bucket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion src/mbgl/renderer/buckets/fill_extrusion_bucket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
32 changes: 14 additions & 18 deletions src/mbgl/renderer/render_layer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 8 additions & 5 deletions src/mbgl/renderer/render_layer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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<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
Expand Down
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 9e7f1cc

Please sign in to comment.