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 std.experimental.allocator.common : 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 /** 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 this(in Capacity initialCapacity, Allocator allocator) { 44 _capacity = initialCapacity; 45 this.allocator = allocator; 46 allocate(initialCapacity); 47 } 48 this() @disable; ///< No default construction if an allocator must be provided. 49 } else 50 this(in Capacity initialCapacity) { 51 _capacity = initialCapacity; 52 allocate(initialCapacity); 53 } 54 55 this(this) @disable; // disable copy constructor 56 57 ~this() nothrow @trusted @nogc { 58 import std.experimental.allocator : dispose; 59 static foreach (const index, _; S.tupleof) 60 allocator.dispose(getArray!index); 61 } 62 63 pragma(inline, true): 64 65 Capacity length() const @property pure nothrow @safe @nogc => _length; 66 Capacity capacity() const @property pure nothrow @safe @nogc => _capacity; 67 bool empty() const @property pure nothrow @safe @nogc => _length == 0; 68 69 inout(SoAElementRef!S) opIndex()(in Capacity elementIndex) inout return /*tlm*/ 70 in(elementIndex < _length) 71 => typeof(return)(&this, elementIndex); 72 73 inout(SoASlice!S) opSlice()() inout return /*tlm*/ 74 => typeof(return)(&this); 75 76 void opOpAssign(string op, S)(S value) if (op == "~") => insertBack(value); 77 78 pragma(inline): // reset 79 80 auto opDispatch(string name)() { 81 static foreach (const index, memberSymbol; S.tupleof) 82 static if (name == memberSymbol.stringof) 83 return getArray!index; 84 /+ TODO: static assert(0, S.stringof ~ " has no field named " ~ name); +/ 85 } 86 87 /** Push element (struct) `value` to back of array. */ 88 void insertBack()(S value) @trusted /*tlm*/ { 89 import core.lifetime : moveEmplace; 90 reserveOneExtra(); 91 static foreach (const index, memberSymbol; S.tupleof) 92 moveEmplace(__traits(getMember, value, memberSymbol.stringof), 93 getArray!index[_length]); /+ TODO: assert that +/ 94 ++_length; 95 } 96 97 /** Push element `value` to back of array using its data members `members`. */ 98 void insertBackMembers()(Types members) @trusted /*tlm*/ { 99 import core.lifetime : moveEmplace; 100 reserveOneExtra(); 101 // move each member to its position respective array 102 static foreach (const index, _; members) 103 moveEmplace(members[index], getArray!index[_length]); // same as `getArray!index[_length] = members[index];` 104 ++_length; 105 } 106 107 private: 108 import nxt.allocator_traits : AllocatorState; 109 mixin AllocatorState!Allocator; // put first as emsi-containers do 110 111 // generate array definitions 112 static foreach (const index, Type; Types) 113 mixin(Type.stringof ~ `[] _container` ~ index.stringof ~ ";"); 114 115 /** Get array of all fields at aggregate field index `index`. */ 116 pragma(inline, true) 117 ref inout(Types[index][]) getArray(Capacity index)() inout return { 118 mixin(`return _container` ~ index.stringof ~ ";"); 119 } 120 121 Capacity _length = 0; ///< Current length. 122 Capacity _capacity = 0; ///< Current capacity. 123 124 void allocate(in Capacity newCapacity) @trusted 125 { 126 import std.experimental.allocator : makeArray; 127 static foreach (const index, _; S.tupleof) 128 getArray!index = allocator.makeArray!(Types[index])(newCapacity); 129 } 130 131 /** Grow storage with at least on element. */ 132 void grow() @trusted 133 { 134 // Motivation: https://github.com/facebook/folly/blob/master/folly/docs/FBVector.md#memory-handling 135 import std.algorithm.comparison : max; 136 import std.experimental.allocator : expandArray; 137 const newCapacity = _capacity == 1 ? 2 : max(1, _growthP * _capacity / _growthQ); 138 const expandSize = newCapacity - _capacity; 139 if (_capacity is 0) 140 allocate(newCapacity); 141 else 142 static foreach (const index, _; S.tupleof) 143 allocator.expandArray(getArray!index, expandSize); 144 _capacity = newCapacity; 145 } 146 147 void reserveOneExtra() { 148 if (_length == _capacity) 149 grow(); 150 } 151 } 152 alias StructArrays = SoA; 153 154 /** Reference to element in `soaPtr` at index `elementIndex`. */ 155 private struct SoAElementRef(S) 156 if (is(S == struct)) /+ TODO: extend to `isAggregate!S`? +/ { 157 SoA!S* soaPtr; 158 size_t elementIndex; 159 160 this(this) @disable; 161 162 /** Access member name `memberName`. */ 163 auto ref opDispatch(string memberName)() @trusted return scope { 164 mixin(`return ` ~ `(*soaPtr).` ~ memberName ~ `[elementIndex];`); 165 } 166 } 167 168 /** Reference to slice in `soaPtr`. */ 169 private struct SoASlice(S) 170 if (is(S == struct)) /+ TODO: extend to `isAggregate!S`? +/ 171 { 172 SoA!S* soaPtr; 173 174 this(this) @disable; 175 176 /** Access aggregate at `index`. */ 177 inout(S) opIndex(in size_t index) inout @trusted return scope { 178 S s = void; 179 static foreach (const memberIndex, memberSymbol; S.tupleof) 180 mixin(`s.` ~ memberSymbol.stringof ~ `= (*soaPtr).getArray!` ~ memberIndex.stringof ~ `[index];`); 181 return s; 182 } 183 } 184 185 pure nothrow @safe @nogc unittest { 186 import nxt.dip_traits : hasPreviewDIP1000; 187 188 struct S { int i; float f; } 189 190 auto x = SoA!S(); 191 192 static assert(is(typeof(x.getArray!0()) == int[])); 193 static assert(is(typeof(x.getArray!1()) == float[])); 194 195 assert(x.length == 0); 196 197 x.insertBack(S.init); 198 assert(x.length == 1); 199 200 x ~= S.init; 201 assert(x.length == 2); 202 203 x.insertBackMembers(42, 43f); 204 assert(x.length == 3); 205 assert(x.i[2] == 42); 206 assert(x.f[2] == 43f); 207 208 // uses opDispatch 209 assert(x[2].i == 42); 210 assert(x[2].f == 43f); 211 212 const x3 = SoA!S(3); 213 assert(x3.length == 0); 214 assert(x3.capacity == 3); 215 216 /+ TODO: make foreach work +/ 217 // foreach (_; x[]) 218 // { 219 // } 220 221 static if (hasPreviewDIP1000) { 222 static assert(!__traits(compiles, 223 { 224 ref int testScope() @safe 225 { 226 auto y = SoA!S(1); 227 y ~= S(42, 43f); 228 return y[0].i; 229 } 230 })); 231 } 232 }