I started thinking this would be a rehashing of how column-oriented storage formats can be far more efficient for certain access patterns. But it really was a showcase of what a bloated mess C++ has become.
Struct of Arrays vs Array of Structs have different performance, depending on how they are accessed, mainly due to how memory will be cached.<p>If you are iterating over objects sequentially, and looking at every field, they will perform about the same. If you are iterating over objects sequentially, and only looking at a few fields, Struct of Arrays performs better. If you are accessing objects at random, and reading every field, Array of Structs performs better.<p>Array of Structs has a multiplication step when you calculate the address of an element. Struct of Arrays basically guarantees that the multiplication will be by a power of 2, so it's a bitshift rather than a multiplication. (Unless your struct contains a type whose size is not a power of 2). Multiplication is still very fast though on most architectures, so that's not all that much of a difference.
Interesting article. It does show how modern C++ can be quite scary.<p>It reminded me of a Haxe macro used by the Dune: Spice Wars devs to transform an AoS into a SoA at compile time to increase performance: <a href="https://youtu.be/pZcKyqLcjzc?t=941" rel="nofollow">https://youtu.be/pZcKyqLcjzc?t=941</a><p>The end result is quite cool, though those compile time type generation macros always look too magical to me. Makes me wonder if just getting values using an index wouldn't end up being more readable.
I attempted to write a minimal version of the idea in Common Lisp, if anyone was curious about how it'd look like in that language,<p><a href="https://peri.pages.dev/struct-of-arrays-snippet" rel="nofollow">https://peri.pages.dev/struct-of-arrays-snippet</a>
I think I've lost the thread on the abstractions. (Me not being very familiar with Zig outside of its most basic syntax is probably why.) I've been doing a lot of SoA work in rust lately; specifically because I have numerical/scientific code that uses CPU SIMD and CUDA; SoA works great for these.<p>The workflow is, I set up Vec3x8, and Quaternionx8, which wrap a simd instrinsic for each field (x: f32x8, y: f32x8...) etc.<p>For the GPU and general flattening, I just pack the args as Vecs, then the fn signature contains them like eps: &[f32], sigma: &[f32] etc. So, I'm having trouble mapping this SoA approach to the abstractions used in the article. Then the (C++-like CUDA) kernel sees these as *float3 params etc. It also feels like a complexity reverse of the Rust/Zig stereotypes...<p>Examples:<p><pre><code> struct Vec3x8 {
x: f32x8,
y: f32x8,
z: f32x8
} // appropriate operator overloads...
struct Setup {
eps: Vec<f32>,
sigma: Vec<f32>,
}
</code></pre>
So, Structs of Arrays, plainly. Are the abstractions used here something like Jai is attempting, where the internal implementation is decoupled from the API, so you don't compromise on performance vice ergonomics?
Interest in SOA is bringing to mind the “art of the meta object protocol” which argues for a stage between class definition and implementation that would allow you to choose the layout and access method for instances of a class.
JOVIAL language had TABLE structures, which could be declared SERIAL or PARALLEL
(<a href="https://ntrl.ntis.gov/NTRL/dashboard/searchResults/titleDetail/PB82135062.xhtml" rel="nofollow">https://ntrl.ntis.gov/NTRL/dashboard/searchResults/titleDeta...</a> page 281).
Towards the middle you realize this is about the reflection more than anything else.<p>I do like how directly accessing the fields individually (the whole reason you would do this) is a hypothetical presented as an after thought. Enjoyably absurd.
The use of reflection is interesting, but is there a significant advantage, compared to something like this:<p><pre><code> template<template<class> class G>
struct Point {
G<int> x;
G<int> y;
auto get() { return std::tie(x,y); }
};
template<template<template<class> class> class C>
struct SOA {
template<class T> using Id = T;
template<class T> using Ref = T&;
C<std::vector> vs;
void push_back(C<Id> x) {
std::apply([&] (auto&&... r) {
std::apply([&](auto&&... v){ ( (r.push_back(v)),...); }, x.get());
}, vs.get());
}
C<Ref> operator[](size_t i) {
return std::apply([&] (auto&&... r) { return C<Ref>{ r[i]...}; }, vs.get());
}
};
int main() {
SOA<Point> soa_point;
soa_point.push_back({1,2});
auto [x,y] = soa_point[0];
}</code></pre>
It is skeptical if this is even needed, why not design the structs etc. properly from the start so they are columnar if columnar works better?<p>Meta programming is never free
The problem I run into with these sorts of structures is the whole aggregation versus composition issue.<p>With composition the SoA problem is pretty simple. This object belongs to an owner or a category and it just never 'moves' so the most naive solution is just fine.<p>I haven't looked at C++ in a million years, but looks like this implementation fixes the aggregation problem by handing out handles to the data, but I don't see anything in here where it tries to hand out the same handle for the same offset. I don't know about C++ but that matters in some languages.