-
Notifications
You must be signed in to change notification settings - Fork 14.3k
[libc++] Speed up set_intersection() by fast-forwarding over ranges of non-matching elements with one-sided binary search. #75230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b65415f
f6bcf27
36bb63e
c23272c
0b57ea0
08af548
7aa3927
c44c2a2
46cc95f
450f5ce
d0c5f2b
faa3115
995d04b
d568d49
6ba7061
76c33ca
bb872e0
a1cd8ff
24d1d5b
f17fa58
4b73773
65bd9b7
d0facc5
a12aa37
69dba78
fe1fe8c
bb2c758
c6b895c
31321b9
6c88549
3805e95
090df86
cb92d3c
f4a6f36
3f9cfec
1afb99d
613e64a
4588447
2af9a6f
4f05ded
161d81c
3c9f800
4aa4a82
8307b2d
be6c5c8
62a6010
e2af5cc
89201ea
5f6e7fe
109e5a4
cc95b51
91e4e51
c977bb7
b4fad5b
87f12c2
505c004
95b118a
b1bfa0f
f501bdc
c5df570
6189e95
6eacf2f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
// See https://llvm.org/LICENSE.txt for license information. | ||
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
#include <algorithm> | ||
#include <cstdlib> | ||
#include <iterator> | ||
#include <set> | ||
#include <vector> | ||
|
||
#include "common.h" | ||
#include "test_iterators.h" | ||
|
||
namespace { | ||
|
||
// types of containers we'll want to test, covering interesting iterator types | ||
struct VectorContainer { | ||
template <typename... Args> | ||
using type = std::vector<Args...>; | ||
|
||
static constexpr const char* Name = "Vector"; | ||
}; | ||
|
||
struct SetContainer { | ||
template <typename... Args> | ||
using type = std::set<Args...>; | ||
|
||
static constexpr const char* Name = "Set"; | ||
}; | ||
|
||
using AllContainerTypes = std::tuple<VectorContainer, SetContainer>; | ||
|
||
// set_intersection performance may depend on where matching values lie | ||
enum class OverlapPosition { | ||
None, | ||
Front, | ||
// performance-wise, matches at the back are identical to ones at the front | ||
Interlaced, | ||
}; | ||
|
||
struct AllOverlapPositions : EnumValuesAsTuple<AllOverlapPositions, OverlapPosition, 3> { | ||
static constexpr const char* Names[] = {"None", "Front", "Interlaced"}; | ||
}; | ||
|
||
// forward_iterator wrapping which, for each increment, moves the underlying iterator forward Stride elements | ||
template <typename Wrapped> | ||
struct StridedFwdIt { | ||
Wrapped base_; | ||
unsigned stride_; | ||
|
||
using iterator_category = std::forward_iterator_tag; | ||
using difference_type = typename Wrapped::difference_type; | ||
using value_type = typename Wrapped::value_type; | ||
using pointer = typename Wrapped::pointer; | ||
using reference = typename Wrapped::reference; | ||
|
||
StridedFwdIt(Wrapped base, unsigned stride) : base_(base), stride_(stride) { assert(stride_ != 0); } | ||
|
||
StridedFwdIt operator++() { | ||
for (unsigned i = 0; i < stride_; ++i) | ||
++base_; | ||
return *this; | ||
} | ||
StridedFwdIt operator++(int) { | ||
auto tmp = *this; | ||
++*this; | ||
return tmp; | ||
} | ||
value_type& operator*() { return *base_; } | ||
const value_type& operator*() const { return *base_; } | ||
value_type& operator->() { return *base_; } | ||
const value_type& operator->() const { return *base_; } | ||
bool operator==(const StridedFwdIt& o) const { return base_ == o.base_; } | ||
bool operator!=(const StridedFwdIt& o) const { return !operator==(o); } | ||
}; | ||
template <typename Wrapped> | ||
StridedFwdIt(Wrapped, unsigned) -> StridedFwdIt<Wrapped>; | ||
|
||
template <typename T> | ||
std::vector<T> getVectorOfRandom(size_t N) { | ||
std::vector<T> v; | ||
fillValues(v, N, Order::Random); | ||
sortValues(v, Order::Random); | ||
return std::vector<T>(v); | ||
} | ||
|
||
// Realistically, data won't all be nicely contiguous in a container, | ||
// we'll go through some effort to ensure that it's shuffled through memory | ||
// this is especially important for containers with non-contiguous element | ||
// storage, but it will affect even a std::vector, because when you copy a | ||
// std::vector<std::string> the underlying data storage position for the char | ||
// arrays of the copy are likely to have high locality | ||
template <class Container> | ||
std::pair<Container, Container> genCacheUnfriendlyData(size_t size1, size_t size2, OverlapPosition pos) { | ||
using ValueType = typename Container::value_type; | ||
auto move_into = [](auto first, auto last) { | ||
Container out; | ||
std::move(first, last, std::inserter(out, out.begin())); | ||
return out; | ||
}; | ||
const auto src_size = pos == OverlapPosition::None ? size1 + size2 : std::max(size1, size2); | ||
std::vector<ValueType> src = getVectorOfRandom<ValueType>(src_size); | ||
|
||
if (pos == OverlapPosition::None) { | ||
std::sort(src.begin(), src.end()); | ||
return std::make_pair(move_into(src.begin(), src.begin() + size1), move_into(src.begin() + size1, src.end())); | ||
} | ||
|
||
// All other overlap types will have to copy some part of the data, but if | ||
// we copy after sorting it will likely have high locality, so we sort | ||
// each copy separately | ||
auto copy = src; | ||
std::sort(src.begin(), src.end()); | ||
std::sort(copy.begin(), copy.end()); | ||
|
||
switch (pos) { | ||
case OverlapPosition::None: | ||
// we like -Wswitch :) | ||
break; | ||
|
||
case OverlapPosition::Front: | ||
return std::make_pair(move_into(src.begin(), src.begin() + size1), move_into(copy.begin(), copy.begin() + size2)); | ||
|
||
case OverlapPosition::Interlaced: | ||
const auto stride1 = size1 < size2 ? size2 / size1 : 1; | ||
const auto stride2 = size2 < size1 ? size1 / size2 : 1; | ||
return std::make_pair(move_into(StridedFwdIt(src.begin(), stride1), StridedFwdIt(src.end(), stride1)), | ||
move_into(StridedFwdIt(copy.begin(), stride2), StridedFwdIt(copy.end(), stride2))); | ||
} | ||
std::abort(); // would be std::unreachable() if it could | ||
return std::pair<Container, Container>(); | ||
} | ||
|
||
template <class ValueType, class Container, class Overlap> | ||
struct SetIntersection { | ||
using ContainerType = typename Container::template type<Value<ValueType>>; | ||
size_t size1_; | ||
size_t size2_; | ||
|
||
SetIntersection(size_t size1, size_t size2) : size1_(size1), size2_(size2) {} | ||
|
||
bool skip() const noexcept { | ||
// let's save some time and skip simmetrical runs | ||
return size1_ < size2_; | ||
} | ||
|
||
void run(benchmark::State& state) const { | ||
auto input = genCacheUnfriendlyData<ContainerType>(size1_, size2_, Overlap()); | ||
std::vector<Value<ValueType>> out(std::min(size1_, size2_)); | ||
|
||
const auto BATCH_SIZE = std::max(size_t{512}, (2 * TestSetElements) / (size1_ + size2_)); | ||
for (const auto& _ : state) { | ||
while (state.KeepRunningBatch(BATCH_SIZE)) { | ||
for (unsigned i = 0; i < BATCH_SIZE; ++i) { | ||
const auto& [c1, c2] = input; | ||
auto res = std::set_intersection(c1.begin(), c1.end(), c2.begin(), c2.end(), out.begin()); | ||
benchmark::DoNotOptimize(res); | ||
} | ||
} | ||
} | ||
} | ||
|
||
std::string name() const { | ||
return std::string("SetIntersection") + Overlap::name() + '_' + Container::Name + ValueType::name() + '_' + | ||
std::to_string(size1_) + '_' + std::to_string(size2_); | ||
} | ||
}; | ||
|
||
} // namespace | ||
|
||
int main(int argc, char** argv) { /**/ | ||
benchmark::Initialize(&argc, argv); | ||
if (benchmark::ReportUnrecognizedArguments(argc, argv)) | ||
return 1; | ||
|
||
makeCartesianProductBenchmark<SetIntersection, AllValueTypes, AllContainerTypes, AllOverlapPositions>( | ||
Quantities, Quantities); | ||
benchmark::RunSpecifiedBenchmarks(); | ||
return 0; | ||
} | ||
ichaer marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,11 +27,13 @@ | |
|
||
_LIBCPP_BEGIN_NAMESPACE_STD | ||
|
||
template <class _AlgPolicy, class _Iter, class _Sent, class _Type, class _Proj, class _Comp> | ||
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _Iter | ||
__lower_bound(_Iter __first, _Sent __last, const _Type& __value, _Comp& __comp, _Proj& __proj) { | ||
auto __len = _IterOps<_AlgPolicy>::distance(__first, __last); | ||
|
||
template <class _AlgPolicy, class _Iter, class _Type, class _Proj, class _Comp> | ||
_LIBCPP_NODISCARD _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _Iter __lower_bound_bisecting( | ||
_Iter __first, | ||
const _Type& __value, | ||
typename iterator_traits<_Iter>::difference_type __len, | ||
_Comp& __comp, | ||
_Proj& __proj) { | ||
while (__len != 0) { | ||
auto __l2 = std::__half_positive(__len); | ||
_Iter __m = __first; | ||
|
@@ -46,6 +48,48 @@ __lower_bound(_Iter __first, _Sent __last, const _Type& __value, _Comp& __comp, | |
return __first; | ||
} | ||
|
||
// One-sided binary search, aka meta binary search, has been in the public domain for decades, and has the general | ||
// advantage of being \Omega(1) rather than the classic algorithm's \Omega(log(n)), with the downside of executing at | ||
// most 2*log(n) comparisons vs the classic algorithm's exact log(n). There are two scenarios in which it really shines: | ||
// the first one is when operating over non-random-access iterators, because the classic algorithm requires knowing the | ||
// container's size upfront, which adds \Omega(n) iterator increments to the complexity. The second one is when you're | ||
// traversing the container in order, trying to fast-forward to the next value: in that case, the classic algorithm | ||
// would yield \Omega(n*log(n)) comparisons and, for non-random-access iterators, \Omega(n^2) iterator increments, | ||
// whereas the one-sided version will yield O(n) operations on both counts, with a \Omega(log(n)) bound on the number of | ||
// comparisons. | ||
template <class _AlgPolicy, class _ForwardIterator, class _Sent, class _Type, class _Proj, class _Comp> | ||
_LIBCPP_NODISCARD _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _ForwardIterator | ||
__lower_bound_onesided(_ForwardIterator __first, _Sent __last, const _Type& __value, _Comp& __comp, _Proj& __proj) { | ||
// step = 0, ensuring we can always short-circuit when distance is 1 later on | ||
if (__first == __last || !std::__invoke(__comp, std::__invoke(__proj, *__first), __value)) | ||
return __first; | ||
|
||
using _Distance = typename iterator_traits<_ForwardIterator>::difference_type; | ||
for (_Distance __step = 1; __first != __last; __step <<= 1) { | ||
auto __it = __first; | ||
auto __dist = __step - _IterOps<_AlgPolicy>::__advance_to(__it, __step, __last); | ||
// once we reach the last range where needle can be we must start | ||
// looking inwards, bisecting that range | ||
if (__it == __last || !std::__invoke(__comp, std::__invoke(__proj, *__it), __value)) { | ||
// we've already checked the previous value and it was less, we can save | ||
// one comparison by skipping bisection | ||
if (__dist == 1) | ||
return __it; | ||
return std::__lower_bound_bisecting<_AlgPolicy>(__first, __value, __dist, __comp, __proj); | ||
} | ||
// range not found, move forward! | ||
__first = __it; | ||
} | ||
return __first; | ||
} | ||
|
||
template <class _AlgPolicy, class _ForwardIterator, class _Sent, class _Type, class _Proj, class _Comp> | ||
_LIBCPP_NODISCARD inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _ForwardIterator | ||
__lower_bound(_ForwardIterator __first, _Sent __last, const _Type& __value, _Comp& __comp, _Proj& __proj) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it might be possible to do something like (pseudo): template <class _Iterator, class _Sent, class _Tp, class _Compare, class _Projection>
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _Iterator
__lower_bound(_Iterator __first, _Sent __sent, const _Tp& __value, _Compare __comp, _Projection __proj) {
if constexpr (_Iterator is not random access &&
_Tp is a fundamental type &&
_Compare desugars to a builtin operation &&
__is_identity<_Projection>) {
use-one-sided-lower-bound;
} else {
use-classic-lower-bound;
}
} @philnik777 Do you have thoughts on this? I wonder if we should consider that it's OK (under the as-if rule) to change the complexity of an algorithm with respect to a certain operation if that change cannot be observed other than by e.g. timing the algorithm. At first glance, I would say this is a very pedantic and not especially useful way of constraining ourselves, but I'm curious to hear what you think about this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can certainly improve the complexity here (http://eel.is/c++draft/structure#specifications-7). I do wonder whether this is also the case for the "exactly N times" specifications, but I don't think it's in the interest of anybody to force implementations to add overhead for conformance. Making the complexity worse (by more than a constant factor) would be another story. We already break the complexity requirements by a constant factor if it's not observable, e.g. in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's also my thinking. @ichaer After this patch has landed, you could consider a patch like the above if you want -- I think we'd take it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Awesome =D, thanks! |
||
const auto __dist = _IterOps<_AlgPolicy>::distance(__first, __last); | ||
return std::__lower_bound_bisecting<_AlgPolicy>(__first, __value, __dist, __comp, __proj); | ||
} | ||
|
||
template <class _ForwardIterator, class _Tp, class _Compare> | ||
_LIBCPP_NODISCARD inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _ForwardIterator | ||
lower_bound(_ForwardIterator __first, _ForwardIterator __last, const _Tp& __value, _Compare __comp) { | ||
|
Uh oh!
There was an error while loading. Please reload this page.