1 /** Structure of arrays (SoA). 2 * 3 * SoAs are common in game engines. 4 * 5 * Initially a builtin feature in the Jai programming language that later was 6 * made into a library solution. 7 * 8 * TODO: merge with soa_petar_kirov.d by 9 * 1. allocate all arrays in a single chunk 10 * 2. calculating `_capacity` based on `_length` 11 * 12 * TODO: merge with variant_arrays.d? 13 * TODO: Maybe growth logic can be hidden inside a wrapper Allocator 14 * 15 * See_Also: http://forum.dlang.org/post/wvulryummkqtskiwrusb@forum.dlang.org 16 * See_Also: https://forum.dlang.org/post/purhollnapramxczmcka@forum.dlang.org 17 * See_Also: https://forum.dlang.org/post/cvxuagislrpfomalcelc@forum.dlang.org 18 * See_Also: https://maikklein.github.io/post/soa-d/ 19 */ 20 module nxt.container.soa; 21 22 import std.experimental.allocator.mallocator : Mallocator; 23 import nxt.allocator_traits : isAllocator; 24 25 /** Structure of Arrays similar to members of `S`. 26 */ 27 struct SoA(S, Capacity = size_t, Allocator = Mallocator) 28 if (is(S == struct) && // TODO: extend to `isAggregate!S`? 29 isAllocator!Allocator) 30 { 31 /** Growth factor P/Q. 32 https://github.com/facebook/folly/blob/master/folly/docs/FBVector.md#memory-handling 33 Use 1.5 like Facebook's `fbvector` does. 34 */ 35 enum _growthP = 3; // numerator 36 /// ditto 37 enum _growthQ = 2; // denominator 38 39 private alias toType(string s) = typeof(__traits(getMember, S, s)); 40 private alias Types = typeof(S.tupleof); 41 42 import std.experimental.allocator.common : stateSize; 43 static if (stateSize!Allocator != 0) 44 { 45 this(in Capacity initialCapacity, Allocator allocator) 46 { 47 _capacity = initialCapacity; 48 this.allocator = allocator; 49 allocate(initialCapacity); 50 } 51 52 this() @disable; ///< No default construction if an allocator must be provided. 53 } 54 else 55 this(in Capacity initialCapacity) 56 { 57 _capacity = initialCapacity; 58 allocate(initialCapacity); 59 } 60 61 @disable this(this); // disable copy constructor 62 63 ~this() nothrow @trusted @nogc 64 { 65 import std.experimental.allocator : dispose; 66 static foreach (const index, _; S.tupleof) 67 allocator.dispose(getArray!index); 68 } 69 70 pragma(inline, true): 71 72 Capacity length() const @property @safe pure nothrow @nogc => _length; 73 Capacity capacity() const @property @safe pure nothrow @nogc => _capacity; 74 bool empty() const @property @safe pure nothrow @nogc => _length == 0; 75 76 inout(SoAElementRef!S) opIndex()(in Capacity elementIndex) inout return /* template-lazy */ 77 in(elementIndex < _length) 78 => typeof(return)(&this, elementIndex); 79 80 inout(SoASlice!S) opSlice()() inout return /* template-lazy */ 81 => typeof(return)(&this); 82 83 void opOpAssign(string op, S)(S value) if (op == "~") => insertBack(value); 84 85 pragma(inline): // reset 86 87 auto opDispatch(string name)() 88 { 89 static foreach (const index, memberSymbol; S.tupleof) 90 static if (name == memberSymbol.stringof) 91 return getArray!index; 92 // TODO: static assert(0, S.stringof ~ " has no field named " ~ name); 93 } 94 95 /** Push element (struct) `value` to back of array. */ 96 void insertBack()(S value) @trusted /* template-lazy */ 97 { 98 import core.lifetime : moveEmplace; 99 reserveOneExtra(); 100 static foreach (const index, memberSymbol; S.tupleof) 101 moveEmplace(__traits(getMember, value, memberSymbol.stringof), 102 getArray!index[_length]); // TODO: assert that 103 ++_length; 104 } 105 106 /** Push element (struct) `value` to back of array using its data members `members`. */ 107 void insertBackMembers()(Types members) @trusted /* template-lazy */ 108 { 109 import core.lifetime : moveEmplace; 110 reserveOneExtra(); 111 // move each member to its position respective array 112 static foreach (const index, _; members) 113 moveEmplace(members[index], getArray!index[_length]); // same as `getArray!index[_length] = members[index];` 114 ++_length; 115 } 116 117 private: 118 import nxt.allocator_traits : AllocatorState; 119 mixin AllocatorState!Allocator; // put first as emsi-containers do 120 121 // generate array definitions 122 static foreach (const index, Type; Types) 123 mixin(Type.stringof ~ `[] _container` ~ index.stringof ~ ";"); 124 125 /** Get array of all fields at aggregate field index `index`. */ 126 pragma(inline, true) 127 ref inout(Types[index][]) getArray(Capacity index)() inout return 128 { 129 mixin(`return _container` ~ index.stringof ~ ";"); 130 } 131 132 Capacity _length = 0; ///< Current length. 133 Capacity _capacity = 0; ///< Current capacity. 134 135 void allocate(in Capacity newCapacity) @trusted 136 { 137 import std.experimental.allocator : makeArray; 138 static foreach (const index, _; S.tupleof) 139 getArray!index = allocator.makeArray!(Types[index])(newCapacity); 140 } 141 142 /** Grow storage with at least on element. */ 143 void grow() @trusted 144 { 145 // Motivation: https://github.com/facebook/folly/blob/master/folly/docs/FBVector.md#memory-handling 146 import std.algorithm.comparison : max; 147 import std.experimental.allocator : expandArray; 148 const newCapacity = _capacity == 1 ? 2 : max(1, _growthP * _capacity / _growthQ); 149 const expandSize = newCapacity - _capacity; 150 if (_capacity is 0) 151 allocate(newCapacity); 152 else 153 static foreach (const index, _; S.tupleof) 154 allocator.expandArray(getArray!index, expandSize); 155 _capacity = newCapacity; 156 } 157 158 void reserveOneExtra() 159 { 160 if (_length == _capacity) 161 grow(); 162 } 163 } 164 alias StructArrays = SoA; 165 166 /** Reference to element in `soaPtr` at index `elementIndex`. */ 167 private struct SoAElementRef(S) 168 if (is(S == struct)) // TODO: extend to `isAggregate!S`? 169 { 170 SoA!S* soaPtr; 171 size_t elementIndex; 172 173 @disable this(this); 174 175 /** Access member name `memberName`. */ 176 auto ref opDispatch(string memberName)() @trusted return scope 177 { 178 mixin(`return ` ~ `(*soaPtr).` ~ memberName ~ `[elementIndex];`); 179 } 180 } 181 182 /** Reference to slice in `soaPtr`. */ 183 private struct SoASlice(S) 184 if (is(S == struct)) // TODO: extend to `isAggregate!S`? 185 { 186 SoA!S* soaPtr; 187 188 @disable this(this); 189 190 /** Access aggregate at `index`. */ 191 inout(S) opIndex(in size_t index) inout @trusted return scope 192 { 193 S s = void; 194 static foreach (const memberIndex, memberSymbol; S.tupleof) 195 mixin(`s.` ~ memberSymbol.stringof ~ `= (*soaPtr).getArray!` ~ memberIndex.stringof ~ `[index];`); 196 return s; 197 } 198 } 199 200 @safe pure nothrow @nogc unittest 201 { 202 import nxt.dip_traits : hasPreviewDIP1000; 203 204 struct S { int i; float f; } 205 206 auto x = SoA!S(); 207 208 static assert(is(typeof(x.getArray!0()) == int[])); 209 static assert(is(typeof(x.getArray!1()) == float[])); 210 211 assert(x.length == 0); 212 213 x.insertBack(S.init); 214 assert(x.length == 1); 215 216 x ~= S.init; 217 assert(x.length == 2); 218 219 x.insertBackMembers(42, 43f); 220 assert(x.length == 3); 221 assert(x.i[2] == 42); 222 assert(x.f[2] == 43f); 223 224 // uses opDispatch 225 assert(x[2].i == 42); 226 assert(x[2].f == 43f); 227 228 const x3 = SoA!S(3); 229 assert(x3.length == 0); 230 assert(x3.capacity == 3); 231 232 // TODO: make foreach work 233 // foreach (_; x[]) 234 // { 235 // } 236 237 static if (hasPreviewDIP1000) 238 { 239 static assert(!__traits(compiles, 240 { 241 ref int testScope() @safe 242 { 243 auto y = SoA!S(1); 244 y ~= S(42, 43f); 245 return y[0].i; 246 } 247 })); 248 } 249 }