The `IndexMask` data structure was designed to allow us to implement set
operations like `union`, `intersection` and `difference` efficiently
(2cfcb8b0b8). This patch adds an evaluator for
arbitrary expressions involving the mentioned operations. The evaluator makes
use of the design of the `IndexMask` data structure to be quite efficient.
In some common cases, the evaluator runs in constant time. So it's very fast
even if the mask contains many millions of indices. If possible the evaluator
works on entire segments at once instead of looking at the individual indices.
This results in a very low constant factor even if the evaluation time is
linear. If the evaluator has to look at the individual indices to be able to
perform the operation, it can make use of multi-threading.
The evaluation consists of the following steps:
1. A coarse evaluation that looks at entire segments at once.
2. All segments that couldn't be fully evaluated by the coarse evaluation are
evaluated exactly by looking at the actual indices. There are two evaluators
for this case. One that is based on `std::set_union` etc. The other one first
converts the index masks to bit spans, then does bit operations to evaluate
the expression, and then converts the bits back into indices. Depending on
the expression, one or the other can be more efficient.
3. Construct an index mask from the evaluated segments.
Showing the performance of the evaluator is kind of difficult because it highly
depends on the input data. Comparing the performance to something that does not
short-circuit when there are full ranges is meaningless, because one can
construct an example where the new evaluator is arbitrarily faster. I'm still
working on a case where performance can be compared to e.g. using
`std::set_union`. This comparison is only fair when the input data when
constructing a case where the new evaluator can't short-circuit.
One of the main remaining bottlenecks are the calls to `slice_content` on large
index masks. I think the impact of those can still be reduced.
We are not using this evaluator much yet, except through `IndexMask::complement`
calls. I intend to use it when I get to refactoring the field evaluator for
geometry nodes to optimize the evaluation of selections.
Pull Request: https://projects.blender.org/blender/blender/pulls/117805
353 lines
9.2 KiB
C++
353 lines
9.2 KiB
C++
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
#pragma once
|
|
|
|
/** \file
|
|
* \ingroup bli
|
|
*
|
|
* A `blender::IndexRange` wraps an interval of non-negative integers. It can be used to reference
|
|
* consecutive elements in an array. Furthermore, it can make for loops more convenient and less
|
|
* error prone, especially when using nested loops.
|
|
*
|
|
* I'd argue that the second loop is more readable and less error prone than the first one. That is
|
|
* not necessarily always the case, but often it is.
|
|
*
|
|
* for (int64_t i = 0; i < 10; i++) {
|
|
* for (int64_t j = 0; j < 20; j++) {
|
|
* for (int64_t k = 0; k < 30; k++) {
|
|
*
|
|
* for (int64_t i : IndexRange(10)) {
|
|
* for (int64_t j : IndexRange(20)) {
|
|
* for (int64_t k : IndexRange(30)) {
|
|
*
|
|
* Some containers like blender::Vector have an index_range() method. This will return the
|
|
* IndexRange that contains all indices that can be used to access the container. This is
|
|
* particularly useful when you want to iterate over the indices and the elements (much like
|
|
* Python's enumerate(), just worse). Again, I think the second example here is better:
|
|
*
|
|
* for (int64_t i = 0; i < my_vector_with_a_long_name.size(); i++) {
|
|
* do_something(i, my_vector_with_a_long_name[i]);
|
|
*
|
|
* for (int64_t i : my_vector_with_a_long_name.index_range()) {
|
|
* do_something(i, my_vector_with_a_long_name[i]);
|
|
*
|
|
* Ideally this could be could be even closer to Python's enumerate(). We might get that in the
|
|
* future with newer C++ versions.
|
|
*/
|
|
|
|
#include <algorithm>
|
|
#include <iosfwd>
|
|
|
|
#include "BLI_assert.h"
|
|
#include "BLI_random_access_iterator_mixin.hh"
|
|
|
|
namespace blender {
|
|
|
|
template<typename T> class Span;
|
|
|
|
class IndexRange {
|
|
private:
|
|
int64_t start_ = 0;
|
|
int64_t size_ = 0;
|
|
|
|
public:
|
|
constexpr IndexRange() = default;
|
|
|
|
constexpr explicit IndexRange(int64_t size) : start_(0), size_(size)
|
|
{
|
|
BLI_assert(size >= 0);
|
|
}
|
|
|
|
constexpr IndexRange(const int64_t start, const int64_t size) : start_(start), size_(size)
|
|
{
|
|
BLI_assert(start >= 0);
|
|
BLI_assert(size >= 0);
|
|
}
|
|
|
|
constexpr static IndexRange from_begin_size(const int64_t begin, const int64_t size)
|
|
{
|
|
return IndexRange(begin, size);
|
|
}
|
|
|
|
constexpr static IndexRange from_begin_end(const int64_t begin, const int64_t end)
|
|
{
|
|
return IndexRange(begin, end - begin);
|
|
}
|
|
|
|
constexpr static IndexRange from_begin_end_inclusive(const int64_t begin, const int64_t last)
|
|
{
|
|
return IndexRange(begin, last - begin + 1);
|
|
}
|
|
|
|
constexpr static IndexRange from_end_size(const int64_t end, const int64_t size)
|
|
{
|
|
return IndexRange(end - size, size);
|
|
}
|
|
|
|
constexpr static IndexRange from_single(const int64_t index)
|
|
{
|
|
return IndexRange(index, 1);
|
|
}
|
|
|
|
class Iterator : public iterator::RandomAccessIteratorMixin<Iterator> {
|
|
public:
|
|
using value_type = int64_t;
|
|
using pointer = const int64_t *;
|
|
using reference = int64_t;
|
|
|
|
private:
|
|
int64_t current_;
|
|
|
|
public:
|
|
constexpr explicit Iterator(int64_t current) : current_(current) {}
|
|
|
|
constexpr int64_t operator*() const
|
|
{
|
|
return current_;
|
|
}
|
|
|
|
const int64_t &iter_prop() const
|
|
{
|
|
return current_;
|
|
}
|
|
};
|
|
|
|
constexpr Iterator begin() const
|
|
{
|
|
return Iterator(start_);
|
|
}
|
|
|
|
constexpr Iterator end() const
|
|
{
|
|
return Iterator(start_ + size_);
|
|
}
|
|
|
|
/**
|
|
* Access an element in the range.
|
|
*/
|
|
constexpr int64_t operator[](int64_t index) const
|
|
{
|
|
BLI_assert(index >= 0);
|
|
BLI_assert(index < this->size());
|
|
return start_ + index;
|
|
}
|
|
|
|
/**
|
|
* Two ranges compare equal when they contain the same numbers.
|
|
*/
|
|
constexpr friend bool operator==(IndexRange a, IndexRange b)
|
|
{
|
|
return (a.size_ == b.size_) && (a.start_ == b.start_ || a.size_ == 0);
|
|
}
|
|
constexpr friend bool operator!=(IndexRange a, IndexRange b)
|
|
{
|
|
return !(a == b);
|
|
}
|
|
|
|
/**
|
|
* Get the amount of numbers in the range.
|
|
*/
|
|
constexpr int64_t size() const
|
|
{
|
|
return size_;
|
|
}
|
|
|
|
constexpr IndexRange index_range() const
|
|
{
|
|
return IndexRange(size_);
|
|
}
|
|
|
|
/**
|
|
* Returns true if the size is zero.
|
|
*/
|
|
constexpr bool is_empty() const
|
|
{
|
|
return size_ == 0;
|
|
}
|
|
|
|
/**
|
|
* Creates a new index range with the same beginning but a different end.
|
|
*/
|
|
constexpr IndexRange with_new_end(const int64_t new_end) const
|
|
{
|
|
return IndexRange::from_begin_end(start_, new_end);
|
|
}
|
|
|
|
/**
|
|
* Create a new range starting at the end of the current one.
|
|
*/
|
|
constexpr IndexRange after(int64_t n) const
|
|
{
|
|
BLI_assert(n >= 0);
|
|
return IndexRange(start_ + size_, n);
|
|
}
|
|
|
|
/**
|
|
* Create a new range that ends at the start of the current one.
|
|
*/
|
|
constexpr IndexRange before(int64_t n) const
|
|
{
|
|
BLI_assert(n >= 0);
|
|
return IndexRange(start_ - n, n);
|
|
}
|
|
|
|
/**
|
|
* Get the first element in the range.
|
|
* Asserts when the range is empty.
|
|
*/
|
|
constexpr int64_t first() const
|
|
{
|
|
BLI_assert(this->size() > 0);
|
|
return start_;
|
|
}
|
|
|
|
/**
|
|
* Get the nth last element in the range.
|
|
* Asserts when the range is empty or when n is negative.
|
|
*/
|
|
constexpr int64_t last(const int64_t n = 0) const
|
|
{
|
|
BLI_assert(n >= 0);
|
|
BLI_assert(n < size_);
|
|
BLI_assert(this->size() > 0);
|
|
return start_ + size_ - 1 - n;
|
|
}
|
|
|
|
/**
|
|
* Get the element one before the beginning. The returned value is undefined when the range is
|
|
* empty, and the range must start after zero already.
|
|
*/
|
|
constexpr int64_t one_before_start() const
|
|
{
|
|
BLI_assert(start_ > 0);
|
|
return start_ - 1;
|
|
}
|
|
|
|
/**
|
|
* Get the element one after the end. The returned value is undefined when the range is empty.
|
|
*/
|
|
constexpr int64_t one_after_last() const
|
|
{
|
|
return start_ + size_;
|
|
}
|
|
|
|
/**
|
|
* Get the first element in the range. The returned value is undefined when the range is empty.
|
|
*/
|
|
constexpr int64_t start() const
|
|
{
|
|
return start_;
|
|
}
|
|
|
|
/**
|
|
* Returns true when the range contains a certain number, otherwise false.
|
|
*/
|
|
constexpr bool contains(int64_t value) const
|
|
{
|
|
return value >= start_ && value < start_ + size_;
|
|
}
|
|
|
|
/**
|
|
* Returns a new range, that contains a sub-interval of the current one.
|
|
*/
|
|
constexpr IndexRange slice(int64_t start, int64_t size) const
|
|
{
|
|
BLI_assert(start >= 0);
|
|
BLI_assert(size >= 0);
|
|
int64_t new_start = start_ + start;
|
|
BLI_assert(new_start + size <= start_ + size_ || size == 0);
|
|
return IndexRange(new_start, size);
|
|
}
|
|
constexpr IndexRange slice(IndexRange range) const
|
|
{
|
|
return this->slice(range.start(), range.size());
|
|
}
|
|
|
|
/**
|
|
* Returns a new IndexRange that contains the intersection of the current one with the given
|
|
* range. Returns empty range if there are no overlapping indices. The returned range is always
|
|
* a valid slice of this range.
|
|
*/
|
|
constexpr IndexRange intersect(IndexRange other) const
|
|
{
|
|
const int64_t old_end = start_ + size_;
|
|
const int64_t new_start = std::min(old_end, std::max(start_, other.start_));
|
|
const int64_t new_end = std::max(new_start, std::min(old_end, other.start_ + other.size_));
|
|
return IndexRange(new_start, new_end - new_start);
|
|
}
|
|
|
|
/**
|
|
* Returns a new IndexRange with n elements removed from the beginning of the range.
|
|
* This invokes undefined behavior when n is negative.
|
|
*/
|
|
constexpr IndexRange drop_front(int64_t n) const
|
|
{
|
|
BLI_assert(n >= 0);
|
|
const int64_t new_size = std::max<int64_t>(0, size_ - n);
|
|
return IndexRange(start_ + n, new_size);
|
|
}
|
|
|
|
/**
|
|
* Returns a new IndexRange with n elements removed from the end of the range.
|
|
* This invokes undefined behavior when n is negative.
|
|
*/
|
|
constexpr IndexRange drop_back(int64_t n) const
|
|
{
|
|
BLI_assert(n >= 0);
|
|
const int64_t new_size = std::max<int64_t>(0, size_ - n);
|
|
return IndexRange(start_, new_size);
|
|
}
|
|
|
|
/**
|
|
* Returns a new IndexRange that only contains the first n elements. This invokes undefined
|
|
* behavior when n is negative.
|
|
*/
|
|
constexpr IndexRange take_front(int64_t n) const
|
|
{
|
|
BLI_assert(n >= 0);
|
|
const int64_t new_size = std::min<int64_t>(size_, n);
|
|
return IndexRange(start_, new_size);
|
|
}
|
|
|
|
/**
|
|
* Returns a new IndexRange that only contains the last n elements. This invokes undefined
|
|
* behavior when n is negative.
|
|
*/
|
|
constexpr IndexRange take_back(int64_t n) const
|
|
{
|
|
BLI_assert(n >= 0);
|
|
const int64_t new_size = std::min<int64_t>(size_, n);
|
|
return IndexRange(start_ + size_ - new_size, new_size);
|
|
}
|
|
|
|
/**
|
|
* Move the range forward or backward within the larger array. The amount may be negative,
|
|
* but its absolute value cannot be greater than the existing start of the range.
|
|
*/
|
|
constexpr IndexRange shift(int64_t n) const
|
|
{
|
|
return IndexRange(start_ + n, size_);
|
|
}
|
|
|
|
friend std::ostream &operator<<(std::ostream &stream, IndexRange range);
|
|
};
|
|
|
|
struct AlignedIndexRanges {
|
|
IndexRange prefix;
|
|
IndexRange aligned;
|
|
IndexRange suffix;
|
|
};
|
|
|
|
/**
|
|
* Split a range into three parts so that the boundaries of the middle part are aligned to some
|
|
* power of two.
|
|
*
|
|
* This can be used when an algorithm can be optimized on aligned indices/memory. The algorithm
|
|
* then needs a slow path for the beginning and end, and a fast path for the aligned elements.
|
|
*/
|
|
AlignedIndexRanges split_index_range_by_alignment(const IndexRange range, const int64_t alignment);
|
|
|
|
} // namespace blender
|