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 @property 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(Sink)(ref scope Sink 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 	import std.meta : AliasSeq;
239 	enum mulBy2(alias x) = x * 2;
240 	enum add(alias a, alias b) = a + b;
241 	static assert(staticMapReduce!(mulBy2, add, AliasSeq!(1)) == 2);
242 	static assert(staticMapReduce!(mulBy2, add, AliasSeq!(1, 2)) == 6);
243 	static assert(staticMapReduce!(mulBy2, add, AliasSeq!(1, 2, 3, 4, 5)) == 30);
244 }
245 
246 @system unittest {
247 	// import std.stdio : writeln;
248 
249 	static struct Vec3
250 	{
251 		float x, y, z;
252 	}
253 
254 	auto soa = SoA!Vec3(5);
255 
256 	soa.y[] = 2;
257 	soa.z[] = 3;
258 	soa.x[] = soa.y[] * soa.z[];
259 	// soa.writeln;
260 
261 	// soa[0].writeln;
262 
263 	soa[1].x++;
264 	soa[1].y += 2;
265 	soa[1].z *= 3;
266 	// soa.writeln;
267 
268 	soa.insertBack(Vec3(21, 42, 84));
269 	// soa.writeln;
270 
271 	soa.insertBack(0.5f, 0.25f, 0.125f);
272 	// soa.writeln;
273 }