Files
test2/source/blender/blenlib/tests/BLI_index_mask_expression_test.cc
Jacques Lucke ee1fa8e1ca 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
2024-03-17 09:52:32 +01:00

270 lines
9.4 KiB
C++

/* 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