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