1 module nxt.soa_petar_kirov;
2 
3 /++
4  Structure of Arrays
5 
6  This type constructor turns a structure with any number of fields into a
7  structure with (conceptually) the same number of arrays with minimal overhead
8  (basically two pointers). Even though the implementation uses low-level tricks
9  like pointer-arithmetic, the interface is completely type-safe.
10 
11  The representation in memory is roughly this:
12  storage = [ [ Fields!T[0], .. ] ... [ Fields!T[$ - 1] .. ] ]
13  (storage is a single memory allocation)
14 
15  Original at https://gist.github.com/PetarKirov/a074073a12482e761a5e88eec559e5a8
16  +/
17 struct SoA(T, alias Allocator = from!`std.experimental.allocator.gc_allocator`.GCAllocator)
18 if (is(T == struct))
19 {
20     static assert (__traits(isPOD, T));
21     static assert (typeof(this).sizeof == size_t.sizeof * 2); // only two words
22 
23     private // internal state
24     {
25         void* _storage;
26         size_t _length;
27         alias allocator = allocatorInstanceOf!Allocator;
28         alias Fields = from!`std.traits`.Fields!T;
29         enum FieldNames = from!`std.traits`.FieldNameTuple!T;
30     }
31 
32     /** Creates a structure of arrays instance with `newLength` elements. */
33     this(size_t newLength) nothrow
34     in (newLength)
35     out (; this._storage)
36     {
37         _length = newLength;
38 
39         // TODO: Respect individual field alignment
40         _storage = allocator.allocate(T.sizeof * capacity()).ptr;
41 
42         // Initialize each array with the default value for each field type:
43         static foreach (idx; 0 .. Fields.length)
44             chunkFor!idx(_storage, capacity())[] = Fields[idx].init;
45     }
46 
47     void toString(Writer)(Writer sink) const
48     {
49         import std.format : formattedWrite;
50 
51         sink.formattedWrite("%s\n{\n", typeof(this).stringof);
52         static foreach (idx; 0 .. Fields.length)
53             sink.formattedWrite("  %s[] %s = %s,\n",
54                                 Fields[idx].stringof,
55                                 T.tupleof[idx].stringof,
56                                 chunkFor!idx(_storage, capacity)[0 .. length]
57             );
58         sink.formattedWrite("}");
59     }
60 
61     nothrow:
62 
63     /** Returns the number of inserted elements. */
64     size_t length() const pure @safe
65     {
66         return _length;
67     }
68 
69     alias opDollar = length;
70 
71     /** Returns the current capacity - the current length rounded up to a power
72         of two. This assumption allows us to save on having a separate field.
73     */
74     size_t capacity() const pure @safe
75     {
76         return _length.roundUpToPowerOf2;
77     }
78 
79     /** Returns the number of elements that can be inserted without allocation. */
80     size_t availableCapacity() const pure
81     {
82         assert (length <= capacity);
83         return capacity - length;
84     }
85 
86     /** Returns true iff no elements are present. */
87     bool empty() const pure @safe { return !length; }
88 
89     /** Given a field name, returns its index.
90         Callable at compile-time.
91     */
92     enum size_t fieldIndex(string fieldName) = from!"std.meta".staticIndexOf!(fieldName, FieldNames);
93 
94     auto opIndex(size_t idx)
95     {
96         auto self = this;
97         static struct Result
98         {
99             typeof(self) parent;
100             size_t idx;
101             void toString(scope void delegate(const(char)[]) sink) const
102             {
103                 import std.format : formattedWrite;
104                 sink.formattedWrite("%s\n{\n", T.stringof ~ "Ref");
105                 static foreach (field; 0 .. Fields.length)
106                 {{
107                         enum fieldName = T.tupleof[field].stringof;
108                         sink.formattedWrite("  %s: %s,\n",
109                                             fieldName,
110                                             opDispatch!fieldName()
111                         );
112                     }}
113                 sink.formattedWrite("}");
114             }
115 
116             ref opDispatch(string field)() inout
117             {
118                 return parent.opDispatch!field()[idx];
119             }
120         }
121         return Result(self, idx);
122     }
123 
124     /// Returns the array corresponding to the instances of `field`.
125     auto opDispatch(string field)() inout pure @safe
126     in (!empty)
127     {
128         enum idx = fieldIndex!field;
129         return chunkFor!idx(_storage, capacity())[0 .. _length];
130     }
131 
132     /// Given an argument of type `T` or argument sequence of type `Fields!T`,
133     /// inserts the fields of T at the back of the container.
134     void insertBack(Args...)(auto ref Args args) @safe
135     if (is(Args == from!`std.meta`.AliasSeq!T) ||
136         is(Args == Fields))
137     {
138         if (availableCapacity)
139             _length++;
140         else
141             grow!true;
142 
143         static foreach (idx; 0 .. Fields.length)
144             static if (is(Args == from!`std.meta`.AliasSeq!T))
145                 chunkFor!idx(_storage, capacity())[_length - 1] =
146                 args[0].tupleof[idx];
147 
148             else static if (is(Args == Fields))
149                 chunkFor!idx(_storage, capacity())[_length - 1] =
150                 args[idx];
151     }
152 
153     /// Returns `inout(U)[]`- the slice of memory pointing where elements of
154     /// type `U == Field!T[idx]` are stored.
155     private static pure @trusted
156     inout(Fields[idx])[] chunkFor(size_t idx)(inout(void)* base, size_t size)
157     {
158         auto add(alias a, alias b)() { return a + b; }
159         size_t sizeOf(X)() { return X.sizeof * size; }
160 
161         static if (idx)
162             size_t offset = staticMapReduce!(sizeOf, add, Fields[0 .. idx]);
163         else
164             size_t offset = 0;
165 
166         return (cast(inout(Fields[idx])*)(base + offset))[0 .. size];
167     }
168 
169     /// Doubles the available size (capacity) and conditionally copies the data
170     /// to the new location.
171     private void grow(bool relocateData)() @trusted
172     {
173         auto old_capacity = this.capacity();
174         auto new_capacity = this._length++? this.capacity() : 1;
175 
176         auto add(alias a, alias b)() { return a + b; }
177         auto sizeOfOld(X)() { return X.sizeof * old_capacity; }
178         auto sizeOfNew(X)() { return X.sizeof * new_capacity; }
179 
180         auto old_storage_size = staticMapReduce!(sizeOfOld, add, Fields);
181         auto new_storage_size = staticMapReduce!(sizeOfNew, add, Fields);
182 
183         auto new_storage = allocator.allocate(new_storage_size);
184 
185         // move chunk by chunk to the new location
186         static if (relocateData)
187             static foreach(idx; 0 .. Fields.length)
188                 chunkFor!idx(new_storage.ptr, new_capacity)[0 .. length - 1] =
189                 chunkFor!idx(_storage, old_capacity)[0 .. length - 1];
190 
191         if (old_capacity)
192             allocator.deallocate(_storage[0 .. old_storage_size]);
193 
194         _storage = new_storage.ptr;
195     }
196 }
197 
198 template from(string module_)
199 {
200     mixin("import from = ", module_, ';');
201 }
202 
203 template allocatorInstanceOf(alias Allocator)
204 {
205     static if (is(typeof(Allocator)))
206         alias allocatorInstanceOf = Allocator;
207     else static if (is(typeof(Allocator.instance)))
208         alias allocatorInstanceOf = Allocator.instance;
209     else
210         static assert (0);
211 }
212 
213 size_t roundUpToPowerOf2(size_t s) @safe @nogc nothrow pure
214 in (s <= (size_t.max >> 1) + 1)
215 {
216     --s;
217     static foreach (i; 0 .. 5)
218         s |= s >> (1 << i);
219     return s + 1;
220 }
221 
222 /// Applies `map` to each element of a compile-time sequence and
223 /// then recursively calls `reduce` on each pair.
224 template staticMapReduce(alias map, alias reduce, Args...)
225 if (Args.length > 0)
226 {
227     static if (Args.length == 1)
228         alias staticMapReduce = map!(Args[0]);
229     else
230         alias staticMapReduce = reduce!(
231             staticMapReduce!(map, reduce, Args[0]),
232             staticMapReduce!(map, reduce, Args[1 .. $])
233         );
234 }
235 
236 ///
237 unittest
238 {
239     import std.meta : AliasSeq;
240     enum mulBy2(alias x) = x * 2;
241     enum add(alias a, alias b) = a + b;
242     static assert(staticMapReduce!(mulBy2, add, AliasSeq!(1)) == 2);
243     static assert(staticMapReduce!(mulBy2, add, AliasSeq!(1, 2)) == 6);
244     static assert(staticMapReduce!(mulBy2, add, AliasSeq!(1, 2, 3, 4, 5)) == 30);
245 }
246 
247 @system unittest
248 {
249     // import std.stdio : writeln;
250 
251     static struct Vec3
252     {
253         float x, y, z;
254     }
255 
256     auto soa = SoA!Vec3(5);
257 
258     soa.y[] = 2;
259     soa.z[] = 3;
260     soa.x[] = soa.y[] * soa.z[];
261     // soa.writeln;
262 
263     // soa[0].writeln;
264 
265     soa[1].x++;
266     soa[1].y += 2;
267     soa[1].z *= 3;
268     // soa.writeln;
269 
270     soa.insertBack(Vec3(21, 42, 84));
271     // soa.writeln;
272 
273     soa.insertBack(0.5f, 0.25f, 0.125f);
274     // soa.writeln;
275 }