Goals: * Better high level control over where devirtualization occurs. There is always a trade-off between performance and compile-time/binary-size. * Simplify using array devirtualization. * Better performance for cases where devirtualization wasn't used before. Many geometry nodes accept fields as inputs. Internally, that means that the execution functions have to accept so called "virtual arrays" as inputs. Those can be e.g. actual arrays, just single values, or lazily computed arrays. Due to these different possible virtual arrays implementations, access to individual elements is slower than it would be if everything was just a normal array (access does through a virtual function call). For more complex execution functions, this overhead does not matter, but for small functions (like a simple addition) it very much does. The virtual function call also prevents the compiler from doing some optimizations (e.g. loop unrolling and inserting simd instructions). The solution is to "devirtualize" the virtual arrays for small functions where the overhead is measurable. Essentially, the function is generated many times with different array types as input. Then there is a run-time dispatch that calls the best implementation. We have been doing devirtualization in e.g. math nodes for a long time already. This patch just generalizes the concept and makes it easier to control. It also makes it easier to investigate the different trade-offs when it comes to devirtualization. Nodes that we've optimized using devirtualization before didn't get a speedup. However, a couple of nodes are using devirtualization now, that didn't before. Those got a 2-4x speedup in common cases. * Map Range * Random Value * Switch * Combine XYZ Differential Revision: https://developer.blender.org/D14628
1217 lines
35 KiB
C++
1217 lines
35 KiB
C++
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
#pragma once
|
|
|
|
/** \file
|
|
* \ingroup bli
|
|
*
|
|
* A virtual array is a data structure that behaves similar to an array, but its elements are
|
|
* accessed through virtual methods. This improves the decoupling of a function from its callers,
|
|
* because it does not have to know exactly how the data is laid out in memory, or if it is stored
|
|
* in memory at all. It could just as well be computed on the fly.
|
|
*
|
|
* Taking a virtual array as parameter instead of a more specific non-virtual type has some
|
|
* tradeoffs. Access to individual elements of the individual elements is higher due to function
|
|
* call overhead. On the other hand, potential callers don't have to convert the data into the
|
|
* specific format required for the function. This can be a costly conversion if only few of the
|
|
* elements are accessed in the end.
|
|
*
|
|
* Functions taking a virtual array as input can still optimize for different data layouts. For
|
|
* example, they can check if the array is stored as an array internally or if it is the same
|
|
* element for all indices. Whether it is worth to optimize for different data layouts in a
|
|
* function has to be decided on a case by case basis. One should always do some benchmarking to
|
|
* see of the increased compile time and binary size is worth it.
|
|
*/
|
|
|
|
#include "BLI_any.hh"
|
|
#include "BLI_array.hh"
|
|
#include "BLI_index_mask.hh"
|
|
#include "BLI_span.hh"
|
|
|
|
namespace blender {
|
|
|
|
/** Forward declarations for generic virtual arrays. */
|
|
class GVArray;
|
|
class GVMutableArray;
|
|
|
|
/**
|
|
* Implements the specifics of how the elements of a virtual array are accessed. It contains a
|
|
* bunch of virtual methods that are wrapped by #VArray.
|
|
*/
|
|
template<typename T> class VArrayImpl {
|
|
protected:
|
|
/**
|
|
* Number of elements in the virtual array. All virtual arrays have a size, but in some cases it
|
|
* may make sense to set it to the max value.
|
|
*/
|
|
int64_t size_;
|
|
|
|
public:
|
|
VArrayImpl(const int64_t size) : size_(size)
|
|
{
|
|
BLI_assert(size_ >= 0);
|
|
}
|
|
|
|
virtual ~VArrayImpl() = default;
|
|
|
|
int64_t size() const
|
|
{
|
|
return size_;
|
|
}
|
|
|
|
/**
|
|
* Get the element at #index. This does not return a reference, because the value may be computed
|
|
* on the fly.
|
|
*/
|
|
virtual T get(int64_t index) const = 0;
|
|
|
|
/**
|
|
* Return true when the virtual array is a plain array internally.
|
|
*/
|
|
virtual bool is_span() const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return the span of the virtual array.
|
|
* This invokes undefined behavior when #is_span returned false.
|
|
*/
|
|
virtual Span<T> get_internal_span() const
|
|
{
|
|
/* Provide a default implementation, so that subclasses don't have to provide it. This method
|
|
* should never be called because #is_span returns false by default. */
|
|
BLI_assert_unreachable();
|
|
return {};
|
|
}
|
|
|
|
/**
|
|
* Return true when the virtual array has the same value at every index.
|
|
*/
|
|
virtual bool is_single() const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return the value that is used at every index.
|
|
* This invokes undefined behavior when #is_single returned false.
|
|
*/
|
|
virtual T get_internal_single() const
|
|
{
|
|
/* Provide a default implementation, so that subclasses don't have to provide it. This method
|
|
* should never be called because #is_single returns false by default. */
|
|
BLI_assert_unreachable();
|
|
return T();
|
|
}
|
|
|
|
/**
|
|
* Copy values from the virtual array into the provided span. The index of the value in the
|
|
* virtual array is the same as the index in the span.
|
|
*/
|
|
virtual void materialize(IndexMask mask, MutableSpan<T> r_span) const
|
|
{
|
|
T *dst = r_span.data();
|
|
/* Optimize for a few different common cases. */
|
|
if (this->is_span()) {
|
|
const T *src = this->get_internal_span().data();
|
|
mask.foreach_index([&](const int64_t i) { dst[i] = src[i]; });
|
|
}
|
|
else if (this->is_single()) {
|
|
const T single = this->get_internal_single();
|
|
mask.foreach_index([&](const int64_t i) { dst[i] = single; });
|
|
}
|
|
else {
|
|
mask.foreach_index([&](const int64_t i) { dst[i] = this->get(i); });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Same as #materialize but #r_span is expected to be uninitialized.
|
|
*/
|
|
virtual void materialize_to_uninitialized(IndexMask mask, MutableSpan<T> r_span) const
|
|
{
|
|
T *dst = r_span.data();
|
|
/* Optimize for a few different common cases. */
|
|
if (this->is_span()) {
|
|
const T *src = this->get_internal_span().data();
|
|
mask.foreach_index([&](const int64_t i) { new (dst + i) T(src[i]); });
|
|
}
|
|
else if (this->is_single()) {
|
|
const T single = this->get_internal_single();
|
|
mask.foreach_index([&](const int64_t i) { new (dst + i) T(single); });
|
|
}
|
|
else {
|
|
mask.foreach_index([&](const int64_t i) { new (dst + i) T(this->get(i)); });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copy values from the virtual array into the provided span. Contrary to #materialize, the index
|
|
* in virtual array is not the same as the index in the output span. Instead, the span is filled
|
|
* without gaps.
|
|
*/
|
|
virtual void materialize_compressed(IndexMask mask, MutableSpan<T> r_span) const
|
|
{
|
|
BLI_assert(mask.size() == r_span.size());
|
|
mask.to_best_mask_type([&](auto best_mask) {
|
|
for (const int64_t i : IndexRange(best_mask.size())) {
|
|
r_span[i] = this->get(best_mask[i]);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Same as #materialize_compressed but #r_span is expected to be uninitialized.
|
|
*/
|
|
virtual void materialize_compressed_to_uninitialized(IndexMask mask, MutableSpan<T> r_span) const
|
|
{
|
|
BLI_assert(mask.size() == r_span.size());
|
|
T *dst = r_span.data();
|
|
mask.to_best_mask_type([&](auto best_mask) {
|
|
for (const int64_t i : IndexRange(best_mask.size())) {
|
|
new (dst + i) T(this->get(best_mask[i]));
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* If this virtual wraps another #GVArray, this method should assign the wrapped array to the
|
|
* provided reference. This allows losslessly converting between generic and typed virtual
|
|
* arrays in all cases.
|
|
* Return true when the virtual array was assigned and false when nothing was done.
|
|
*/
|
|
virtual bool try_assign_GVArray(GVArray &UNUSED(varray)) const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return true when this virtual array may own any of the memory it references. This can be used
|
|
* for optimization purposes when converting or copying the virtual array.
|
|
*/
|
|
virtual bool may_have_ownership() const
|
|
{
|
|
/* Use true by default to be on the safe side. Subclasses that know for sure that they don't
|
|
* own anything can overwrite this with false. */
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Return true when the other virtual array should be considered to be the same, e.g. because it
|
|
* shares the same underlying memory.
|
|
*/
|
|
virtual bool is_same(const VArrayImpl<T> &UNUSED(other)) const
|
|
{
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/** Similar to #VArrayImpl, but adds methods that allow modifying the referenced elements. */
|
|
template<typename T> class VMutableArrayImpl : public VArrayImpl<T> {
|
|
public:
|
|
using VArrayImpl<T>::VArrayImpl;
|
|
|
|
/**
|
|
* Assign the provided #value to the #index.
|
|
*/
|
|
virtual void set(int64_t index, T value) = 0;
|
|
|
|
/**
|
|
* Copy all elements from the provided span into the virtual array.
|
|
*/
|
|
virtual void set_all(Span<T> src)
|
|
{
|
|
if (this->is_span()) {
|
|
const Span<T> const_span = this->get_internal_span();
|
|
const MutableSpan<T> span{(T *)const_span.data(), const_span.size()};
|
|
initialized_copy_n(src.data(), this->size_, span.data());
|
|
}
|
|
else {
|
|
const int64_t size = this->size_;
|
|
for (int64_t i = 0; i < size; i++) {
|
|
this->set(i, src[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Similar to #VArrayImpl::try_assign_GVArray but for mutable virtual arrays.
|
|
*/
|
|
virtual bool try_assign_GVMutableArray(GVMutableArray &UNUSED(varray)) const
|
|
{
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A virtual array implementation that references that wraps a span. This implementation is used by
|
|
* mutable and immutable spans to avoid code duplication.
|
|
*/
|
|
template<typename T> class VArrayImpl_For_Span : public VMutableArrayImpl<T> {
|
|
protected:
|
|
T *data_ = nullptr;
|
|
|
|
public:
|
|
VArrayImpl_For_Span(const MutableSpan<T> data)
|
|
: VMutableArrayImpl<T>(data.size()), data_(data.data())
|
|
{
|
|
}
|
|
|
|
protected:
|
|
VArrayImpl_For_Span(const int64_t size) : VMutableArrayImpl<T>(size)
|
|
{
|
|
}
|
|
|
|
T get(const int64_t index) const final
|
|
{
|
|
return data_[index];
|
|
}
|
|
|
|
void set(const int64_t index, T value) final
|
|
{
|
|
data_[index] = value;
|
|
}
|
|
|
|
bool is_span() const override
|
|
{
|
|
return true;
|
|
}
|
|
|
|
Span<T> get_internal_span() const override
|
|
{
|
|
return Span<T>(data_, this->size_);
|
|
}
|
|
|
|
bool is_same(const VArrayImpl<T> &other) const final
|
|
{
|
|
if (other.size() != this->size_) {
|
|
return false;
|
|
}
|
|
if (!other.is_span()) {
|
|
return false;
|
|
}
|
|
const Span<T> other_span = other.get_internal_span();
|
|
return data_ == other_span.data();
|
|
}
|
|
|
|
void materialize_compressed(IndexMask mask, MutableSpan<T> r_span) const override
|
|
{
|
|
mask.to_best_mask_type([&](auto best_mask) {
|
|
for (const int64_t i : IndexRange(best_mask.size())) {
|
|
r_span[i] = data_[best_mask[i]];
|
|
}
|
|
});
|
|
}
|
|
|
|
void materialize_compressed_to_uninitialized(IndexMask mask,
|
|
MutableSpan<T> r_span) const override
|
|
{
|
|
T *dst = r_span.data();
|
|
mask.to_best_mask_type([&](auto best_mask) {
|
|
for (const int64_t i : IndexRange(best_mask.size())) {
|
|
new (dst + i) T(data_[best_mask[i]]);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A version of #VArrayImpl_For_Span that can not be subclassed. This allows safely overwriting the
|
|
* #may_have_ownership method.
|
|
*/
|
|
template<typename T> class VArrayImpl_For_Span_final final : public VArrayImpl_For_Span<T> {
|
|
public:
|
|
using VArrayImpl_For_Span<T>::VArrayImpl_For_Span;
|
|
|
|
private:
|
|
bool may_have_ownership() const override
|
|
{
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A variant of `VArrayImpl_For_Span` that owns the underlying data.
|
|
* The `Container` type has to implement a `size()` and `data()` method.
|
|
* The `data()` method has to return a pointer to the first element in the continuous array of
|
|
* elements.
|
|
*/
|
|
template<typename Container, typename T = typename Container::value_type>
|
|
class VArrayImpl_For_ArrayContainer : public VArrayImpl_For_Span<T> {
|
|
private:
|
|
Container container_;
|
|
|
|
public:
|
|
VArrayImpl_For_ArrayContainer(Container container)
|
|
: VArrayImpl_For_Span<T>((int64_t)container.size()), container_(std::move(container))
|
|
{
|
|
this->data_ = const_cast<T *>(container_.data());
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A virtual array implementation that returns the same value for every index. This class is final
|
|
* so that it can be devirtualized by the compiler in some cases (e.g. when #devirtualize_varray is
|
|
* used).
|
|
*/
|
|
template<typename T> class VArrayImpl_For_Single final : public VArrayImpl<T> {
|
|
private:
|
|
T value_;
|
|
|
|
public:
|
|
VArrayImpl_For_Single(T value, const int64_t size)
|
|
: VArrayImpl<T>(size), value_(std::move(value))
|
|
{
|
|
}
|
|
|
|
protected:
|
|
T get(const int64_t UNUSED(index)) const override
|
|
{
|
|
return value_;
|
|
}
|
|
|
|
bool is_span() const override
|
|
{
|
|
return this->size_ == 1;
|
|
}
|
|
|
|
Span<T> get_internal_span() const override
|
|
{
|
|
return Span<T>(&value_, 1);
|
|
}
|
|
|
|
bool is_single() const override
|
|
{
|
|
return true;
|
|
}
|
|
|
|
T get_internal_single() const override
|
|
{
|
|
return value_;
|
|
}
|
|
|
|
void materialize_compressed(IndexMask mask, MutableSpan<T> r_span) const override
|
|
{
|
|
BLI_assert(mask.size() == r_span.size());
|
|
UNUSED_VARS_NDEBUG(mask);
|
|
r_span.fill(value_);
|
|
}
|
|
|
|
void materialize_compressed_to_uninitialized(IndexMask mask,
|
|
MutableSpan<T> r_span) const override
|
|
{
|
|
BLI_assert(mask.size() == r_span.size());
|
|
uninitialized_fill_n(r_span.data(), mask.size(), value_);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This class makes it easy to create a virtual array for an existing function or lambda. The
|
|
* `GetFunc` should take a single `index` argument and return the value at that index.
|
|
*/
|
|
template<typename T, typename GetFunc> class VArrayImpl_For_Func final : public VArrayImpl<T> {
|
|
private:
|
|
GetFunc get_func_;
|
|
|
|
public:
|
|
VArrayImpl_For_Func(const int64_t size, GetFunc get_func)
|
|
: VArrayImpl<T>(size), get_func_(std::move(get_func))
|
|
{
|
|
}
|
|
|
|
private:
|
|
T get(const int64_t index) const override
|
|
{
|
|
return get_func_(index);
|
|
}
|
|
|
|
void materialize(IndexMask mask, MutableSpan<T> r_span) const override
|
|
{
|
|
T *dst = r_span.data();
|
|
mask.foreach_index([&](const int64_t i) { dst[i] = get_func_(i); });
|
|
}
|
|
|
|
void materialize_to_uninitialized(IndexMask mask, MutableSpan<T> r_span) const override
|
|
{
|
|
T *dst = r_span.data();
|
|
mask.foreach_index([&](const int64_t i) { new (dst + i) T(get_func_(i)); });
|
|
}
|
|
|
|
void materialize_compressed(IndexMask mask, MutableSpan<T> r_span) const override
|
|
{
|
|
BLI_assert(mask.size() == r_span.size());
|
|
T *dst = r_span.data();
|
|
mask.to_best_mask_type([&](auto best_mask) {
|
|
for (const int64_t i : IndexRange(best_mask.size())) {
|
|
dst[i] = get_func_(best_mask[i]);
|
|
}
|
|
});
|
|
}
|
|
|
|
void materialize_compressed_to_uninitialized(IndexMask mask,
|
|
MutableSpan<T> r_span) const override
|
|
{
|
|
BLI_assert(mask.size() == r_span.size());
|
|
T *dst = r_span.data();
|
|
mask.to_best_mask_type([&](auto best_mask) {
|
|
for (const int64_t i : IndexRange(best_mask.size())) {
|
|
new (dst + i) T(get_func_(best_mask[i]));
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* \note: This is `final` so that #may_have_ownership can be implemented reliably.
|
|
*/
|
|
template<typename StructT,
|
|
typename ElemT,
|
|
ElemT (*GetFunc)(const StructT &),
|
|
void (*SetFunc)(StructT &, ElemT) = nullptr>
|
|
class VArrayImpl_For_DerivedSpan final : public VMutableArrayImpl<ElemT> {
|
|
private:
|
|
StructT *data_;
|
|
|
|
public:
|
|
VArrayImpl_For_DerivedSpan(const MutableSpan<StructT> data)
|
|
: VMutableArrayImpl<ElemT>(data.size()), data_(data.data())
|
|
{
|
|
}
|
|
|
|
template<typename OtherStructT,
|
|
typename OtherElemT,
|
|
OtherElemT (*OtherGetFunc)(const OtherStructT &),
|
|
void (*OtherSetFunc)(OtherStructT &, OtherElemT)>
|
|
friend class VArrayImpl_For_DerivedSpan;
|
|
|
|
private:
|
|
ElemT get(const int64_t index) const override
|
|
{
|
|
return GetFunc(data_[index]);
|
|
}
|
|
|
|
void set(const int64_t index, ElemT value) override
|
|
{
|
|
SetFunc(data_[index], std::move(value));
|
|
}
|
|
|
|
void materialize(IndexMask mask, MutableSpan<ElemT> r_span) const override
|
|
{
|
|
ElemT *dst = r_span.data();
|
|
mask.foreach_index([&](const int64_t i) { dst[i] = GetFunc(data_[i]); });
|
|
}
|
|
|
|
void materialize_to_uninitialized(IndexMask mask, MutableSpan<ElemT> r_span) const override
|
|
{
|
|
ElemT *dst = r_span.data();
|
|
mask.foreach_index([&](const int64_t i) { new (dst + i) ElemT(GetFunc(data_[i])); });
|
|
}
|
|
|
|
void materialize_compressed(IndexMask mask, MutableSpan<ElemT> r_span) const override
|
|
{
|
|
BLI_assert(mask.size() == r_span.size());
|
|
ElemT *dst = r_span.data();
|
|
mask.to_best_mask_type([&](auto best_mask) {
|
|
for (const int64_t i : IndexRange(best_mask.size())) {
|
|
dst[i] = GetFunc(data_[best_mask[i]]);
|
|
}
|
|
});
|
|
}
|
|
|
|
void materialize_compressed_to_uninitialized(IndexMask mask,
|
|
MutableSpan<ElemT> r_span) const override
|
|
{
|
|
BLI_assert(mask.size() == r_span.size());
|
|
ElemT *dst = r_span.data();
|
|
mask.to_best_mask_type([&](auto best_mask) {
|
|
for (const int64_t i : IndexRange(best_mask.size())) {
|
|
new (dst + i) ElemT(GetFunc(data_[best_mask[i]]));
|
|
}
|
|
});
|
|
}
|
|
|
|
bool may_have_ownership() const override
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool is_same(const VArrayImpl<ElemT> &other) const override
|
|
{
|
|
if (other.size() != this->size_) {
|
|
return false;
|
|
}
|
|
if (const VArrayImpl_For_DerivedSpan<StructT, ElemT, GetFunc> *other_typed =
|
|
dynamic_cast<const VArrayImpl_For_DerivedSpan<StructT, ElemT, GetFunc> *>(&other)) {
|
|
return other_typed->data_ == data_;
|
|
}
|
|
if (const VArrayImpl_For_DerivedSpan<StructT, ElemT, GetFunc, SetFunc> *other_typed =
|
|
dynamic_cast<const VArrayImpl_For_DerivedSpan<StructT, ElemT, GetFunc, SetFunc> *>(
|
|
&other)) {
|
|
return other_typed->data_ == data_;
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
namespace detail {
|
|
|
|
/**
|
|
* Struct that can be passed as `ExtraInfo` into an #Any.
|
|
* This struct is only intended to be used by #VArrayCommon.
|
|
*/
|
|
template<typename T> struct VArrayAnyExtraInfo {
|
|
/**
|
|
* Gets the virtual array that is stored at the given pointer.
|
|
*/
|
|
const VArrayImpl<T> *(*get_varray)(const void *buffer);
|
|
|
|
template<typename StorageT> static constexpr VArrayAnyExtraInfo get()
|
|
{
|
|
/* These are the only allowed types in the #Any. */
|
|
static_assert(
|
|
std::is_base_of_v<VArrayImpl<T>, StorageT> ||
|
|
is_same_any_v<StorageT, const VArrayImpl<T> *, std::shared_ptr<const VArrayImpl<T>>>);
|
|
|
|
/* Depending on how the virtual array implementation is stored in the #Any, a different
|
|
* #get_varray function is required. */
|
|
if constexpr (std::is_base_of_v<VArrayImpl<T>, StorageT>) {
|
|
return {[](const void *buffer) {
|
|
return static_cast<const VArrayImpl<T> *>((const StorageT *)buffer);
|
|
}};
|
|
}
|
|
else if constexpr (std::is_same_v<StorageT, const VArrayImpl<T> *>) {
|
|
return {[](const void *buffer) { return *(const StorageT *)buffer; }};
|
|
}
|
|
else if constexpr (std::is_same_v<StorageT, std::shared_ptr<const VArrayImpl<T>>>) {
|
|
return {[](const void *buffer) { return ((const StorageT *)buffer)->get(); }};
|
|
}
|
|
else {
|
|
BLI_assert_unreachable();
|
|
return {};
|
|
}
|
|
}
|
|
};
|
|
|
|
} // namespace detail
|
|
|
|
/**
|
|
* Utility class to reduce code duplication for methods available on #VArray and #VMutableArray.
|
|
* Deriving #VMutableArray from #VArray would have some issues:
|
|
* - Static methods on #VArray would also be available on #VMutableArray.
|
|
* - It would allow assigning a #VArray to a #VMutableArray under some circumstances which is not
|
|
* allowed and could result in hard to find bugs.
|
|
*/
|
|
template<typename T> class VArrayCommon {
|
|
protected:
|
|
/**
|
|
* Store the virtual array implementation in an #Any. This makes it easy to avoid a memory
|
|
* allocation if the implementation is small enough and is copyable. This is the case for the
|
|
* most common virtual arrays.
|
|
* Other virtual array implementations are typically stored as #std::shared_ptr. That works even
|
|
* when the implementation itself is not copyable and makes copying #VArrayCommon cheaper.
|
|
*/
|
|
using Storage = Any<detail::VArrayAnyExtraInfo<T>, 24, 8>;
|
|
|
|
/**
|
|
* Pointer to the currently contained virtual array implementation. This is allowed to be null.
|
|
*/
|
|
const VArrayImpl<T> *impl_ = nullptr;
|
|
/**
|
|
* Does the memory management for the virtual array implementation. It contains one of the
|
|
* following:
|
|
* - Inlined subclass of #VArrayImpl.
|
|
* - Non-owning pointer to a #VArrayImpl.
|
|
* - Shared pointer to a #VArrayImpl.
|
|
*/
|
|
Storage storage_;
|
|
|
|
protected:
|
|
VArrayCommon() = default;
|
|
|
|
/** Copy constructor. */
|
|
VArrayCommon(const VArrayCommon &other) : storage_(other.storage_)
|
|
{
|
|
impl_ = this->impl_from_storage();
|
|
}
|
|
|
|
/** Move constructor. */
|
|
VArrayCommon(VArrayCommon &&other) noexcept : storage_(std::move(other.storage_))
|
|
{
|
|
impl_ = this->impl_from_storage();
|
|
other.storage_.reset();
|
|
other.impl_ = nullptr;
|
|
}
|
|
|
|
/**
|
|
* Wrap an existing #VArrayImpl and don't take ownership of it. This should rarely be used in
|
|
* practice.
|
|
*/
|
|
VArrayCommon(const VArrayImpl<T> *impl) : impl_(impl)
|
|
{
|
|
storage_ = impl_;
|
|
}
|
|
|
|
/**
|
|
* Wrap an existing #VArrayImpl that is contained in a #std::shared_ptr. This takes ownership.
|
|
*/
|
|
VArrayCommon(std::shared_ptr<const VArrayImpl<T>> impl) : impl_(impl.get())
|
|
{
|
|
if (impl) {
|
|
storage_ = std::move(impl);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Replace the contained #VArrayImpl.
|
|
*/
|
|
template<typename ImplT, typename... Args> void emplace(Args &&...args)
|
|
{
|
|
/* Make sure we are actually constructing a #VArrayImpl. */
|
|
static_assert(std::is_base_of_v<VArrayImpl<T>, ImplT>);
|
|
if constexpr (std::is_copy_constructible_v<ImplT> && Storage::template is_inline_v<ImplT>) {
|
|
/* Only inline the implementation when it is copyable and when it fits into the inline
|
|
* buffer of the storage. */
|
|
impl_ = &storage_.template emplace<ImplT>(std::forward<Args>(args)...);
|
|
}
|
|
else {
|
|
/* If it can't be inlined, create a new #std::shared_ptr instead and store that in the
|
|
* storage. */
|
|
std::shared_ptr<const VArrayImpl<T>> ptr = std::make_shared<ImplT>(
|
|
std::forward<Args>(args)...);
|
|
impl_ = &*ptr;
|
|
storage_ = std::move(ptr);
|
|
}
|
|
}
|
|
|
|
/** Utility to implement a copy assignment operator in a subclass. */
|
|
void copy_from(const VArrayCommon &other)
|
|
{
|
|
if (this == &other) {
|
|
return;
|
|
}
|
|
storage_ = other.storage_;
|
|
impl_ = this->impl_from_storage();
|
|
}
|
|
|
|
/** Utility to implement a move assignment operator in a subclass. */
|
|
void move_from(VArrayCommon &&other) noexcept
|
|
{
|
|
if (this == &other) {
|
|
return;
|
|
}
|
|
storage_ = std::move(other.storage_);
|
|
impl_ = this->impl_from_storage();
|
|
other.storage_.reset();
|
|
other.impl_ = nullptr;
|
|
}
|
|
|
|
/** Get a pointer to the virtual array implementation that is currently stored in #storage_, or
|
|
* null. */
|
|
const VArrayImpl<T> *impl_from_storage() const
|
|
{
|
|
if (!storage_.has_value()) {
|
|
return nullptr;
|
|
}
|
|
return storage_.extra_info().get_varray(storage_.get());
|
|
}
|
|
|
|
public:
|
|
/** Return false when there is no virtual array implementation currently. */
|
|
operator bool() const
|
|
{
|
|
return impl_ != nullptr;
|
|
}
|
|
|
|
/**
|
|
* Get the element at a specific index.
|
|
* \note: This can't return a reference because the value may be computed on the fly. This also
|
|
* implies that one can not use this method for assignments.
|
|
*/
|
|
T operator[](const int64_t index) const
|
|
{
|
|
BLI_assert(*this);
|
|
BLI_assert(index >= 0);
|
|
BLI_assert(index < this->size());
|
|
return impl_->get(index);
|
|
}
|
|
|
|
/**
|
|
* Same as the #operator[] but is sometimes easier to use when one has a pointer to a virtual
|
|
* array.
|
|
*/
|
|
T get(const int64_t index) const
|
|
{
|
|
return (*this)[index];
|
|
}
|
|
|
|
/**
|
|
* Return the size of the virtual array. It's allowed to call this method even when there is no
|
|
* virtual array. In this case 0 is returned.
|
|
*/
|
|
int64_t size() const
|
|
{
|
|
if (impl_ == nullptr) {
|
|
return 0;
|
|
}
|
|
return impl_->size();
|
|
}
|
|
|
|
/** True when the size is zero or when there is no virtual array. */
|
|
bool is_empty() const
|
|
{
|
|
return this->size() == 0;
|
|
}
|
|
|
|
IndexRange index_range() const
|
|
{
|
|
return IndexRange(this->size());
|
|
}
|
|
|
|
/** Return true when the virtual array is stored as a span internally. */
|
|
bool is_span() const
|
|
{
|
|
BLI_assert(*this);
|
|
return impl_->is_span();
|
|
}
|
|
|
|
/**
|
|
* Returns the internally used span of the virtual array. This invokes undefined behavior if the
|
|
* virtual array is not stored as a span internally.
|
|
*/
|
|
Span<T> get_internal_span() const
|
|
{
|
|
BLI_assert(this->is_span());
|
|
return impl_->get_internal_span();
|
|
}
|
|
|
|
/** Return true when the virtual array returns the same value for every index. */
|
|
bool is_single() const
|
|
{
|
|
BLI_assert(*this);
|
|
return impl_->is_single();
|
|
}
|
|
|
|
/**
|
|
* Return the value that is returned for every index. This invokes undefined behavior if the
|
|
* virtual array would not return the same value for every index.
|
|
*/
|
|
T get_internal_single() const
|
|
{
|
|
BLI_assert(this->is_single());
|
|
return impl_->get_internal_single();
|
|
}
|
|
|
|
/**
|
|
* Return true when the other virtual references the same underlying memory.
|
|
*/
|
|
bool is_same(const VArrayCommon<T> &other) const
|
|
{
|
|
if (!*this || !other) {
|
|
return false;
|
|
}
|
|
/* Check in both directions in case one does not know how to compare to the other
|
|
* implementation. */
|
|
if (impl_->is_same(*other.impl_)) {
|
|
return true;
|
|
}
|
|
if (other.impl_->is_same(*impl_)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Copy the entire virtual array into a span. */
|
|
void materialize(MutableSpan<T> r_span) const
|
|
{
|
|
this->materialize(IndexMask(this->size()), r_span);
|
|
}
|
|
|
|
/** Copy some indices of the virtual array into a span. */
|
|
void materialize(IndexMask mask, MutableSpan<T> r_span) const
|
|
{
|
|
BLI_assert(mask.min_array_size() <= this->size());
|
|
impl_->materialize(mask, r_span);
|
|
}
|
|
|
|
void materialize_to_uninitialized(MutableSpan<T> r_span) const
|
|
{
|
|
this->materialize_to_uninitialized(IndexMask(this->size()), r_span);
|
|
}
|
|
|
|
void materialize_to_uninitialized(IndexMask mask, MutableSpan<T> r_span) const
|
|
{
|
|
BLI_assert(mask.min_array_size() <= this->size());
|
|
impl_->materialize_to_uninitialized(mask, r_span);
|
|
}
|
|
|
|
/** Copy some elements of the virtual array into a span. */
|
|
void materialize_compressed(IndexMask mask, MutableSpan<T> r_span) const
|
|
{
|
|
impl_->materialize_compressed(mask, r_span);
|
|
}
|
|
|
|
void materialize_compressed_to_uninitialized(IndexMask mask, MutableSpan<T> r_span) const
|
|
{
|
|
impl_->materialize_compressed_to_uninitialized(mask, r_span);
|
|
}
|
|
|
|
/** See #GVArrayImpl::try_assign_GVArray. */
|
|
bool try_assign_GVArray(GVArray &varray) const
|
|
{
|
|
return impl_->try_assign_GVArray(varray);
|
|
}
|
|
|
|
/** See #GVArrayImpl::may_have_ownership. */
|
|
bool may_have_ownership() const
|
|
{
|
|
return impl_->may_have_ownership();
|
|
}
|
|
};
|
|
|
|
template<typename T> class VMutableArray;
|
|
|
|
/**
|
|
* A #VArray wraps a virtual array implementation and provides easy access to its elements. It can
|
|
* be copied and moved. While it is relatively small, it should still be passed by reference if
|
|
* possible (other than e.g. #Span).
|
|
*/
|
|
template<typename T> class VArray : public VArrayCommon<T> {
|
|
friend VMutableArray<T>;
|
|
|
|
public:
|
|
VArray() = default;
|
|
VArray(const VArray &other) = default;
|
|
VArray(VArray &&other) noexcept = default;
|
|
|
|
VArray(const VArrayImpl<T> *impl) : VArrayCommon<T>(impl)
|
|
{
|
|
}
|
|
|
|
VArray(std::shared_ptr<const VArrayImpl<T>> impl) : VArrayCommon<T>(std::move(impl))
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Construct a new virtual array for a custom #VArrayImpl.
|
|
*/
|
|
template<typename ImplT, typename... Args> static VArray For(Args &&...args)
|
|
{
|
|
static_assert(std::is_base_of_v<VArrayImpl<T>, ImplT>);
|
|
VArray varray;
|
|
varray.template emplace<ImplT>(std::forward<Args>(args)...);
|
|
return varray;
|
|
}
|
|
|
|
/**
|
|
* Construct a new virtual array that has the same value at every index.
|
|
*/
|
|
static VArray ForSingle(T value, const int64_t size)
|
|
{
|
|
return VArray::For<VArrayImpl_For_Single<T>>(std::move(value), size);
|
|
}
|
|
|
|
/**
|
|
* Construct a new virtual array for an existing span. This does not take ownership of the
|
|
* underlying memory.
|
|
*/
|
|
static VArray ForSpan(Span<T> values)
|
|
{
|
|
/* Cast const away, because the virtual array implementation for const and non const spans is
|
|
* shared. */
|
|
MutableSpan<T> span{const_cast<T *>(values.data()), values.size()};
|
|
return VArray::For<VArrayImpl_For_Span_final<T>>(span);
|
|
}
|
|
|
|
/**
|
|
* Construct a new virtual that will invoke the provided function whenever an element is
|
|
* accessed.
|
|
*/
|
|
template<typename GetFunc> static VArray ForFunc(const int64_t size, GetFunc get_func)
|
|
{
|
|
return VArray::For<VArrayImpl_For_Func<T, decltype(get_func)>>(size, std::move(get_func));
|
|
}
|
|
|
|
/**
|
|
* Construct a new virtual array for an existing span with a mapping function. This does not take
|
|
* ownership of the span.
|
|
*/
|
|
template<typename StructT, T (*GetFunc)(const StructT &)>
|
|
static VArray ForDerivedSpan(Span<StructT> values)
|
|
{
|
|
/* Cast const away, because the virtual array implementation for const and non const derived
|
|
* spans is shared. */
|
|
MutableSpan<StructT> span{const_cast<StructT *>(values.data()), values.size()};
|
|
return VArray::For<VArrayImpl_For_DerivedSpan<StructT, T, GetFunc>>(span);
|
|
}
|
|
|
|
/**
|
|
* Construct a new virtual array for an existing container. Every container that lays out the
|
|
* elements in a plain array works. This takes ownership of the passed in container. If that is
|
|
* not desired, use #ForSpan instead.
|
|
*/
|
|
template<typename ContainerT> static VArray ForContainer(ContainerT container)
|
|
{
|
|
return VArray::For<VArrayImpl_For_ArrayContainer<ContainerT>>(std::move(container));
|
|
}
|
|
|
|
VArray &operator=(const VArray &other)
|
|
{
|
|
this->copy_from(other);
|
|
return *this;
|
|
}
|
|
|
|
VArray &operator=(VArray &&other) noexcept
|
|
{
|
|
this->move_from(std::move(other));
|
|
return *this;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Similar to #VArray but references a virtual array that can be modified.
|
|
*/
|
|
template<typename T> class VMutableArray : public VArrayCommon<T> {
|
|
public:
|
|
VMutableArray() = default;
|
|
VMutableArray(const VMutableArray &other) = default;
|
|
VMutableArray(VMutableArray &&other) noexcept = default;
|
|
|
|
VMutableArray(const VMutableArrayImpl<T> *impl) : VArrayCommon<T>(impl)
|
|
{
|
|
}
|
|
|
|
VMutableArray(std::shared_ptr<const VMutableArrayImpl<T>> impl)
|
|
: VArrayCommon<T>(std::move(impl))
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Construct a new virtual array for a custom #VMutableArrayImpl.
|
|
*/
|
|
template<typename ImplT, typename... Args> static VMutableArray For(Args &&...args)
|
|
{
|
|
static_assert(std::is_base_of_v<VMutableArrayImpl<T>, ImplT>);
|
|
VMutableArray varray;
|
|
varray.template emplace<ImplT>(std::forward<Args>(args)...);
|
|
return varray;
|
|
}
|
|
|
|
/**
|
|
* Construct a new virtual array for an existing span. This does not take ownership of the span.
|
|
*/
|
|
static VMutableArray ForSpan(MutableSpan<T> values)
|
|
{
|
|
return VMutableArray::For<VArrayImpl_For_Span_final<T>>(values);
|
|
}
|
|
|
|
/**
|
|
* Construct a new virtual array for an existing span with a mapping function. This does not take
|
|
* ownership of the span.
|
|
*/
|
|
template<typename StructT, T (*GetFunc)(const StructT &), void (*SetFunc)(StructT &, T)>
|
|
static VMutableArray ForDerivedSpan(MutableSpan<StructT> values)
|
|
{
|
|
return VMutableArray::For<VArrayImpl_For_DerivedSpan<StructT, T, GetFunc, SetFunc>>(values);
|
|
}
|
|
|
|
/** Convert to a #VArray by copying. */
|
|
operator VArray<T>() const &
|
|
{
|
|
VArray<T> varray;
|
|
varray.copy_from(*this);
|
|
return varray;
|
|
}
|
|
|
|
/** Convert to a #VArray by moving. */
|
|
operator VArray<T>() &&noexcept
|
|
{
|
|
VArray<T> varray;
|
|
varray.move_from(std::move(*this));
|
|
return varray;
|
|
}
|
|
|
|
VMutableArray &operator=(const VMutableArray &other)
|
|
{
|
|
this->copy_from(other);
|
|
return *this;
|
|
}
|
|
|
|
VMutableArray &operator=(VMutableArray &&other) noexcept
|
|
{
|
|
this->move_from(std::move(other));
|
|
return *this;
|
|
}
|
|
|
|
/**
|
|
* Get access to the internal span. This invokes undefined behavior if the #is_span returned
|
|
* false.
|
|
*/
|
|
MutableSpan<T> get_internal_span() const
|
|
{
|
|
BLI_assert(this->is_span());
|
|
const Span<T> span = this->impl_->get_internal_span();
|
|
return MutableSpan<T>(const_cast<T *>(span.data()), span.size());
|
|
}
|
|
|
|
/**
|
|
* Set the value at the given index.
|
|
*/
|
|
void set(const int64_t index, T value)
|
|
{
|
|
BLI_assert(index >= 0);
|
|
BLI_assert(index < this->size());
|
|
this->get_impl()->set(index, std::move(value));
|
|
}
|
|
|
|
/**
|
|
* Copy the values from the source span to all elements in the virtual array.
|
|
*/
|
|
void set_all(Span<T> src)
|
|
{
|
|
BLI_assert(src.size() == this->size());
|
|
this->get_impl()->set_all(src);
|
|
}
|
|
|
|
/** See #GVMutableArrayImpl::try_assign_GVMutableArray. */
|
|
bool try_assign_GVMutableArray(GVMutableArray &varray) const
|
|
{
|
|
return this->get_impl()->try_assign_GVMutableArray(varray);
|
|
}
|
|
|
|
private:
|
|
/** Utility to get the pointer to the wrapped #VMutableArrayImpl. */
|
|
VMutableArrayImpl<T> *get_impl() const
|
|
{
|
|
/* This cast is valid by the invariant that a #VMutableArray->impl_ is always a
|
|
* #VMutableArrayImpl. */
|
|
return (VMutableArrayImpl<T> *)this->impl_;
|
|
}
|
|
};
|
|
|
|
template<typename T> static constexpr bool is_VArray_v = false;
|
|
template<typename T> static constexpr bool is_VArray_v<VArray<T>> = true;
|
|
|
|
template<typename T> static constexpr bool is_VMutableArray_v = false;
|
|
template<typename T> static constexpr bool is_VMutableArray_v<VMutableArray<T>> = true;
|
|
|
|
/**
|
|
* In many cases a virtual array is a span internally. In those cases, access to individual could
|
|
* be much more efficient than calling a virtual method. When the underlying virtual array is not a
|
|
* span, this class allocates a new array and copies the values over.
|
|
*
|
|
* This should be used in those cases:
|
|
* - All elements in the virtual array are accessed multiple times.
|
|
* - In most cases, the underlying virtual array is a span, so no copy is necessary to benefit
|
|
* from faster access.
|
|
* - An API is called, that does not accept virtual arrays, but only spans.
|
|
*/
|
|
template<typename T> class VArray_Span final : public Span<T> {
|
|
private:
|
|
VArray<T> varray_;
|
|
Array<T> owned_data_;
|
|
|
|
public:
|
|
VArray_Span(VArray<T> varray) : Span<T>(), varray_(std::move(varray))
|
|
{
|
|
this->size_ = varray_.size();
|
|
if (varray_.is_span()) {
|
|
this->data_ = varray_.get_internal_span().data();
|
|
}
|
|
else {
|
|
owned_data_.~Array();
|
|
new (&owned_data_) Array<T>(varray_.size(), NoInitialization{});
|
|
varray_.materialize_to_uninitialized(owned_data_);
|
|
this->data_ = owned_data_.data();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Same as #VArray_Span, but for a mutable span.
|
|
* The important thing to note is that when changing this span, the results might not be
|
|
* immediately reflected in the underlying virtual array (only when the virtual array is a span
|
|
* internally). The #save method can be used to write all changes to the underlying virtual array,
|
|
* if necessary.
|
|
*/
|
|
template<typename T> class VMutableArray_Span final : public MutableSpan<T> {
|
|
private:
|
|
VMutableArray<T> varray_;
|
|
Array<T> owned_data_;
|
|
bool save_has_been_called_ = false;
|
|
bool show_not_saved_warning_ = true;
|
|
|
|
public:
|
|
/* Create a span for any virtual array. This is cheap when the virtual array is a span itself. If
|
|
* not, a new array has to be allocated as a wrapper for the underlying virtual array. */
|
|
VMutableArray_Span(VMutableArray<T> varray, const bool copy_values_to_span = true)
|
|
: MutableSpan<T>(), varray_(std::move(varray))
|
|
{
|
|
this->size_ = varray_.size();
|
|
if (varray_.is_span()) {
|
|
this->data_ = varray_.get_internal_span().data();
|
|
}
|
|
else {
|
|
if (copy_values_to_span) {
|
|
owned_data_.~Array();
|
|
new (&owned_data_) Array<T>(varray_.size(), NoInitialization{});
|
|
varray_.materialize_to_uninitialized(owned_data_);
|
|
}
|
|
else {
|
|
owned_data_.reinitialize(varray_.size());
|
|
}
|
|
this->data_ = owned_data_.data();
|
|
}
|
|
}
|
|
|
|
~VMutableArray_Span()
|
|
{
|
|
if (show_not_saved_warning_) {
|
|
if (!save_has_been_called_) {
|
|
std::cout << "Warning: Call `save()` to make sure that changes persist in all cases.\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Write back all values from a temporary allocated array to the underlying virtual array. */
|
|
void save()
|
|
{
|
|
save_has_been_called_ = true;
|
|
if (this->data_ != owned_data_.data()) {
|
|
return;
|
|
}
|
|
varray_.set_all(owned_data_);
|
|
}
|
|
|
|
void disable_not_applied_warning()
|
|
{
|
|
show_not_saved_warning_ = false;
|
|
}
|
|
};
|
|
|
|
template<typename T> class SingleAsSpan {
|
|
private:
|
|
T value_;
|
|
int64_t size_;
|
|
|
|
public:
|
|
SingleAsSpan(T value, int64_t size) : value_(std::move(value)), size_(size)
|
|
{
|
|
BLI_assert(size_ >= 0);
|
|
}
|
|
|
|
SingleAsSpan(const VArray<T> &varray) : SingleAsSpan(varray.get_internal_single(), varray.size())
|
|
{
|
|
}
|
|
|
|
const T &operator[](const int64_t index) const
|
|
{
|
|
BLI_assert(index >= 0);
|
|
BLI_assert(index < size_);
|
|
UNUSED_VARS_NDEBUG(index);
|
|
return value_;
|
|
}
|
|
};
|
|
|
|
} // namespace blender
|