1 module mempooled.fixed;
2 
3 debug import core.stdc.stdio;
4 import core.memory : pureMalloc, pureCalloc, pureFree;
5 import core.lifetime : emplace, forward;
6 import mempooled.intrinsics;
7 
8 nothrow @nogc:
9 
10 /**
11  * Create instance of `FixedPool`
12  *
13  * Params:
14  *      T = type of the pooled item
15  *      blockSize = size of one block in a pool
16  *      size = number of items
17  *      buffer = optional buffer to initiate `FixedPool` over
18  */
19 auto fixedPool(T, size_t size)(ubyte[] buffer = null)
20 {
21     auto res = FixedPool!(T.sizeof, size, T)();
22     res.initPool(buffer);
23     return res;
24 }
25 
26 /// ditto
27 auto fixedPool(size_t blockSize, size_t numBlocks)(ubyte[] buffer = null)
28 {
29     auto res = FixedPool!(blockSize, numBlocks, void)();
30     res.initPool(buffer);
31     return res;
32 }
33 
34 /**
35  * Implementation of "Fast Efficient Fixed-Size Memory Pool" as described in
36  * [this](www.thinkmind.org/download.php?articleid=computation_tools_2012_1_10_80006) article.
37  *
38  * It can work as a pool for single templated type or generic pool with a fixed block size (so one
39  * can `alloc` various types with size less or equal to the block size).
40  *
41  * Minimal block size is 4B as data in blocks are used internally to form a linked list of the blocks.
42  *
43  * Internally it uses refcounted payload so can be copied around as needed.
44  *
45  * Params:
46  *      blockSize = size of one item block in a pool
47  *      numBlock = number of blocks in a pool
48  *      T = optional forced type of pooled items - if used, pool is forced to provide items of this type only
49  *
50  * See_Also: implementation here: https://github.com/green-anger/MemoryPool
51  */
52 struct FixedPool(size_t blockSize, size_t numBlocks, T = void)
53 {
54     nothrow @nogc:
55 
56     static assert(blockSize >= 4, "blockSize must be equal or greater than uint.sizeof");
57     static if (!is(T == void))
58     {
59         static assert(T.sizeof <= blockSize, "Blocksize must be greater or equal to T.sizeof");
60     }
61 
62     private
63     {
64         struct Payload
65         {
66             nothrow @nogc @safe pure:
67 
68             ubyte* memStart;        // Beginning of memory pool
69             ubyte* next;            // Num of next free block
70             uint numFreeBlocks;     // Num of remaining blocks
71             uint numInitialized;    // Num of initialized blocks
72             size_t refs;            // Number of references
73             bool ownPool;           // Is memory block allocated by us?
74 
75             void initialize(ubyte[] buffer)
76             {
77                 import core.exception : onOutOfMemoryError;
78                 if (buffer)
79                 {
80                     assert(
81                         buffer.length >= numBlocks * blockSize,
82                         "Provided buffer has wrong size, must be at least numBlocks*blockSize");
83                     memStart = &buffer[0];
84                 }
85                 else
86                 {
87                     memStart = () @trusted { return cast(ubyte*)pureCalloc(numBlocks, blockSize); }();
88                     if (!memStart) onOutOfMemoryError();
89                     ownPool = true;
90                 }
91 
92                 next = memStart;
93                 numFreeBlocks = numBlocks;
94                 refs = 1;
95             }
96 
97             ~this()
98             {
99                 assert(memStart, "memStart is null");
100                 if (ownPool) () @trusted { pureFree(memStart); }();
101             }
102         }
103 
104         Payload* pay;
105     }
106 
107     /// Copy constructor
108     this(ref return scope typeof(this) rhs) pure @safe
109     {
110         // debug printf("copy\n");
111         if (rhs.pay is null) initPool();
112         else
113         {
114             this.pay = rhs.pay;
115             this.pay.refs++;
116         }
117     }
118 
119     /// Destructor
120     ~this() pure @safe
121     {
122         import std.traits : hasElaborateDestructor;
123         if (pay)
124         {
125             pay.refs--;
126             // debug printf("destroy: refs=%d\n", pay.refs);
127             if (pay.refs == 0)
128             {
129                 // debug printf("free\n");
130                 static if (is(T == void) || hasElaborateDestructor!T) {
131                     assert(pay.numFreeBlocks == numBlocks, "Possibly not disposed allocations left in pool");
132                 }
133                 destroy(*pay); // call payload destructor
134                 () @trusted { pureFree(pay); } ();
135             }
136         }
137     }
138 
139     /// Available number of items / blocks that can be allocated
140     size_t capacity() const pure @safe
141     {
142         if (_expect(pay is null, false)) return numBlocks;
143         return pay.numFreeBlocks;
144     }
145 
146     static if (is(T == void)) // allow to allocate anything that fits
147     {
148         /**
149          * Allocates item of requested type from the pool.
150          *
151          * Size of the requested item type must be less or equal to the pool block size.
152          *
153          * Params: args = optional args to be used to initialize item
154          * Returns: pointer to the allocated item or `null` if the pool is already depleted.
155          */
156         U* alloc(U, ARGS...)(auto ref ARGS args)
157         {
158             static assert(U.sizeof <= blockSize, format!"Can't allocate %s of size %s with blockSize=%s"(U, U.sizeof, blockSize));
159             return allocImpl!(U, true)(forward!args);
160         }
161 
162         /**
163          * Similar to `alloc` but doesn't initialize the allocated value.
164          */
165         U* allocVoid(U)()
166         {
167             static assert(U.sizeof <= blockSize, format!"Can't allocate %s of size %s with blockSize=%s"(U, U.sizeof, blockSize));
168             return allocImpl!(U, false)();
169         }
170 
171         /**
172          * Returns previously allocated item back to the pool.
173          *
174          * If the item type has a destructor it is called.
175          *
176          * Params: p = allocated item pointer
177          */
178         void dealloc(U)(ref U* p)
179         {
180             pragma(inline, true)
181             deallocImpl(p);
182         }
183     }
184     else
185     {
186         /**
187          * Allocates item from the pool.
188          *
189          * Params: args = optional args to be used to initialize item
190          * Returns: pointer to the allocated item or `null` if the pool is already depleted.
191          */
192         T* alloc(ARGS...)(auto ref ARGS args)
193         {
194             pragma(inline)
195             return allocImpl!(T, true)(forward!args);
196         }
197 
198         /**
199          * Similar to `alloc` but doesn't initialize the allocated value.
200          */
201         T* allocVoid()
202         {
203             pragma(inline)
204             return allocImpl!(T, false)();
205         }
206 
207         /**
208          * Returns previously allocated item back to the pool.
209          *
210          * If the item type has a destructor it is called.
211          *
212          * Params: p = allocated item pointer
213          */
214         void dealloc(ref T* p)
215         {
216             pragma(inline, true)
217             deallocImpl(p);
218         }
219     }
220 
221     private:
222 
223     void initPool(ubyte[] buffer = null) pure @safe
224     {
225         import core.exception : onOutOfMemoryError;
226 
227         assert(pay is null);
228         // debug printf("init\n");
229         pay = () @trusted { return cast(Payload*)pureMalloc(Payload.sizeof); }();
230         if (!pay) onOutOfMemoryError();
231         emplace(pay);
232         pay.initialize(buffer);
233     }
234 
235     U* allocImpl(U, bool initialize = true, ARGS...)(auto ref ARGS args)
236     {
237         pragma(inline, true);
238         static assert(initialize || ARGS.length == 0);
239         if (_expect(pay is null, false)) initPool();
240 
241         // make sure that list of unused blocks is correct when allocating
242         if (pay.numInitialized < numBlocks)
243         {
244             uint* p = () @trusted { return cast(uint*)addrFromIdx(pay.memStart, pay.numInitialized); }();
245             *p = ++pay.numInitialized;
246         }
247 
248         if (_expect(pay.numFreeBlocks > 0, true))
249         {
250             U* ret = () @trusted { return cast(U*)pay.next; }();
251             if (_expect(--pay.numFreeBlocks != 0, true))
252                 pay.next = addrFromIdx(pay.memStart, () @trusted { return *(cast(uint*)pay.next); }());
253             else pay.next = null;
254             static if (initialize) return emplace(ret, args);
255             else return ret;
256         }
257         return null;
258     }
259 
260     void deallocImpl(U)(ref U* p)
261     {
262         pragma(inline, true);
263         assert(pay, "dealloc called on uninitialized pool");
264         assert(p, "Null pointer");
265 
266         () @trusted {
267             assert(
268                 cast(ubyte*)p >= pay.memStart && cast(ubyte*)p < (pay.memStart + numBlocks*blockSize),
269                 "Pointer out of bounds"
270             );
271             assert((cast(ubyte*)p - pay.memStart) % blockSize == 0, "Invalid item memory offset");
272         }();
273 
274         import std.traits : hasElaborateDestructor;
275         static if (hasElaborateDestructor!U)
276             destroy(*p); // call possible destructors
277 
278         // store index of prev next to newly returned item
279         uint* nip = () @trusted { return cast(uint*)p; }();
280         if (_expect(pay.next !is null, true))
281             *nip = idxFromAddr(pay.memStart, pay.next);
282         else
283             *nip = numBlocks;
284 
285         // and use returned item as next free one
286         pay.next = () @trusted { return cast(ubyte*)p; }();
287         ++pay.numFreeBlocks;
288         p = null;
289     }
290 
291     static ubyte* addrFromIdx(const ubyte* mstart, uint i) pure @trusted
292     {
293         pragma(inline, true)
294         return cast(ubyte*)(mstart + ( i * blockSize));
295     }
296 
297     static uint idxFromAddr(const ubyte* mstart, const ubyte* p) pure @trusted
298     {
299         pragma(inline, true)
300         return ((cast(uint)(p - mstart)) / blockSize);
301     }
302 }
303 
304 @("internal implementation test")
305 @safe unittest
306 {
307     auto pool = fixedPool!(int, 10);
308     foreach (i; 0..10)
309     {
310         auto item = pool.alloc;
311         assert(*item == 0);
312         *item = i;
313     }
314     assert(pool.capacity == 0);
315     assert(pool.pay.next is null);
316 
317     int* pstart = () @trusted { return cast(int*)pool.pay.memStart; }();
318     pool.dealloc(pstart); // dealocate first
319     assert(pool.capacity == 1);
320     assert(pool.pay.next == pool.pay.memStart);
321     assert(pstart is null);
322 
323     auto i = pool.alloc();
324     assert(pool.capacity == 0);
325     assert(pool.pay.next is null);
326 
327     pstart = () @trusted { return cast(int*)pool.pay.memStart; }();
328     int* pend = () @trusted { return cast(int*)(pool.pay.memStart + int.sizeof*9); }();
329     pool.dealloc(pstart); // dealocate it back
330     pool.dealloc(pend); // deallocate last one
331     auto p = pool.alloc;
332     assert(pool.pay.next == pool.pay.memStart);
333     assert(cast(ubyte*)p == () @trusted { return pool.pay.memStart + int.sizeof*9; }());
334 }
335 
336 @("payload destructor")
337 @safe unittest
338 {
339     static int refs;
340     struct Foo
341     {
342         ~this() nothrow @nogc @safe { refs--; }
343     }
344 
345     auto pool = fixedPool!(Foo, 10);
346     alias PFoo = Foo*;
347 
348     PFoo[10] foos;
349     foreach (i; 0..10) foos[i] = pool.alloc();
350     refs = 10;
351     foreach (i; 0..10) pool.dealloc(foos[i]);
352     assert(refs == 0);
353 }
354 
355 @("capacity")
356 @safe unittest
357 {
358     auto pool = fixedPool!(int, 10);
359     assert(pool.capacity == 10);
360     foreach (_; 0..10)
361     {
362         auto p = pool.alloc();
363         assert(p !is null);
364     }
365     assert(pool.capacity == 0);
366     assert(pool.alloc() is null); // no more space
367 }
368 
369 @("untyped")
370 @safe unittest
371 {
372     static int refs;
373     struct Foo
374     {
375         ~this() nothrow @nogc @safe { refs--; }
376     }
377 
378     struct Bar
379     {
380         int baz;
381         this(int i) nothrow @nogc @safe { baz = i; }
382         ~this() nothrow @nogc @safe { refs--; }
383     }
384 
385     auto pool = fixedPool!(100, 1);
386     assert(pool.capacity == 1);
387     auto f = pool.alloc!(Foo);
388     refs++;
389     pool.dealloc(f);
390     f = null;
391     assert(refs == 0);
392 
393     auto b = pool.alloc!Bar(42);
394     refs++;
395     assert(b.baz == 42);
396     pool.dealloc(b);
397     b = null;
398     assert(refs == 0);
399 
400     auto x = pool.alloc!int();
401     assert(x !is null);
402     auto y = pool.alloc!int();
403     assert(y is null);
404     pool.dealloc(x);
405 }
406 
407 @("copy")
408 @safe unittest
409 {
410     auto pool = fixedPool!(int, 10);
411     assert(pool.pay.refs == 1);
412     auto pool2 = pool;
413     assert(pool.pay.refs == 2);
414     assert(pool.pay is pool2.pay);
415 }
416 
417 @("custom buffer")
418 @safe unittest
419 {
420     auto buf = () @trusted { return (cast(ubyte*)pureMalloc(int.sizeof * 1024))[0..1024*int.sizeof]; }();
421     auto pool = fixedPool!(int, 1024)(buf);
422     auto i = pool.alloc();
423     assert(cast(ubyte*)i == &buf[0]);
424 }
425 
426 @("void initializers")
427 @safe unittest
428 {
429     {
430         auto pool = fixedPool!(int, 10);
431         auto a = pool.allocVoid();
432         auto b = pool.allocVoid();
433 
434         // next index is left in place
435         assert(*a == 1);
436         assert(*b == 2);
437     }
438 
439     {
440         auto pool = fixedPool!(4, 10);
441         auto a = pool.allocVoid!int();
442         auto b = pool.allocVoid!int();
443 
444         // next index is left in place
445         assert(*a == 1);
446         assert(*b == 2);
447         pool.dealloc(a);
448         pool.dealloc(b);
449     }
450 }
451 
452 @("noncopyable")
453 @safe unittest
454 {
455     struct Foo
456     {
457         int n;
458         @disable this(this);
459     }
460 
461     struct Bar
462     {
463         Foo* f;
464         this(ref Foo f) @trusted { this.f = &f; }
465     }
466 
467     Foo f = Foo(42);
468     auto pool = fixedPool!(Bar, 10);
469     auto b = pool.alloc(f);
470     assert(b !is null);
471     assert(b.f !is null);
472     assert(b.f.n == 42);
473 }