sorting no longer requires allocations and is much faster

This commit is contained in:
Michele Caini
2019-09-16 14:40:06 +02:00
parent 14bc73cde9
commit 800751cbe0
6 changed files with 211 additions and 93 deletions

View File

@@ -183,47 +183,36 @@ amazing set of features. And even more, of course.
## Performance
As it stands right now, `EnTT` is just fast enough for my requirements when
compared to my first choice (it was already amazingly fast actually).<br/>
Below is a comparison between the two (both of them compiled with GCC 7.3.0 on a
Dell XPS 13 from mid 2014):
compared to my first choice (it was already amazingly fast actually).
| Benchmark | EntityX (compile-time) | EnTT |
|-----------|-------------|-------------|
| Create 1M entities | 0.0147s | **0.0046s** |
| Destroy 1M entities | 0.0053s | **0.0045s** |
| 1M entities, one component | 0.0012s | **1.9e-07s** |
| 1M entities, two components | 0.0012s | **3.8e-07s** |
| 1M entities, two components<br/>Half of the entities have all the components | 0.0009s | **3.8e-07s** |
| 1M entities, two components<br/>One of the entities has all the components | 0.0008s | **1.0e-06s** |
| 1M entities, five components | 0.0010s | **7.0e-07s** |
| 1M entities, ten components | 0.0011s | **1.2e-06s** |
| 1M entities, ten components<br/>Half of the entities have all the components | 0.0010s | **1.2e-06s** |
| 1M entities, ten components<br/>One of the entities has all the components | 0.0008s | **1.2e-06s** |
| Sort 150k entities, one component<br/>Arrays are in reverse order | - | **0.0036s** |
| Sort 150k entities, enforce permutation<br/>Arrays are in reverse order | - | **0.0005s** |
| Sort 150k entities, one component<br/>Arrays are almost sorted, std::sort | - | **0.0035s** |
| Sort 150k entities, one component<br/>Arrays are almost sorted, insertion sort | - | **0.0007s** |
For a long time, this file contained also some benchmarks to show how fast
`EnTT` was. However, I got tired of updating them whenever there is an
improvement. Furthermore, there are a lot of projects out there that use `EnTT`
as a basis for comparison (this should already tell you a lot) and offer their
own more or less ad hoc results to show how they perform well downhill and with
the wind at their back.<br/>
Many of these benchmarks are completely wrong and cannot be used to evaluate any
of the existing libraries, many others are simply incomplete, good at omitting
some information and using the wrong function to compare a given feature.
Certainly there are also good ones but they age quickly if nobody updates them,
especially when the library they are dealing with is actively developed.<br/>
Do you really want to have useless numbers on yet another README file?
Note: The default version of `EntityX` (`master` branch) wasn't added to the
comparison because it's already much slower than its compile-time counterpart.
If you are interested, you can compile the `benchmark` test in release mode (to
enable compiler optimizations, otherwise it would make little sense) by setting
the `BUILD_BENCHMARK` option to `ON`, then evaluate yourself whether you're
satisfied with the results or not.
Pretty interesting results, aren't them? In fact, these benchmarks are the ones
used by `EntityX` to show _how fast it is_. To be honest, they aren't so good
and these results shouldn't be taken too seriously (indeed they are completely
unrealistic).<br/>
The proposed entity-component system is incredibly fast to iterate entities,
this is a fact. The compiler can make a lot of optimizations because of how
`EnTT` works, even more when components aren't used at all. This is exactly the
case for these benchmarks. On the other hand, if we consider real world cases,
`EnTT` is somewhere between a bit and much faster than the other solutions
around when users also access the components and not just the entities, although
it isn't as fast as reported by these benchmarks.<br/>
This is why they are completely wrong and cannot be used to evaluate any of the
entity-component-system libraries out there.
The proposed entity-component system is incredibly fast to iterate entities and
components, this is a fact. Some compilers make a lot of optimizations because
of how `EnTT` works, even more when components aren't used at all. In general,
if we consider real world cases, `EnTT` is somewhere between a bit and much
faster than many of the other solutions around, although I couldn't check them
all for obvious reasons.
The choice to use `EnTT` should be based on its carefully designed API, its
set of features and the general performance, not because some single benchmark
shows it to be the fastest tool available.
set of features and the general performance, **not** because some single
benchmark shows it to be the fastest tool available.
In the future I'll likely try to get even better performance while still adding
new features, mainly for fun.<br/>

1
TODO
View File

@@ -33,4 +33,3 @@ TODO
* nested groups: AB/ABC/ABCD/... (hints: sort, check functions)
* multi component registry::remove and some others?
* range based registry::remove and some others?
* allocation-less and faster sort

View File

@@ -378,10 +378,6 @@ public:
* * An iterator past the last element of the range to sort.
* * A comparison function to use to compare the elements.
*
* The comparison function object received by the sort function object
* hasn't necessarily the type of the one passed along with the other
* parameters to this member function.
*
* @note
* Attempting to iterate elements using a raw pointer returned by a call to
* either `data` or `raw` gives no guarantees on the order, even though
@@ -400,10 +396,13 @@ public:
if constexpr(sizeof...(Component) == 0) {
static_assert(std::is_invocable_v<Compare, const entity_type, const entity_type>);
handler->sort(handler->begin(), handler->end(), std::move(compare), std::move(algo), std::forward<Args>(args)...);
} else if constexpr(sizeof...(Component) == 1) {
handler->sort(handler->begin(), handler->end(), [this, compare = std::move(compare)](const entity_type lhs, const entity_type rhs) {
return compare((std::get<pool_type<Component> *>(pools)->get(lhs), ...), (std::get<pool_type<Component> *>(pools)->get(rhs), ...));
}, std::move(algo), std::forward<Args>(args)...);
} else {
handler->sort(handler->begin(), handler->end(), [this, compare = std::move(compare)](const entity_type lhs, const entity_type rhs) {
// useless this-> used to suppress a warning with clang
return compare(this->get<Component...>(lhs), this->get<Component...>(rhs));
return compare(std::tuple<decltype(get<Component>({}))...>{std::get<pool_type<Component> *>(pools)->get(lhs)...}, std::tuple<decltype(get<Component>({}))...>{std::get<pool_type<Component> *>(pools)->get(rhs)...});
}, std::move(algo), std::forward<Args>(args)...);
}
}
@@ -801,10 +800,6 @@ public:
* * An iterator past the last element of the range to sort.
* * A comparison function to use to compare the elements.
*
* The comparison function object received by the sort function object
* hasn't necessarily the type of the one passed along with the other
* parameters to this member function.
*
* @note
* Attempting to iterate elements using a raw pointer returned by a call to
* either `data` or `raw` gives no guarantees on the order, even though
@@ -825,10 +820,13 @@ public:
if constexpr(sizeof...(Component) == 0) {
static_assert(std::is_invocable_v<Compare, const entity_type, const entity_type>);
cpool->sort(cpool->end()-*length, cpool->end(), std::move(compare), std::move(algo), std::forward<Args>(args)...);
} else if constexpr(sizeof...(Component) == 1) {
cpool->sort(cpool->end()-*length, cpool->end(), [this, compare = std::move(compare)](const entity_type lhs, const entity_type rhs) {
return compare((std::get<pool_type<Component> *>(pools)->get(lhs), ...), (std::get<pool_type<Component> *>(pools)->get(rhs), ...));
}, std::move(algo), std::forward<Args>(args)...);
} else {
cpool->sort(cpool->end()-*length, cpool->end(), [this, compare = std::move(compare)](const entity_type lhs, const entity_type rhs) {
// useless this-> used to suppress a warning with clang
return compare(this->get<Component...>(lhs), this->get<Component...>(rhs));
return compare(std::tuple<decltype(get<Component>({}))...>{std::get<pool_type<Component> *>(pools)->get(lhs)...}, std::tuple<decltype(get<Component>({}))...>{std::get<pool_type<Component> *>(pools)->get(rhs)...});
}, std::move(algo), std::forward<Args>(args)...);
}

View File

@@ -499,10 +499,6 @@ public:
* * An iterator past the last element of the range to sort.
* * A comparison function to use to compare the elements.
*
* The comparison function object received by the sort function object
* hasn't necessarily the type of the one passed along with the other
* parameters to this member function.
*
* @note
* Attempting to iterate elements using a raw pointer returned by a call to
* `data` gives no guarantees on the order, even though `sort` has been
@@ -519,25 +515,74 @@ public:
*/
template<typename Compare, typename Sort = std_sort, typename... Args>
void sort(iterator_type first, iterator_type last, Compare compare, Sort algo = Sort{}, Args &&... args) {
ENTT_ASSERT(!(first > last));
ENTT_ASSERT(!(last < first));
ENTT_ASSERT(!(last > end()));
std::vector<size_type> copy(last - first);
const auto offset = std::distance(last, end());
std::iota(copy.begin(), copy.end(), size_type{});
const auto length = std::distance(first, last);
const auto skip = std::distance(last, end());
const auto to = direct.rend() - skip;
const auto from = to - length;
algo(copy.rbegin(), copy.rend(), [this, offset, compare = std::move(compare)](const auto lhs, const auto rhs) {
return compare(std::as_const(direct[lhs+offset]), std::as_const(direct[rhs+offset]));
}, std::forward<Args>(args)...);
algo(from, to, std::move(compare), std::forward<Args>(args)...);
for(size_type pos{}, length = copy.size(); pos < length; ++pos) {
for(size_type pos = skip, end = skip+length; pos < end; pos++) {
auto [page, offset] = map(direct[pos]);
reverse[page][offset] = entity_type(pos);
}
}
/**
* @brief Sort elements according to the given comparison function.
*
* @sa sort
*
* This function is a slightly slower version of `sort` that invokes the
* caller to indicate which entities are swapped.<br/>
* It's recommended when the caller wants to sort its own data structures to
* align them with the order induced in the sparse set.
*
* The signature of the callback should be equivalent to the following:
*
* @code{.cpp}
* bool(const Entity, const Entity);
* @endcode
*
* @tparam Apply Type of function object to invoke to notify the caller.
* @tparam Compare Type of comparison function object.
* @tparam Sort Type of sort function object.
* @tparam Args Types of arguments to forward to the sort function object.
* @param first An iterator to the first element of the range to sort.
* @param last An iterator past the last element of the range to sort.
* @param apply A valid function object to use as a callback.
* @param compare A valid comparison function object.
* @param algo A valid sort function object.
* @param args Arguments to forward to the sort function object, if any.
*/
template<typename Apply, typename Compare, typename Sort = std_sort, typename... Args>
void arrange(iterator_type first, iterator_type last, Apply apply, Compare compare, Sort algo = Sort{}, Args &&... args) {
ENTT_ASSERT(!(last < first));
ENTT_ASSERT(!(last > end()));
const auto length = std::distance(first, last);
const auto skip = std::distance(last, end());
const auto to = direct.rend() - skip;
const auto from = to - length;
algo(from, to, std::move(compare), std::forward<Args>(args)...);
for(size_type pos = skip, end = skip+length; pos < end; pos++) {
auto curr = pos;
auto next = copy[curr];
auto next = index(direct[curr]);
while(curr != next) {
swap(direct[copy[curr] + offset], direct[copy[next] + offset]);
copy[curr] = curr;
auto [src_page, src_offset] = map(direct[curr]);
auto [dst_page, dst_offset] = map(direct[next]);
apply(direct[curr], direct[next]);
std::swap(reverse[src_page][src_offset], reverse[dst_page][dst_offset]);
curr = next;
next = copy[curr];
next = index(direct[curr]);
}
}
}

View File

@@ -426,10 +426,6 @@ public:
* * An iterator past the last element of the range to sort.
* * A comparison function to use to compare the elements.
*
* The comparison function object received by the sort function object
* hasn't necessarily the type of the one passed along with the other
* parameters to this member function.
*
* @note
* Attempting to iterate elements using a raw pointer returned by a call to
* either `data` or `raw` gives no guarantees on the order, even though
@@ -446,19 +442,24 @@ public:
*/
template<typename Compare, typename Sort = std_sort, typename... Args>
void sort(iterator_type first, iterator_type last, Compare compare, Sort algo = Sort{}, Args &&... args) {
ENTT_ASSERT(!(first > last));
ENTT_ASSERT(!(last < first));
ENTT_ASSERT(!(last > end()));
const auto from = underlying_type::begin() + std::distance(begin(), first);
const auto to = from + std::distance(first, last);
const auto apply = [this](const auto lhs, const auto rhs) {
std::swap(instances[underlying_type::index(lhs)], instances[underlying_type::index(rhs)]);
};
if constexpr(std::is_invocable_v<Compare, const object_type &, const object_type &>) {
static_assert(!std::is_empty_v<object_type>);
underlying_type::sort(from, to, [this, compare = std::move(compare)](const auto lhs, const auto rhs) {
underlying_type::arrange(from, to, std::move(apply), [this, compare = std::move(compare)](const auto lhs, const auto rhs) {
return compare(std::as_const(instances[underlying_type::index(lhs)]), std::as_const(instances[underlying_type::index(rhs)]));
}, std::move(algo), std::forward<Args>(args)...);
} else {
underlying_type::sort(from, to, std::move(compare), std::move(algo), std::forward<Args>(args)...);
underlying_type::arrange(from, to, std::move(apply), std::move(compare), std::move(algo), std::forward<Args>(args)...);
}
}

View File

@@ -1,6 +1,8 @@
#include <cstdint>
#include <utility>
#include <iterator>
#include <algorithm>
#include <functional>
#include <type_traits>
#include <gtest/gtest.h>
#include <entt/entity/sparse_set.hpp>
@@ -240,9 +242,7 @@ TEST(SparseSet, SortOrdered) {
ASSERT_EQ(*(set.data() + 3u), entt::entity{7});
ASSERT_EQ(*(set.data() + 4u), entt::entity{3});
set.sort(set.begin(), set.end(), [](const auto lhs, const auto rhs) {
return std::underlying_type_t<entt::entity>(lhs) < std::underlying_type_t<entt::entity>(rhs);
});
set.sort(set.begin(), set.end(), std::less{});
ASSERT_EQ(*(set.data() + 0u), entt::entity{42});
ASSERT_EQ(*(set.data() + 1u), entt::entity{12});
@@ -276,9 +276,7 @@ TEST(SparseSet, SortReverse) {
ASSERT_EQ(*(set.data() + 3u), entt::entity{12});
ASSERT_EQ(*(set.data() + 4u), entt::entity{42});
set.sort(set.begin(), set.end(), [](const auto lhs, const auto rhs) {
return std::underlying_type_t<entt::entity>(lhs) < std::underlying_type_t<entt::entity>(rhs);
});
set.sort(set.begin(), set.end(), std::less{});
ASSERT_EQ(*(set.data() + 0u), entt::entity{42});
ASSERT_EQ(*(set.data() + 1u), entt::entity{12});
@@ -312,9 +310,7 @@ TEST(SparseSet, SortUnordered) {
ASSERT_EQ(*(set.data() + 3u), entt::entity{12});
ASSERT_EQ(*(set.data() + 4u), entt::entity{42});
set.sort(set.begin(), set.end(), [](const auto lhs, const auto rhs) {
return std::underlying_type_t<entt::entity>(lhs) < std::underlying_type_t<entt::entity>(rhs);
});
set.sort(set.begin(), set.end(), std::less{});
ASSERT_EQ(*(set.data() + 0u), entt::entity{42});
ASSERT_EQ(*(set.data() + 1u), entt::entity{12});
@@ -348,9 +344,7 @@ TEST(SparseSet, SortRange) {
ASSERT_EQ(*(set.data() + 3u), entt::entity{12});
ASSERT_EQ(*(set.data() + 4u), entt::entity{42});
set.sort(set.end(), set.end(), [](const auto lhs, const auto rhs) {
return std::underlying_type_t<entt::entity>(lhs) < std::underlying_type_t<entt::entity>(rhs);
});
set.sort(set.end(), set.end(), std::less{});
ASSERT_EQ(*(set.data() + 0u), entt::entity{9});
ASSERT_EQ(*(set.data() + 1u), entt::entity{7});
@@ -358,9 +352,7 @@ TEST(SparseSet, SortRange) {
ASSERT_EQ(*(set.data() + 3u), entt::entity{12});
ASSERT_EQ(*(set.data() + 4u), entt::entity{42});
set.sort(set.begin(), set.begin(), [](const auto lhs, const auto rhs) {
return std::underlying_type_t<entt::entity>(lhs) < std::underlying_type_t<entt::entity>(rhs);
});
set.sort(set.begin(), set.begin(), std::less{});
ASSERT_EQ(*(set.data() + 0u), entt::entity{9});
ASSERT_EQ(*(set.data() + 1u), entt::entity{7});
@@ -368,9 +360,7 @@ TEST(SparseSet, SortRange) {
ASSERT_EQ(*(set.data() + 3u), entt::entity{12});
ASSERT_EQ(*(set.data() + 4u), entt::entity{42});
set.sort(set.begin()+2, set.begin()+3, [](const auto lhs, const auto rhs) {
return std::underlying_type_t<entt::entity>(lhs) < std::underlying_type_t<entt::entity>(rhs);
});
set.sort(set.begin()+2, set.begin()+3, std::less{});
ASSERT_EQ(*(set.data() + 0u), entt::entity{9});
ASSERT_EQ(*(set.data() + 1u), entt::entity{7});
@@ -378,9 +368,7 @@ TEST(SparseSet, SortRange) {
ASSERT_EQ(*(set.data() + 3u), entt::entity{12});
ASSERT_EQ(*(set.data() + 4u), entt::entity{42});
set.sort(++set.begin(), --set.end(), [](const auto lhs, const auto rhs) {
return std::underlying_type_t<entt::entity>(lhs) < std::underlying_type_t<entt::entity>(rhs);
});
set.sort(++set.begin(), --set.end(), std::less{});
ASSERT_EQ(*(set.data() + 0u), entt::entity{9});
ASSERT_EQ(*(set.data() + 1u), entt::entity{12});
@@ -399,6 +387,104 @@ TEST(SparseSet, SortRange) {
ASSERT_EQ(begin, end);
}
TEST(SparseSet, ArrangOrdered) {
entt::sparse_set<entt::entity> set;
entt::entity entities[5]{entt::entity{42}, entt::entity{12}, entt::entity{9}, entt::entity{7}, entt::entity{3}};
set.batch(std::begin(entities), std::end(entities));
set.arrange(set.begin(), set.end(), [](auto...) { FAIL(); }, std::less{});
ASSERT_EQ(*(set.data() + 0u), entt::entity{42});
ASSERT_EQ(*(set.data() + 1u), entt::entity{12});
ASSERT_EQ(*(set.data() + 2u), entt::entity{9});
ASSERT_EQ(*(set.data() + 3u), entt::entity{7});
ASSERT_EQ(*(set.data() + 4u), entt::entity{3});
ASSERT_TRUE(std::equal(std::begin(entities), std::end(entities), set.data()));
}
TEST(SparseSet, ArrangeReverse) {
entt::sparse_set<entt::entity> set;
entt::entity entities[5]{entt::entity{3}, entt::entity{7}, entt::entity{9}, entt::entity{12}, entt::entity{42}};
set.batch(std::begin(entities), std::end(entities));
set.arrange(set.begin(), set.end(), [&set, &entities](const auto lhs, const auto rhs) {
std::swap(entities[set.index(lhs)], entities[set.index(rhs)]);
}, std::less{});
ASSERT_EQ(*(set.data() + 0u), entt::entity{42});
ASSERT_EQ(*(set.data() + 1u), entt::entity{12});
ASSERT_EQ(*(set.data() + 2u), entt::entity{9});
ASSERT_EQ(*(set.data() + 3u), entt::entity{7});
ASSERT_EQ(*(set.data() + 4u), entt::entity{3});
ASSERT_TRUE(std::equal(std::begin(entities), std::end(entities), set.data()));
}
TEST(SparseSet, ArrangeUnordered) {
entt::sparse_set<entt::entity> set;
entt::entity entities[5]{entt::entity{9}, entt::entity{7}, entt::entity{3}, entt::entity{12}, entt::entity{42}};
set.batch(std::begin(entities), std::end(entities));
set.arrange(set.begin(), set.end(), [&set, &entities](const auto lhs, const auto rhs) {
std::swap(entities[set.index(lhs)], entities[set.index(rhs)]);
}, std::less{});
ASSERT_EQ(*(set.data() + 0u), entt::entity{42});
ASSERT_EQ(*(set.data() + 1u), entt::entity{12});
ASSERT_EQ(*(set.data() + 2u), entt::entity{9});
ASSERT_EQ(*(set.data() + 3u), entt::entity{7});
ASSERT_EQ(*(set.data() + 4u), entt::entity{3});
ASSERT_TRUE(std::equal(std::begin(entities), std::end(entities), set.data()));
}
TEST(SparseSet, ArrangeRange) {
entt::sparse_set<entt::entity> set;
entt::entity entities[5]{entt::entity{9}, entt::entity{7}, entt::entity{3}, entt::entity{12}, entt::entity{42}};
set.batch(std::begin(entities), std::end(entities));
set.arrange(set.end(), set.end(), [&set, &entities](const auto lhs, const auto rhs) {
std::swap(entities[set.index(lhs)], entities[set.index(rhs)]);
}, std::less{});
ASSERT_EQ(*(set.data() + 0u), entt::entity{9});
ASSERT_EQ(*(set.data() + 1u), entt::entity{7});
ASSERT_EQ(*(set.data() + 2u), entt::entity{3});
ASSERT_EQ(*(set.data() + 3u), entt::entity{12});
ASSERT_EQ(*(set.data() + 4u), entt::entity{42});
set.arrange(set.begin(), set.begin(), [&set, &entities](const auto lhs, const auto rhs) {
std::swap(entities[set.index(lhs)], entities[set.index(rhs)]);
}, std::less{});
ASSERT_EQ(*(set.data() + 0u), entt::entity{9});
ASSERT_EQ(*(set.data() + 1u), entt::entity{7});
ASSERT_EQ(*(set.data() + 2u), entt::entity{3});
ASSERT_EQ(*(set.data() + 3u), entt::entity{12});
ASSERT_EQ(*(set.data() + 4u), entt::entity{42});
set.arrange(set.begin()+2, set.begin()+3, [&set, &entities](const auto lhs, const auto rhs) {
std::swap(entities[set.index(lhs)], entities[set.index(rhs)]);
}, std::less{});
ASSERT_EQ(*(set.data() + 0u), entt::entity{9});
ASSERT_EQ(*(set.data() + 1u), entt::entity{7});
ASSERT_EQ(*(set.data() + 2u), entt::entity{3});
ASSERT_EQ(*(set.data() + 3u), entt::entity{12});
ASSERT_EQ(*(set.data() + 4u), entt::entity{42});
set.arrange(++set.begin(), --set.end(), [&set, &entities](const auto lhs, const auto rhs) {
std::swap(entities[set.index(lhs)], entities[set.index(rhs)]);
}, std::less{});
ASSERT_EQ(*(set.data() + 0u), entt::entity{9});
ASSERT_EQ(*(set.data() + 1u), entt::entity{12});
ASSERT_EQ(*(set.data() + 2u), entt::entity{7});
ASSERT_EQ(*(set.data() + 3u), entt::entity{3});
ASSERT_EQ(*(set.data() + 4u), entt::entity{42});
}
TEST(SparseSet, RespectDisjoint) {
entt::sparse_set<entt::entity> lhs;
entt::sparse_set<entt::entity> rhs;