1 module mempooled.fixed;
2 
3 debug import core.stdc.stdio;
4 import core.stdc.stdlib;
5 import std.conv : 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 the same as used T.sizeof");
62     }
63 
64     private
65     {
66         struct Payload
67         {
68             nothrow @nogc:
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                 assert(buffer is null || buffer.length == numBlocks * blockSize, "Provided buffer has wrong size, must be numBlocks*blockSize");
80                 if (buffer) memStart = &buffer[0];
81                 else
82                 {
83                     memStart = cast(ubyte*)calloc(numBlocks, blockSize);
84                     assert(memStart, "failed to allocate pool");
85                     ownPool = true;
86                 }
87 
88                 next = memStart;
89                 numFreeBlocks = numBlocks;
90                 refs = 1;
91             }
92 
93             ~this()
94             {
95                 assert(memStart, "memStart is null");
96                 if (ownPool) free(memStart);
97             }
98         }
99 
100         Payload* pay;
101     }
102 
103     /// Copy constructor
104     this(ref return scope typeof(this) rhs)
105     {
106         // debug printf("copy\n");
107         if (rhs.pay is null) initPool();
108         else
109         {
110             this.pay = rhs.pay;
111             this.pay.refs++;
112         }
113     }
114 
115     /// Destructor
116     ~this()
117     {
118         if (pay)
119         {
120             pay.refs--;
121             // debug printf("destroy: refs=%d\n", pay.refs);
122             if (pay.refs == 0)
123             {
124                 // debug printf("free\n");
125                 destroy(*pay); // call payload destructor
126                 free(pay);
127             }
128         }
129     }
130 
131     /// Available number of items / blocks that can be allocated
132     size_t capacity() const
133     {
134         if (pay is null) return numBlocks;
135         return pay.numFreeBlocks;
136     }
137 
138     static if (is(T == void)) // allow to allocate anything that fits
139     {
140         /**
141          * Allocates item of requested type from the pool.
142          *
143          * Size of the requested item type must be less or equal to the pool block size.
144          *
145          * Params: args = optional args to be used to initialize item
146          * Returns: pointer to the allocated item or `null` if the pool is already depleted.
147          */
148         U* alloc(U, ARGS...)(ARGS args)
149         {
150             pragma(inline)
151             static assert(U.sizeof <= blockSize, format!"Can't allocate %s of size %s with blockSize=%s"(U, U.sizeof, blockSize));
152             void* p = allocImpl();
153             if (p) return emplace(cast(U*)p, args);
154             return null;
155         }
156 
157         /**
158          * Returns previously allocated item back to the pool.
159          *
160          * If the item type has a destructor it is called.
161          *
162          * Params: p = allocated item pointer
163          */
164         void dealloc(U)(U* p)
165         {
166             pragma(inline, true)
167             deallocImpl(p);
168         }
169     }
170     else
171     {
172         /**
173          * Allocates item from the pool.
174          *
175          * Params: args = optional args to be used to initialize item
176          * Returns: pointer to the allocated item or `null` if the pool is already depleted.
177          */
178         T* alloc(ARGS...)(ARGS args)
179         {
180             pragma(inline)
181             void* p = allocImpl();
182             if (p) return emplace(cast(T*)p, args);
183             return null;
184         }
185 
186         /**
187          * Returns previously allocated item back to the pool.
188          *
189          * If the item type has a destructor it is called.
190          *
191          * Params: p = allocated item pointer
192          */
193         void dealloc(T* p)
194         {
195             pragma(inline, true)
196             deallocImpl(p);
197         }
198     }
199 
200     private:
201 
202     void initPool(ubyte[] buffer = null)
203     {
204         assert(pay is null);
205         // debug printf("init\n");
206         pay = cast(Payload*)malloc(Payload.sizeof);
207         emplace(pay);
208         pay.initialize(buffer);
209     }
210 
211     void* allocImpl()
212     {
213         if (pay is null) initPool();
214 
215         // make sure that list of unused blocks is correct when allocating
216         if (pay.numInitialized < numBlocks)
217         {
218             uint* p = cast(uint*)addrFromIdx(pay.memStart, pay.numInitialized);
219             *p = ++pay.numInitialized;
220         }
221 
222         void* ret;
223         if (pay.numFreeBlocks > 0)
224         {
225             ret = cast(void*)pay.next;
226             if (--pay.numFreeBlocks != 0)
227                 pay.next = addrFromIdx(pay.memStart, *(cast(uint*)pay.next));
228             else pay.next = null;
229         }
230         return ret;
231     }
232 
233     void deallocImpl(U)(U* p)
234     {
235         assert(
236             cast(ubyte*)p >= pay.memStart && cast(ubyte*)p < (pay.memStart + numBlocks*blockSize),
237             "Out of bounds"
238         );
239         assert((cast(ubyte*)p - pay.memStart) % blockSize == 0, "Invalid item memory offset");
240 
241         import std.traits : hasElaborateDestructor;
242         static if (hasElaborateDestructor!U)
243             destroy(*p); // call possible destructors
244 
245         // store index of prev next to newly returned item
246         if (pay.next !is null)
247             *(cast(uint*)p) = idxFromAddr(pay.memStart, pay.next);
248         else
249             *(cast(uint*)p) = numBlocks;
250 
251         // and use returned item as next free one
252         pay.next = cast(ubyte*)p;
253         ++pay.numFreeBlocks;
254     }
255 
256     static ubyte* addrFromIdx(const ubyte* mstart, uint i) pure
257     {
258         pragma(inline, true)
259         return cast(ubyte*)(mstart + ( i * blockSize));
260     }
261 
262     static uint idxFromAddr(const ubyte* mstart, const ubyte* p) pure
263     {
264         pragma(inline, true)
265         return ((cast(uint)(p - mstart)) / blockSize);
266     }
267 }
268 
269 @("internal implementation test")
270 unittest
271 {
272     auto pool = fixedPool!(int, 10);
273     foreach (i; 0..10)
274     {
275         auto item = pool.alloc;
276         assert(*item == 0);
277         *item = i;
278     }
279     assert(pool.capacity == 0);
280     assert(pool.pay.next is null);
281 
282     pool.dealloc(cast(int*)pool.pay.memStart); // dealocate first
283     assert(pool.capacity == 1);
284     assert(pool.pay.next == pool.pay.memStart);
285 
286     auto i = pool.alloc();
287     assert(pool.capacity == 0);
288     assert(pool.pay.next is null);
289 
290     pool.dealloc(cast(int*)pool.pay.memStart); // dealocate it back
291     pool.dealloc(cast(int*)(pool.pay.memStart + int.sizeof*9)); // deallocate last one
292     auto p = pool.alloc;
293     assert(pool.pay.next == pool.pay.memStart);
294     assert(cast(ubyte*)p == pool.pay.memStart + int.sizeof*9);
295 }
296 
297 @("payload destructor")
298 unittest
299 {
300     static int refs;
301     struct Foo
302     {
303         ~this() nothrow @nogc { refs--; }
304     }
305 
306     auto pool = fixedPool!(Foo, 10);
307     alias PFoo = Foo*;
308 
309     PFoo[10] foos;
310     foreach (i; 0..10) foos[i] = pool.alloc();
311     refs = 10;
312     foreach (i; 0..10) pool.dealloc(foos[i]);
313     assert(refs == 0);
314 }
315 
316 @("capacity")
317 unittest
318 {
319     auto pool = fixedPool!(int, 10);
320     assert(pool.capacity == 10);
321     foreach (_; 0..10)
322     {
323         auto p = pool.alloc();
324         assert(p !is null);
325     }
326     assert(pool.capacity == 0);
327     assert(pool.alloc() is null); // no more space
328 }
329 
330 @("untyped")
331 unittest
332 {
333     static int refs;
334     struct Foo
335     {
336         ~this() nothrow @nogc { refs--; }
337     }
338 
339     struct Bar
340     {
341         int baz;
342         this(int i) nothrow @nogc { baz = i; }
343         ~this() nothrow @nogc { refs--; }
344     }
345 
346     auto pool = fixedPool!(100, 1);
347     assert(pool.capacity == 1);
348     auto f = pool.alloc!(Foo);
349     refs++;
350     pool.dealloc(f);
351     f = null;
352     assert(refs == 0);
353 
354     auto b = pool.alloc!Bar(42);
355     refs++;
356     assert(b.baz == 42);
357     pool.dealloc(b);
358     b = null;
359     assert(refs == 0);
360 
361     auto x = pool.alloc!int();
362     assert(x !is null);
363     auto y = pool.alloc!int();
364     assert(y is null);
365 }
366 
367 @("copy")
368 unittest
369 {
370     auto pool = fixedPool!(int, 10);
371     assert(pool.pay.refs == 1);
372     auto pool2 = pool;
373     assert(pool.pay.refs == 2);
374     assert(pool.pay is pool2.pay);
375 }
376 
377 @("custom buffer")
378 unittest
379 {
380     auto buf = (cast(ubyte*)malloc(int.sizeof * 1024))[0..1024*int.sizeof];
381     auto pool = fixedPool!(int, 1024)(buf);
382     auto i = pool.alloc();
383     assert(cast(ubyte*)i == &buf[0]);
384 }