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 }