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