BLI: support set operations on index masks

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
This commit is contained in:
Jacques Lucke 2024-03-17 09:52:32 +01:00
parent f9cb2eb988
commit ee1fa8e1ca
8 changed files with 1818 additions and 176 deletions

View file

@ -128,6 +128,12 @@ class IndexMaskSegment : public OffsetSpan<int64_t, int16_t> {
IndexMaskSegment slice(const IndexRange &range) const;
IndexMaskSegment slice(const int64_t start, const int64_t size) const;
/**
* Get a new segment where each index is modified by the given amount. This works in constant
* time, because only the offset value is changed.
*/
IndexMaskSegment shift(const int64_t shift) const;
};
/**
@ -423,7 +429,7 @@ class IndexMask : private IndexMaskData {
/**
* Set the bits at indices in the mask to 1 and all other bits to 0.
*/
void to_bits(MutableBitSpan r_bits) const;
void to_bits(MutableBitSpan r_bits, int64_t offset = 0) const;
/**
* Set the bools at indices in the mask to true and all others to false.
*/
@ -534,6 +540,16 @@ inline void masked_fill(MutableSpan<T> data, const T &value, const IndexMask &ma
*/
template<typename T> void build_reverse_map(const IndexMask &mask, MutableSpan<T> r_map);
/**
* Joins segments together based on heuristics. Generally, one wants as few segments as possible,
* but one also wants full-range-segments if possible and we don't want to copy too many indices
* around to reduce the number of segments.
*
* \return Number of consolidated segments. Those are ordered to the beginning of the span.
*/
int64_t consolidate_index_mask_segments(MutableSpan<IndexMaskSegment> segments,
IndexMaskMemory &memory);
/* -------------------------------------------------------------------- */
/** \name #RawMaskIterator Inline Methods
* \{ */
@ -568,6 +584,12 @@ inline IndexMaskSegment IndexMaskSegment::slice(const int64_t start, const int64
static_cast<const OffsetSpan<int64_t, int16_t> *>(this)->slice(start, size));
}
inline IndexMaskSegment IndexMaskSegment::shift(const int64_t shift) const
{
BLI_assert(this->is_empty() || (*this)[0] + shift >= 0);
return IndexMaskSegment(this->offset() + shift, this->base_span());
}
/* -------------------------------------------------------------------- */
/** \name #IndexMask Inline Methods
* \{ */

View file

@ -0,0 +1,94 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_index_mask.hh"
#include "BLI_resource_scope.hh"
namespace blender::index_mask {
struct AtomicExpr;
struct UnionExpr;
struct IntersectionExpr;
struct DifferenceExpr;
struct Expr {
enum class Type {
Atomic,
Union,
Intersection,
Difference,
};
Type type;
int index;
Vector<const Expr *> terms;
int expression_array_size() const;
const AtomicExpr &as_atomic() const;
const UnionExpr &as_union() const;
const IntersectionExpr &as_intersection() const;
const DifferenceExpr &as_difference() const;
};
struct AtomicExpr : public Expr {
const IndexMask *mask;
};
struct UnionExpr : public Expr {};
struct IntersectionExpr : public Expr {};
struct DifferenceExpr : public Expr {};
class ExprBuilder {
private:
ResourceScope scope_;
int expr_count_ = 0;
public:
using Term = std::variant<const Expr *, const IndexMask *, IndexRange>;
const UnionExpr &merge(const Span<Term> terms);
const DifferenceExpr &subtract(const Term &main_term, const Span<Term> subtract_terms);
const IntersectionExpr &intersect(const Span<Term> terms);
private:
const Expr &term_to_expr(const Term &term);
};
IndexMask evaluate_expression(const Expr &expression, IndexMaskMemory &memory);
inline int Expr::expression_array_size() const
{
return this->index + 1;
}
inline const AtomicExpr &Expr::as_atomic() const
{
BLI_assert(this->type == Type::Atomic);
return static_cast<const AtomicExpr &>(*this);
}
inline const UnionExpr &Expr::as_union() const
{
BLI_assert(this->type == Type::Union);
return static_cast<const UnionExpr &>(*this);
}
inline const IntersectionExpr &Expr::as_intersection() const
{
BLI_assert(this->type == Type::Intersection);
return static_cast<const IntersectionExpr &>(*this);
}
inline const DifferenceExpr &Expr::as_difference() const
{
BLI_assert(this->type == Type::Difference);
return static_cast<const DifferenceExpr &>(*this);
}
} // namespace blender::index_mask

View file

@ -167,6 +167,14 @@ class IndexRange {
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.
*/

View file

@ -213,6 +213,38 @@ template<typename Allocator = GuardedAllocator> class LinearAllocator : NonCopya
this->provide_buffer(aligned_buffer.ptr(), Size);
}
/**
* Some algorithms can be implemented more efficiently by over-allocating the destination memory
* a bit. This allows the algorithm not to worry about having enough memory. Generally, this can
* be a useful strategy if the actual required memory is not known in advance, but an upper bound
* can be found. Ideally, one can free the over-allocated memory in the end again to reduce
* memory consumption.
*
* A linear allocator generally does allow freeing any memory. However, there is one exception.
* One can free the end of the last allocation (but not any previous allocation). While uses of
* this approach are quite limited, it's still the best option in some situations.
*/
void free_end_of_previous_allocation(const int64_t original_allocation_size,
const void *free_after)
{
/* If the original allocation size was large, it might have been separately allocated. In this
* case, we can't free the end of it anymore. */
if (original_allocation_size <= large_buffer_threshold) {
const int64_t new_begin = uintptr_t(free_after);
BLI_assert(new_begin <= current_begin_);
#ifndef NDEBUG
/* This condition is not really necessary but it helps finding the cases where memory was
* freed. */
const int64_t freed_bytes_num = current_begin_ - new_begin;
if (freed_bytes_num > 0) {
current_begin_ = new_begin;
}
#else
current_begin_ = new_begin;
#endif
}
}
/**
* This allocator takes ownership of the buffers owned by `other`. Therefor, when `other` is
* destructed, memory allocated using it is not freed.

View file

@ -82,6 +82,7 @@ set(SRC
intern/hash_tables.cc
intern/implicit_sharing.cc
intern/index_mask.cc
intern/index_mask_expression.cc
intern/index_range.cc
intern/jitter_2d.c
intern/kdtree_1d.c
@ -252,6 +253,7 @@ set(SRC
BLI_implicit_sharing_ptr.hh
BLI_index_mask.hh
BLI_index_mask_fwd.hh
BLI_index_mask_expression.hh
BLI_index_range.hh
BLI_inplace_priority_queue.hh
BLI_iterator.h
@ -516,6 +518,7 @@ if(WITH_GTESTS)
tests/BLI_heap_test.cc
tests/BLI_implicit_sharing_test.cc
tests/BLI_index_mask_test.cc
tests/BLI_index_mask_expression_test.cc
tests/BLI_index_range_test.cc
tests/BLI_inplace_priority_queue_test.cc
tests/BLI_kdopbvh_test.cc

View file

@ -10,6 +10,7 @@
#include "BLI_bit_vector.hh"
#include "BLI_enumerable_thread_specific.hh"
#include "BLI_index_mask.hh"
#include "BLI_index_mask_expression.hh"
#include "BLI_math_base.hh"
#include "BLI_set.hh"
#include "BLI_sort.hh"
@ -211,15 +212,11 @@ IndexMask IndexMask::shift(const int64_t offset, IndexMaskMemory &memory) const
return shifted_mask;
}
/**
* Merges consecutive segments in some cases. Having fewer but larger segments generally allows for
* better performance when using the mask later on.
*/
static void consolidate_segments(Vector<IndexMaskSegment, 16> &segments,
IndexMaskMemory & /*memory*/)
int64_t consolidate_index_mask_segments(MutableSpan<IndexMaskSegment> segments,
IndexMaskMemory & /*memory*/)
{
if (segments.is_empty()) {
return;
return 0;
}
const Span<int16_t> static_indices = get_static_indices_array();
@ -268,7 +265,13 @@ static void consolidate_segments(Vector<IndexMaskSegment, 16> &segments,
finish_group(segments.size() - 1);
/* Remove all segments that have been merged into previous segments. */
segments.remove_if([](const IndexMaskSegment segment) { return segment.is_empty(); });
const int64_t new_segments_num = std::remove_if(segments.begin(),
segments.end(),
[](const IndexMaskSegment segment) {
return segment.is_empty();
}) -
segments.begin();
return new_segments_num;
}
IndexMask IndexMask::from_segments(const Span<IndexMaskSegment> segments, IndexMaskMemory &memory)
@ -389,162 +392,12 @@ struct ParallelSegmentsCollector {
}
};
/**
* Convert a range to potentially multiple index mask segments.
*/
static void range_to_segments(const IndexRange range, Vector<IndexMaskSegment, 16> &r_segments)
{
const Span<int16_t> static_indices = get_static_indices_array();
for (int64_t start = 0; start < range.size(); start += max_segment_size) {
const int64_t size = std::min(max_segment_size, range.size() - start);
r_segments.append_as(range.start() + start, static_indices.take_front(size));
}
}
static int64_t get_size_before_gap(const Span<int16_t> indices)
{
BLI_assert(indices.size() >= 2);
if (indices[1] > indices[0] + 1) {
/* For sparse indices, often the next gap is just after the next index.
* In this case we can skip the logarithmic check below. */
return 1;
}
return unique_sorted_indices::find_size_of_next_range(indices);
}
static void inverted_indices_to_segments(const IndexMaskSegment segment,
LinearAllocator<> &allocator,
Vector<IndexMaskSegment, 16> &r_segments)
{
constexpr int64_t range_threshold = 64;
const int64_t offset = segment.offset();
const Span<int16_t> static_indices = get_static_indices_array();
int64_t inverted_index_count = 0;
std::array<int16_t, max_segment_size> inverted_indices_array;
auto add_indices = [&](const int16_t start, const int16_t num) {
int16_t *new_indices_begin = inverted_indices_array.data() + inverted_index_count;
std::iota(new_indices_begin, new_indices_begin + num, start);
inverted_index_count += num;
};
auto finish_indices = [&]() {
if (inverted_index_count == 0) {
return;
}
MutableSpan<int16_t> offset_indices = allocator.allocate_array<int16_t>(inverted_index_count);
offset_indices.copy_from(Span(inverted_indices_array).take_front(inverted_index_count));
r_segments.append_as(offset, offset_indices);
inverted_index_count = 0;
};
Span<int16_t> indices = segment.base_span();
while (indices.size() > 1) {
const int64_t size_before_gap = get_size_before_gap(indices);
if (size_before_gap == indices.size()) {
break;
}
const int16_t gap_first = indices[size_before_gap - 1] + 1;
const int16_t next = indices[size_before_gap];
const int16_t gap_size = next - gap_first;
if (gap_size > range_threshold) {
finish_indices();
r_segments.append_as(offset + gap_first, static_indices.take_front(gap_size));
}
else {
add_indices(gap_first, gap_size);
}
indices = indices.drop_front(size_before_gap);
}
finish_indices();
}
static void invert_segments(const IndexMask &mask,
const IndexRange segment_range,
LinearAllocator<> &allocator,
Vector<IndexMaskSegment, 16> &r_segments)
{
for (const int64_t segment_i : segment_range) {
const IndexMaskSegment segment = mask.segment(segment_i);
inverted_indices_to_segments(segment, allocator, r_segments);
const IndexMaskSegment next_segment = mask.segment(segment_i + 1);
const int64_t between_start = segment.last() + 1;
const int64_t size_between_segments = next_segment[0] - segment.last() - 1;
const IndexRange range_between_segments(between_start, size_between_segments);
if (!range_between_segments.is_empty()) {
range_to_segments(range_between_segments, r_segments);
}
}
}
IndexMask IndexMask::complement(const IndexRange universe, IndexMaskMemory &memory) const
{
if (this->is_empty()) {
return universe;
}
if (universe.is_empty()) {
return {};
}
const std::optional<IndexRange> this_range = this->to_range();
if (this_range) {
const bool first_in_range = this_range->first() <= universe.first();
const bool last_in_range = this_range->last() >= universe.last();
if (first_in_range && last_in_range) {
/* This mask fills the entire universe, so the complement is empty. */
return {};
}
if (first_in_range) {
/* This mask is a range that contains the start of the universe.
* The complement is a range that contains the end of the universe. */
return IndexRange::from_begin_end(this_range->one_after_last(), universe.one_after_last());
}
if (last_in_range) {
/* This mask is a range that contains the end of the universe.
* The complement is a range that contains the start of the universe. */
return IndexRange::from_begin_end(universe.first(), this_range->first());
}
}
Vector<IndexMaskSegment, 16> segments;
if (universe.start() < this->first()) {
range_to_segments(universe.take_front(this->first() - universe.start()), segments);
}
if (!this_range) {
const int64_t segments_num = this->segments_num();
constexpr int64_t min_grain_size = 16;
constexpr int64_t max_grain_size = 4096;
const int64_t threads_num = BLI_system_thread_count();
const int64_t grain_size = std::clamp(
segments_num / threads_num, min_grain_size, max_grain_size);
const IndexRange non_last_segments = IndexRange(segments_num).drop_back(1);
if (segments_num < min_grain_size) {
invert_segments(*this, non_last_segments, memory, segments);
}
else {
ParallelSegmentsCollector segments_collector;
threading::parallel_for(non_last_segments, grain_size, [&](const IndexRange range) {
ParallelSegmentsCollector::LocalData &local_data =
segments_collector.data_by_thread.local();
invert_segments(*this, range, local_data.allocator, local_data.segments);
});
segments_collector.reduce(memory, segments);
}
inverted_indices_to_segments(this->segment(segments_num - 1), memory, segments);
}
if (universe.last() > this->first()) {
range_to_segments(universe.take_back(universe.last() - this->last()), segments);
}
return IndexMask::from_segments(segments, memory);
ExprBuilder builder;
const IndexMask universe_mask{universe};
const Expr &expr = builder.subtract(&universe_mask, {this});
return evaluate_expression(expr, memory);
}
template<typename T>
@ -580,7 +433,8 @@ IndexMask IndexMask::from_indices(const Span<T> indices, IndexMaskMemory &memory
});
segments_collector.reduce(memory, segments);
}
consolidate_segments(segments, memory);
const int64_t consolidated_segments_num = consolidate_index_mask_segments(segments, memory);
segments.resize(consolidated_segments_num);
return IndexMask::from_segments(segments, memory);
}
@ -636,13 +490,9 @@ IndexMask IndexMask::from_union(const IndexMask &mask_a,
const IndexMask &mask_b,
IndexMaskMemory &memory)
{
const int64_t new_size = math::max(mask_a.min_array_size(), mask_b.min_array_size());
Array<bool> tmp(new_size, false);
mask_a.foreach_index_optimized<int64_t>(GrainSize(2048),
[&](const int64_t i) { tmp[i] = true; });
mask_b.foreach_index_optimized<int64_t>(GrainSize(2048),
[&](const int64_t i) { tmp[i] = true; });
return IndexMask::from_bools(tmp, memory);
ExprBuilder builder;
const Expr &expr = builder.merge({&mask_a, &mask_b});
return evaluate_expression(expr, memory);
}
IndexMask IndexMask::from_initializers(const Span<Initializer> initializers,
@ -684,17 +534,20 @@ template<typename T> void IndexMask::to_indices(MutableSpan<T> r_indices) const
});
}
void IndexMask::to_bits(MutableBitSpan r_bits) const
void IndexMask::to_bits(MutableBitSpan r_bits, const int64_t offset) const
{
BLI_assert(r_bits.size() >= this->min_array_size());
BLI_assert(r_bits.size() >= this->min_array_size() + offset);
r_bits.reset_all();
this->foreach_segment_optimized([&](const auto segment) {
if constexpr (std::is_same_v<std::decay_t<decltype(segment)>, IndexRange>) {
const IndexRange range = segment;
r_bits.slice(range).set_all();
const IndexRange shifted_range = range.shift(offset);
r_bits.slice(shifted_range).set_all();
}
else {
for (const int64_t i : segment) {
const IndexMaskSegment indices = segment;
const IndexMaskSegment shifted_indices = indices.shift(offset);
for (const int64_t i : shifted_indices) {
r_bits[i].set();
}
}
@ -785,7 +638,8 @@ IndexMask from_predicate_impl(
segments_collector.reduce(memory, segments);
}
consolidate_segments(segments, memory);
const int64_t consolidated_segments_num = consolidate_index_mask_segments(segments, memory);
segments.resize(consolidated_segments_num);
return IndexMask::from_segments(segments, memory);
}
} // namespace detail

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,269 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#include "BLI_array.hh"
#include "BLI_index_mask_expression.hh"
#include "BLI_rand.hh"
#include "BLI_set.hh"
#include "BLI_strict_flags.h"
#include "BLI_timeit.hh"
#include "testing/testing.h"
namespace blender::index_mask::tests {
TEST(index_mask_expression, Union)
{
IndexMaskMemory memory;
const IndexMask mask_a = IndexMask::from_initializers({5, IndexRange(50, 100), 100'000}, memory);
const IndexMask mask_b = IndexMask::from_initializers({IndexRange(10, 10), 60, 200}, memory);
ExprBuilder builder;
const Expr &expr = builder.merge({&mask_a, &mask_b});
const IndexMask union_mask = evaluate_expression(expr, memory);
EXPECT_EQ(union_mask,
IndexMask::from_initializers(
{5, IndexRange(10, 10), IndexRange(50, 100), 200, 100'000}, memory));
}
TEST(index_mask_expression, UnionMulti)
{
IndexMaskMemory memory;
const IndexMask mask_a = IndexMask::from_initializers({3, 5, 6, 8, 9}, memory);
const IndexMask mask_b = IndexMask::from_initializers({4, 6, 7, 12}, memory);
const IndexMask mask_c = IndexMask::from_initializers({0, 5}, memory);
const IndexMask mask_d = IndexMask::from_initializers({6, 7, 10}, memory);
ExprBuilder builder;
const Expr &expr = builder.merge({&mask_a, &mask_b, &mask_c, &mask_d});
const IndexMask union_mask = evaluate_expression(expr, memory);
EXPECT_EQ(union_mask, IndexMask::from_initializers({0, 3, 4, 5, 6, 7, 8, 9, 10, 12}, memory));
}
TEST(index_mask_expression, IntersectMulti)
{
IndexMaskMemory memory;
const IndexMask mask_a = IndexMask::from_initializers({3, 5, 6, 8, 9}, memory);
const IndexMask mask_b = IndexMask::from_initializers({2, 5, 6, 10}, memory);
const IndexMask mask_c = IndexMask::from_initializers({4, 5, 6}, memory);
const IndexMask mask_d = IndexMask::from_initializers({1, 5, 10}, memory);
ExprBuilder builder;
const Expr &expr = builder.intersect({&mask_a, &mask_b, &mask_c, &mask_d});
const IndexMask intersect_mask = evaluate_expression(expr, memory);
EXPECT_EQ(intersect_mask, IndexMask::from_initializers({5}, memory));
}
TEST(index_mask_expression, DifferenceMulti)
{
IndexMaskMemory memory;
const IndexMask mask_a = IndexMask::from_initializers({1, 2, 3, 5, 6, 7, 9, 10}, memory);
const IndexMask mask_b = IndexMask::from_initializers({2, 5, 6, 10}, memory);
const IndexMask mask_c = IndexMask::from_initializers({4, 5, 6}, memory);
const IndexMask mask_d = IndexMask::from_initializers({1, 5, 10}, memory);
ExprBuilder builder;
const Expr &expr = builder.subtract(&mask_a, {&mask_b, &mask_c, &mask_d});
const IndexMask difference_mask = evaluate_expression(expr, memory);
EXPECT_EQ(difference_mask, IndexMask::from_initializers({3, 7, 9}, memory));
}
TEST(index_mask_expression, Intersection)
{
IndexMaskMemory memory;
const IndexMask mask_a = IndexMask::from_initializers({5, IndexRange(50, 100), 100'000}, memory);
const IndexMask mask_b = IndexMask::from_initializers(
{5, 6, IndexRange(100, 100), 80000, 100'000}, memory);
ExprBuilder builder;
const Expr &expr = builder.intersect({&mask_a, &mask_b});
const IndexMask intersection_mask = evaluate_expression(expr, memory);
EXPECT_EQ(intersection_mask,
IndexMask::from_initializers({5, IndexRange(100, 50), 100'000}, memory));
}
TEST(index_mask_expression, Difference)
{
IndexMaskMemory memory;
const IndexMask mask_a = IndexMask::from_initializers({5, IndexRange(50, 100), 100'000}, memory);
const IndexMask mask_b = IndexMask::from_initializers({5, 60, IndexRange(100, 20)}, memory);
ExprBuilder builder;
const Expr &expr = builder.subtract(&mask_a, {&mask_b});
const IndexMask difference_mask = evaluate_expression(expr, memory);
EXPECT_EQ(difference_mask,
IndexMask::from_initializers(
{IndexRange(50, 10), IndexRange(61, 39), IndexRange(120, 30), 100'000}, memory));
}
TEST(index_mask_expression, FizzBuzz)
{
IndexMaskMemory memory;
const IndexMask mask_3 = IndexMask::from_every_nth(3, 11, 0, memory); /* 0 - 30 */
const IndexMask mask_5 = IndexMask::from_every_nth(5, 11, 0, memory); /* 0 - 50 */
{
ExprBuilder builder;
const Expr &expr = builder.merge({&mask_3, &mask_5});
const IndexMask result = evaluate_expression(expr, memory);
EXPECT_EQ(
result,
IndexMask::from_initializers(
{0, 3, 5, 6, 9, 10, 12, 15, 18, 20, 21, 24, 25, 27, 30, 35, 40, 45, 50}, memory));
}
{
ExprBuilder builder;
const Expr &expr = builder.intersect({&mask_3, &mask_5});
const IndexMask result = evaluate_expression(expr, memory);
EXPECT_EQ(result, IndexMask::from_initializers({0, 15, 30}, memory));
}
{
ExprBuilder builder;
const Expr &expr = builder.subtract(&mask_3, {&mask_5});
const IndexMask result = evaluate_expression(expr, memory);
EXPECT_EQ(result, IndexMask::from_initializers({3, 6, 9, 12, 18, 21, 24, 27}, memory));
}
{
ExprBuilder builder;
const Expr &expr = builder.merge(
{&builder.intersect({&mask_3, &mask_5}), &builder.subtract(&mask_3, {&mask_5})});
const IndexMask &result = evaluate_expression(expr, memory);
EXPECT_EQ(result,
IndexMask::from_initializers({0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30}, memory));
}
}
TEST(index_mask_expression, UnionToFullRange)
{
IndexMaskMemory memory;
const IndexMask mask_1 = IndexMask::from_initializers({2, 4, 5, 7}, memory);
const IndexMask mask_2 = IndexMask::from_initializers({6, 8}, memory);
const IndexMask mask_3 = IndexMask::from_initializers({1, 3}, memory);
ExprBuilder builder;
const Expr &expr = builder.merge({&mask_1, &mask_2, &mask_3});
const IndexMask result = evaluate_expression(expr, memory);
EXPECT_TRUE(result.to_range().has_value());
EXPECT_EQ(*result.to_range(), IndexRange::from_begin_end_inclusive(1, 8));
EXPECT_EQ(result.segments_num(), 1);
}
TEST(index_mask_expression, UnionIndividualIndices)
{
IndexMaskMemory memory;
const IndexMask mask_1 = IndexMask::from_initializers({3}, memory);
const IndexMask mask_2 = IndexMask::from_initializers({6}, memory);
const IndexMask mask_3 = IndexMask::from_initializers({5}, memory);
ExprBuilder builder;
const Expr &expr = builder.merge({&mask_1, &mask_2, &mask_3});
const IndexMask result = evaluate_expression(expr, memory);
EXPECT_EQ(result, IndexMask::from_initializers({3, 5, 6}, memory));
EXPECT_EQ(result.segments_num(), 1);
}
TEST(index_mask_expression, UnionLargeRanges)
{
IndexMaskMemory memory;
const IndexMask mask_a(IndexRange(0, 1'000'000));
const IndexMask mask_b(IndexRange(900'000, 1'100'000));
ExprBuilder builder;
const Expr &expr = builder.merge({&mask_a, &mask_b});
const IndexMask result_mask = evaluate_expression(expr, memory);
EXPECT_EQ(result_mask, IndexMask(IndexRange(0, 2'000'000)));
}
TEST(index_mask_expression, SubtractSmall)
{
IndexMaskMemory memory;
const IndexMask mask_a = IndexMask::from_initializers({3, 4, 5, 6, 7, 8, 9}, memory);
const IndexMask mask_b = IndexMask::from_initializers({5, 7}, memory);
const IndexMask mask_c = IndexMask::from_initializers({8}, memory);
ExprBuilder builder;
const Expr &expr = builder.subtract(&mask_a, {&mask_b, &mask_c});
const IndexMask result = evaluate_expression(expr, memory);
EXPECT_EQ(result, IndexMask::from_initializers({3, 4, 6, 9}, memory));
EXPECT_EQ(result.segments_num(), 1);
}
TEST(index_mask_expression, RangeTerms)
{
IndexMaskMemory memory;
ExprBuilder builder;
const IndexRange range_a = IndexRange::from_begin_end(30'000, 50'000);
const IndexRange range_b = IndexRange::from_begin_end(40'000, 100'000);
const IndexRange range_c = IndexRange::from_begin_end(45'000, 48'000);
const Expr &expr = builder.subtract(&builder.merge({range_a, range_b}), {range_c});
const IndexMask result_mask = evaluate_expression(expr, memory);
EXPECT_EQ(result_mask,
IndexMask::from_initializers({IndexRange::from_begin_end(30'000, 45'000),
IndexRange::from_begin_end(48'000, 100'000)},
memory));
}
TEST(index_mask_expression, SingleMask)
{
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_initializers({5, 6, 8, 9}, memory);
ExprBuilder builder;
const Expr &expr = builder.merge({&mask});
const IndexMask result = evaluate_expression(expr, memory);
EXPECT_EQ(result, mask);
}
TEST(index_mask_expression, SubtractSelf)
{
IndexMaskMemory memory;
const IndexMask mask = IndexMask ::from_initializers({6, 8, 10, 100}, memory);
ExprBuilder builder;
const Expr &expr = builder.subtract(&mask, {&mask});
const IndexMask result = evaluate_expression(expr, memory);
EXPECT_TRUE(result.is_empty());
}
/* Disable benchmark by default. */
#if 0
TEST(index_mask_expression, Benchmark)
{
# ifdef NDEBUG
const int64_t iterations = 100;
# else
const int64_t iterations = 1;
# endif
for ([[maybe_unused]] const int64_t _1 : IndexRange(5)) {
IndexMaskMemory m;
const IndexMask a = IndexMask::from_every_nth(3, 1'000'000, 0, m);
const IndexMask b = IndexMask::from_every_nth(100, 5'000, 0, m);
ExprBuilder builder;
const Expr &expr = builder.merge({&a, &b});
SCOPED_TIMER("benchmark");
for ([[maybe_unused]] const int64_t _2 : IndexRange(iterations)) {
IndexMaskMemory memory;
const IndexMask result = evaluate_expression(expr, memory);
UNUSED_VARS(result);
}
}
}
#endif
} // namespace blender::index_mask::tests