10

One feature that plays a prominent role in many of the writings on data oriented design is that there are many cases where rather than AoS (array of structs):

struct C_AoS {
  int    foo;
  double bar;
};

std::vector<C_AoS> cs;
...
std::cout << cs[42].foo << std::endl;

it is more efficient to arrange one's data in SoA (struct of arrays):

struct C_SoA {
  std::vector<int>    foo;
  std::vector<double> bar;
};

C_SoA cs;
...
std::cout << cs.foo[42] << std::endl;

Now what I am looking for is a solution which would allow me to switch between AoS and SoA without changing the calling interface, i.e. that I could, with minimal effort and with no extra runtime cost (at least to the point of excessive indirection), call e.g. cs[42].foo; regardless of which arrangement of data I'm using.

I should note that the example syntax above is the ideal case, which might very well be impossible, but I'd be very interested in close approximations, too. Any takers?

alarge
  • 2,162
  • 2
  • 13
  • 14
  • I used to play with this a lot, generating tuples at compile-time and so forth, but kinda lost interest as it felt a bit heavy and unwieldy. What I've found useful instead is just write a lot of local accessor functions (could just be static functions in the source file) before you start experimenting with data representations. This way you end up having to change the minimum amount of code to explore a totally different kind of rep. –  Dec 23 '17 at 11:01

1 Answers1

8

I'm going to choose this syntax: cs.foo[42] to be the single syntax and use typedefs to switch between arrangements:

So, obviously given C_SoA from your post, the above syntax works and we can have:

typedef C_SoA Arrangement;

Arrangement cs;

In order to use std::vector<C_AoS> instead we are going to have to introduce something else:

typedef std::vector<C_AoS> AOS;

template<class A, class C, class T>
struct Accessor {
    T operator[](size_t index){
            return arr[index].*pMember;
    }
    T (C::*pMember);
    A& arr;
    Accessor(A& a, T (C::*p)): arr(a), pMember(p){}
};

struct Alt_C_AoS{
    Accessor<AOS, C_AoS, int> foo;
    Accessor<AOS, C_AoS, double> bar;

    AOS aos;
    Alt_C_AoS():foo(aos, &C_AoS::foo), bar(aos, &C_AoS::bar){}
};

Now we can have:

//Choose just one arrangement
typedef Alt_C_AoS Arrangement;
//typedef C_SoA Arrangement;

Arrangement cs;
...
std::cout << cs.foo[42] << std::endl;

Essentially this converts container dot member index into container index dot member.

quamrana
  • 37,849
  • 12
  • 53
  • 71
  • 1
    Nice question and beautiful answer. I have just noted that when I have encountered this "problem", it was because the natural representation of data would be SoA, but coding it's much easier to deal with AoSs, and often coders prefer this approach. So I ended often up with a similar-opposite set-up, using custom accessors and then accessing via an accessor method, `std::cout << cs[42].get_foo() << std::endl` actually as the C++ encapsulation mantra recites. – Sigi Mar 19 '15 at 10:44
  • Curious about your approach -- please consider sharing your code [(have asked question here)](http://stackoverflow.com/q/42184288/1823664). – user1823664 Feb 12 '17 at 04:47
  • I wrote a similar question and answer more recently, a zero cost abstraction using the C++17 (could be also C++14 though), in case you want to update this answer: https://stackoverflow.com/questions/50574639/c-zero-cost-abstraction-for-soa-aos-memory-layouts/50624651#50624651 . The code is also on a github repo: https://github.com/crosetto/SoAvsAoS – Paolo Crosetto Nov 08 '18 at 12:15