utils: Improve StructureOfArrays (#9936)

- Added `copyRange()` to `StructureOfArrays` to efficiently copy a range
  of elements from another SoA of the same type. It uses `std::copy_n`
  which leverages `memcpy` for trivially copyable types.
- Added `operator[]` to `StructureOfArrays` returning `IteratorValueRef`.
- Added `operator=` for `Structure` (tuple) to `IteratorValueRef` to
  allow direct assignment: `soa[i] = tuple`.
- Added unit tests for these new methods in `test_StructureOfArrays.cpp`.
This commit is contained in:
Mathias Agopian
2026-04-29 09:06:26 -07:00
committed by GitHub
parent 25a12b47c9
commit 9c27bfa3e1
2 changed files with 154 additions and 42 deletions

View File

@@ -55,10 +55,19 @@ public:
static constexpr size_t getArrayCount() noexcept { return kArrayCount; }
// Size needed to store "size" array elements
static size_t getNeededSize(size_t size) noexcept {
static size_t getNeededSize(size_t const size) noexcept {
return getOffset(kArrayCount - 1, size) + sizeof(TypeAt<kArrayCount - 1>) * size;
}
template<typename OtherAllocator>
void copyRange(size_t destOffset,
const StructureOfArraysBase<OtherAllocator, Elements...>& src, size_t srcOffset, size_t count) {
assert(destOffset + count <= mCapacity);
assert(srcOffset + count <= src.mSize);
Copier<OtherAllocator> copier{this, src, destOffset, srcOffset, count};
const_cast<StructureOfArraysBase*>(this)->for_each_index(mArrays, copier);
}
// --------------------------------------------------------------------------------------------
class IteratorValue;
@@ -77,21 +86,28 @@ public:
friend class IteratorValue;
friend iterator;
friend const_iterator;
friend class StructureOfArraysBase;
StructureOfArraysBase* const UTILS_RESTRICT soa;
size_t const index;
IteratorValueRef(StructureOfArraysBase* soa, size_t index) : soa(soa), index(index) { }
IteratorValueRef(StructureOfArraysBase* soa, size_t const index) : soa(soa), index(index) { }
// assigns a value_type to a reference (i.e. assigns to what's pointed to by the reference)
template<size_t ... Is>
IteratorValueRef& assign(IteratorValue const& rhs, std::index_sequence<Is...>);
template<size_t ... Is>
IteratorValueRef& assign(Structure const& rhs, std::index_sequence<Is...>);
template<size_t ... Is>
IteratorValueRef& assign(Structure&& rhs, std::index_sequence<Is...>) noexcept;
// assigns a value_type to a reference (i.e. assigns to what's pointed to by the reference)
template<size_t ... Is>
IteratorValueRef& assign(IteratorValue&& rhs, std::index_sequence<Is...>) noexcept;
// objects pointed to by reference can be swapped, so provide the special swap() function.
friend void swap(IteratorValueRef lhs, IteratorValueRef rhs) {
friend void swap(IteratorValueRef const lhs, IteratorValueRef const rhs) {
lhs.soa->swap(lhs.index, rhs.index);
}
@@ -99,6 +115,9 @@ public:
// references can be created by copy-assignment only
IteratorValueRef(IteratorValueRef const& rhs) noexcept : soa(rhs.soa), index(rhs.index) { }
IteratorValueRef& operator=(Structure const& rhs);
IteratorValueRef& operator=(Structure&& rhs) noexcept;
// copy the content of a reference to the content of this one
IteratorValueRef& operator=(IteratorValueRef const& rhs);
@@ -120,6 +139,10 @@ public:
template<size_t I> TypeAt<I>& get() { return soa->elementAt<I>(index); }
};
IteratorValueRef operator[](size_t index) noexcept {
return {this, index};
}
/*
* The value_type of iterator. This is basically the "structure" of the SoA.
@@ -175,7 +198,7 @@ public:
CVQualifiedSOAPointer soa; // don't use restrict, can have aliases if multiple iterators are created
size_t index;
Iterator(CVQualifiedSOAPointer soa, size_t index) : soa(soa), index(index) {}
Iterator(CVQualifiedSOAPointer soa, size_t const index) : soa(soa), index(index) {}
public:
using value_type = IteratorValue;
@@ -196,10 +219,10 @@ public:
Iterator& operator++() { ++index; return *this; }
Iterator& operator--() { --index; return *this; }
Iterator& operator+=(size_t n) { index += n; return *this; }
Iterator& operator-=(size_t n) { index -= n; return *this; }
Iterator operator+(size_t n) const { return { soa, index + n }; }
Iterator operator-(size_t n) const { return { soa, index - n }; }
Iterator& operator+=(size_t const n) { index += n; return *this; }
Iterator& operator-=(size_t const n) { index -= n; return *this; }
Iterator operator+(size_t const n) const { return { soa, index + n }; }
Iterator operator-(size_t const n) const { return { soa, index - n }; }
difference_type operator-(Iterator const& rhs) const { return index - rhs.index; }
bool operator==(Iterator const& rhs) const { return (index == rhs.index); }
bool operator!=(Iterator const& rhs) const { return (index != rhs.index); }
@@ -222,7 +245,7 @@ public:
StructureOfArraysBase() = default;
explicit StructureOfArraysBase(size_t capacity) {
explicit StructureOfArraysBase(size_t const capacity) {
setCapacity(capacity);
}
@@ -270,7 +293,7 @@ public:
// set the capacity of the array. the capacity cannot be smaller than the current size,
// the call is a no-op in that case.
UTILS_NOINLINE
void setCapacity(size_t capacity) {
void setCapacity(size_t const capacity) {
// allocate enough space for "capacity" elements of each array
// capacity cannot change when optional storage is specified
if (capacity >= mSize) {
@@ -292,7 +315,7 @@ public:
}
}
void ensureCapacity(size_t needed) {
void ensureCapacity(size_t const needed) {
if (UTILS_UNLIKELY(needed > mCapacity)) {
// not enough space, increase the capacity
const size_t capacity = (needed > SIZE_MAX / 3) ? needed : (needed * 3 + 1) / 2;
@@ -305,7 +328,7 @@ public:
// If the arrays don't have enough capacity, the capacity is increased accordingly
// (the capacity is set to 3/2 of the asked size).
UTILS_NOINLINE
void resize(size_t needed) {
void resize(size_t const needed) {
ensureCapacity(needed);
resizeNoCheck(needed);
if (needed <= mCapacity) {
@@ -318,7 +341,7 @@ public:
}
inline void swap(size_t i, size_t j) noexcept {
void swap(size_t i, size_t j) noexcept {
forEach([i, j](auto p) {
using std::swap;
swap(p[i], p[j]);
@@ -326,7 +349,7 @@ public:
}
// remove and destroy the last element of each array
inline void pop_back() noexcept {
void pop_back() noexcept {
if (mSize) {
destroy_each(mSize - 1, mSize);
mSize--;
@@ -483,80 +506,97 @@ public:
using Type = typename SoA::template TypeAt<E>;
UTILS_ALWAYS_INLINE Field& operator = (Field&& rhs) noexcept {
soa.elementAt<E>(i) = soa.elementAt<E>(rhs.i);
soa.template elementAt<E>(i) = soa.template elementAt<E>(rhs.i);
return *this;
}
// auto-conversion to the field's type
UTILS_ALWAYS_INLINE operator Type&() noexcept {
return soa.elementAt<E>(i);
return soa.template elementAt<E>(i);
}
UTILS_ALWAYS_INLINE operator Type const&() const noexcept {
return soa.elementAt<E>(i);
return soa.template elementAt<E>(i);
}
// dereferencing the selected field
UTILS_ALWAYS_INLINE Type& operator ->() noexcept {
return soa.elementAt<E>(i);
return soa.template elementAt<E>(i);
}
UTILS_ALWAYS_INLINE Type const& operator ->() const noexcept {
return soa.elementAt<E>(i);
return soa.template elementAt<E>(i);
}
// address-of the selected field
UTILS_ALWAYS_INLINE Type* operator &() noexcept {
return &soa.elementAt<E>(i);
return &soa.template elementAt<E>(i);
}
UTILS_ALWAYS_INLINE Type const* operator &() const noexcept {
return &soa.elementAt<E>(i);
return &soa.template elementAt<E>(i);
}
// assignment to the field
UTILS_ALWAYS_INLINE Type const& operator = (Type const& other) noexcept {
return (soa.elementAt<E>(i) = other);
return (soa.template elementAt<E>(i) = other);
}
UTILS_ALWAYS_INLINE Type const& operator = (Type&& other) noexcept {
return (soa.elementAt<E>(i) = std::forward<Type>(other));
return (soa.template elementAt<E>(i) = std::forward<Type>(other));
}
// comparisons
UTILS_ALWAYS_INLINE bool operator==(Type const& other) const {
return (soa.elementAt<E>(i) == other);
return (soa.template elementAt<E>(i) == other);
}
UTILS_ALWAYS_INLINE bool operator!=(Type const& other) const {
return (soa.elementAt<E>(i) != other);
return (soa.template elementAt<E>(i) != other);
}
// calling the field
template <typename ... ARGS>
UTILS_ALWAYS_INLINE decltype(auto) operator()(ARGS&& ... args) noexcept {
return soa.elementAt<E>(i)(std::forward<ARGS>(args)...);
return soa.template elementAt<E>(i)(std::forward<ARGS>(args)...);
}
template <typename ... ARGS>
UTILS_ALWAYS_INLINE decltype(auto) operator()(ARGS&& ... args) const noexcept {
return soa.elementAt<E>(i)(std::forward<ARGS>(args)...);
return soa.template elementAt<E>(i)(std::forward<ARGS>(args)...);
}
};
private:
template<typename OtherAllocator>
struct Copier {
StructureOfArraysBase* dest;
const StructureOfArraysBase<OtherAllocator, Elements...>& src;
size_t destOffset;
size_t srcOffset;
size_t count;
template<size_t I>
void operator()(TypeAt<I>* destPtr) const {
using ElementType = TypeAt<I>;
ElementType const* const s = std::get<I>(src.mArrays) + srcOffset;
ElementType* const d = destPtr + destOffset;
std::copy_n(s, count, d);
}
};
template<std::size_t I = 0, typename FuncT, typename... Tp>
inline std::enable_if_t<I == sizeof...(Tp), void>
std::enable_if_t<I == sizeof...(Tp), void>
for_each(std::tuple<Tp...>&, FuncT) {}
template<std::size_t I = 0, typename FuncT, typename... Tp>
inline std::enable_if_t<I < sizeof...(Tp), void>
std::enable_if_t<I < sizeof...(Tp), void>
for_each(std::tuple<Tp...>& t, FuncT f) {
f(I, std::get<I>(t));
for_each<I + 1, FuncT, Tp...>(t, f);
}
template<std::size_t I = 0, typename FuncT, typename... Tp>
inline std::enable_if_t<I == sizeof...(Tp), void>
std::enable_if_t<I == sizeof...(Tp), void>
for_each_index(std::tuple<Tp...>&, FuncT) {}
template<std::size_t I = 0, typename FuncT, typename... Tp>
inline std::enable_if_t<I < sizeof...(Tp), void>
std::enable_if_t<I < sizeof...(Tp), void>
for_each_index(std::tuple<Tp...>& t, FuncT f) {
f.template operator()<I>(std::get<I>(t));
for_each_index<I + 1, FuncT, Tp...>(t, f);
}
inline void resizeNoCheck(size_t needed) noexcept {
void resizeNoCheck(size_t const needed) noexcept {
assert(mCapacity >= needed);
if (needed < mSize) {
// we shrink the arrays
@@ -570,12 +610,12 @@ private:
}
// this calculates the offset adjusted for all data alignment of a given array
static inline size_t getOffset(size_t index, size_t capacity) noexcept {
static size_t getOffset(size_t index, size_t const capacity) noexcept {
auto offsets = getOffsets(capacity);
return offsets[index];
}
static inline std::array<size_t, kArrayCount> getOffsets(size_t capacity) noexcept {
static std::array<size_t, kArrayCount> getOffsets(size_t capacity) noexcept {
// compute the required size of each array
const size_t sizes[] = { (sizeof(Elements) * capacity)... };
@@ -618,7 +658,7 @@ private:
});
}
void move_each(void* buffer, size_t capacity) noexcept {
void move_each(void* buffer, size_t const capacity) noexcept {
auto offsets = getOffsets(capacity);
size_t index = 0;
if (mSize) {
@@ -653,7 +693,7 @@ private:
// update the pointers
for_each(mArrays, [buffer, &offsets](size_t i, auto&& p) {
using Type = std::remove_reference_t<decltype(p)>;
p = Type((char*)buffer + offsets[i]);
p = Type(static_cast<char*>(buffer) + offsets[i]);
});
}
@@ -668,7 +708,6 @@ private:
template<typename Allocator, typename... Elements>
inline
typename StructureOfArraysBase<Allocator, Elements...>::IteratorValueRef&
StructureOfArraysBase<Allocator, Elements...>::IteratorValueRef::operator=(
IteratorValueRef const& rhs) {
@@ -676,16 +715,28 @@ StructureOfArraysBase<Allocator, Elements...>::IteratorValueRef::operator=(
}
template<typename Allocator, typename... Elements>
inline
typename StructureOfArraysBase<Allocator, Elements...>::IteratorValueRef&
StructureOfArraysBase<Allocator, Elements...>::IteratorValueRef::operator=(
IteratorValueRef&& rhs) noexcept {
return operator=(IteratorValue(rhs));
}
template<typename Allocator, typename... Elements>
typename StructureOfArraysBase<Allocator, Elements...>::IteratorValueRef&
StructureOfArraysBase<Allocator, Elements...>::IteratorValueRef::operator=(
Structure const& rhs) {
return assign(rhs, std::make_index_sequence<kArrayCount>());
}
template<typename Allocator, typename... Elements>
typename StructureOfArraysBase<Allocator, Elements...>::IteratorValueRef&
StructureOfArraysBase<Allocator, Elements...>::IteratorValueRef::operator=(
Structure&& rhs) noexcept {
return assign(std::move(rhs), std::make_index_sequence<kArrayCount>());
}
template<typename Allocator, typename... Elements>
template<size_t... Is>
inline
typename StructureOfArraysBase<Allocator, Elements...>::IteratorValueRef&
StructureOfArraysBase<Allocator, Elements...>::IteratorValueRef::assign(
IteratorValue const& rhs, std::index_sequence<Is...>) {
@@ -696,7 +747,24 @@ StructureOfArraysBase<Allocator, Elements...>::IteratorValueRef::assign(
template<typename Allocator, typename... Elements>
template<size_t... Is>
inline
typename StructureOfArraysBase<Allocator, Elements...>::IteratorValueRef&
StructureOfArraysBase<Allocator, Elements...>::IteratorValueRef::assign(
Structure const& rhs, std::index_sequence<Is...>) {
auto UTILS_UNUSED l = {(soa->template elementAt<Is>(index) = std::get<Is>(rhs), 0)...};
return *this;
}
template<typename Allocator, typename... Elements>
template<size_t... Is>
typename StructureOfArraysBase<Allocator, Elements...>::IteratorValueRef&
StructureOfArraysBase<Allocator, Elements...>::IteratorValueRef::assign(
Structure&& rhs, std::index_sequence<Is...>) noexcept {
auto UTILS_UNUSED l = {(soa->template elementAt<Is>(index) = std::move(std::get<Is>(rhs)), 0)...};
return *this;
}
template<typename Allocator, typename... Elements>
template<size_t... Is>
typename StructureOfArraysBase<Allocator, Elements...>::IteratorValueRef&
StructureOfArraysBase<Allocator, Elements...>::IteratorValueRef::assign(
IteratorValue&& rhs, std::index_sequence<Is...>) noexcept {

View File

@@ -19,6 +19,11 @@
#include <utils/StructureOfArrays.h>
#include <math/vec4.h>
#include <cstdint>
#include <cstdlib>
#include <memory>
#include <utility>
using namespace filament::math;
using namespace utils;
@@ -71,7 +76,7 @@ TEST(StructureOfArraysTest, Iterator) {
EXPECT_EQ(soa.elementAt<0>(2), 3.0f);
for (size_t i = 0; i < 8; i++) {
soa.elementAt<0>(i) = (float)std::rand();
soa.elementAt<0>(i) = float(std::rand());
soa.elementAt<1>(i) = soa.elementAt<0>(i) * 2;
soa.elementAt<2>(i) = soa.elementAt<0>(i) * 4;
}
@@ -103,7 +108,7 @@ TEST(StructureOfArraysTest, Simple) {
soa.elementAt<2>(i) = i * 4;
}
size_t capacity = soa.capacity();
size_t const capacity = soa.capacity();
// check that each array doesn't overlap the previous one
EXPECT_TRUE((void*)soa.data<1>() >= (void*)(soa.data<0>() + capacity));
@@ -183,3 +188,42 @@ TEST(StructureOfArraysTest, MoveOnly) {
EXPECT_EQ(*soa.elementAt<1>(1).get(), 2);
}
TEST(StructureOfArraysTest, CopyRange) {
StructureOfArrays<float, double, TestFloat4> soa1;
StructureOfArrays<float, double, TestFloat4> soa2;
soa1.setCapacity(10);
soa1.resize(5);
for (size_t i = 0; i < 5; i++) {
soa1.elementAt<0>(i) = i;
soa1.elementAt<1>(i) = i * 2;
soa1.elementAt<2>(i) = i * 4;
}
soa2.setCapacity(10);
soa2.resize(5);
soa2.copyRange(1, soa1, 1, 3);
EXPECT_EQ(soa2.elementAt<0>(1), 1.0f);
EXPECT_EQ(soa2.elementAt<1>(1), 2.0);
EXPECT_EQ(soa2.elementAt<2>(1), TestFloat4{ 4.0f });
EXPECT_EQ(soa2.elementAt<0>(3), 3.0f);
EXPECT_EQ(soa2.elementAt<1>(3), 6.0);
EXPECT_EQ(soa2.elementAt<2>(3), TestFloat4{ 12.0f });
}
TEST(StructureOfArraysTest, AssignTuple) {
StructureOfArrays<float, double, TestFloat4> soa;
soa.setCapacity(10);
soa.resize(5);
auto tuple = std::make_tuple(1.0f, 2.0, TestFloat4{4.0f});
soa[2] = tuple;
EXPECT_EQ(soa.elementAt<0>(2), 1.0f);
EXPECT_EQ(soa.elementAt<1>(2), 2.0);
EXPECT_EQ(soa.elementAt<2>(2), TestFloat4{ 4.0f });
}