1 module mempooled.dynamic;
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  * Memory pool implemented ower linked list of allocated blocks that are being reused.
12  * If there is no preallocated block in the pool, it allocates the new one from the heap.
13  * When block is returned to the pool, it's just prepended at the start of the unused blocks to be available again.
14  *
15  * Unused blocks are freed only when `clear()` is called on the pool or when pool itself is cleared out by ROI.
16  */
17 struct DynamicPool(size_t blockSize)
18 {
19     nothrow @nogc:
20 
21     static assert(blockSize >= 8, "blockSize must be equal or greater than (void*).sizeof");
22 
23     private
24     {
25         static assert(Block.sizeof == 8);
26 
27         struct Block { Block* next; }
28         struct Payload
29         {
30             nothrow @nogc @safe pure:
31 
32             Block* pool;        // Beginning of unused blocks
33             uint numFreeBlocks; // Number of available memory blocks
34             uint numUsedBlocks; // Number of provided memory blocks
35             size_t refs;        // Number of references
36 
37             ~this() @trusted
38             {
39                 assert(!numUsedBlocks, "There are still some not yet returned memory blocks");
40                 while (pool)
41                 {
42                     auto next = pool.next;
43                     pureFree(pool);
44                     numFreeBlocks--;
45                     pool = next;
46                 }
47                 assert(!numFreeBlocks, "Should be zero");
48             }
49         }
50 
51         Payload* pay;
52     }
53 
54     /// Copy constructor
55     this(ref return scope typeof(this) rhs) pure @safe
56     {
57         if (rhs.pay is null) return;
58         else
59         {
60             this.pay = rhs.pay;
61             this.pay.refs++;
62         }
63     }
64 
65     /// Available number of preallocated blocks
66     size_t capacity() const pure @safe
67     {
68         if (_expect(pay is null, false)) return 0;
69         return pay.numFreeBlocks;
70     }
71 
72     /// Destructor
73     ~this() pure @safe
74     {
75         import std.traits : hasElaborateDestructor;
76         if (pay)
77         {
78             pay.refs--;
79             // debug printf("destroy: refs=%d\n", pay.refs);
80             if (pay.refs == 0)
81             {
82                 // debug printf("free\n");
83                 destroy(*pay); // call payload destructor
84                 () @trusted { pureFree(pay); } ();
85             }
86         }
87     }
88 
89     /**
90      * Returns memory block of the request size.
91      *
92      * If there is no preallocated block in the pool, new'd be allocated (with `blockSize` length
93      * regardless of `len` parameter).
94      *
95      * Params:
96      *   len - size of wanted block (must by lower or equal to a pool's `blockSize`)
97      *
98      * Returns:
99      *   Address of the memory block or null if it fails to allocate.
100      */
101     void* alloc(size_t len) @safe pure
102     in (len <= blockSize)
103     {
104         if (_expect(pay is null, false))
105         {
106             import core.exception : onOutOfMemoryError;
107             pay = () @trusted { return cast(Payload*)pureMalloc(Payload.sizeof); }();
108             if (!pay) onOutOfMemoryError();
109             emplace(pay);
110             pay.refs = 1;
111             goto newBlock;
112         }
113 
114         if (pay.pool !is null)
115         {
116             void* ret = cast(void*)pay.pool;
117             pay.pool = pay.pool.next;
118             pay.numFreeBlocks--;
119             pay.numUsedBlocks++;
120             return ret;
121         }
122 
123         newBlock:
124         auto res = () @trusted { return pureMalloc(blockSize); }();
125         if (res) pay.numUsedBlocks++;
126         return res;
127     }
128 
129     /**
130      * Allocates requested type over pooled memory block and returns it.
131      * `onOutOfMemoryError` is called when new memory block fails to be allocated.
132      */
133     T* alloc(T, ARGS...)(auto ref ARGS args)
134     {
135         import core.exception : onOutOfMemoryError;
136 
137         static assert(T.sizeof <= blockSize, "Requested type don't fit in pool's blocks size");
138         auto mem = () @trusted { return cast(T*)alloc(T.sizeof); }();
139         if (!mem) onOutOfMemoryError();
140         return mem.emplace(forward!args);
141     }
142 
143     /**
144      * Returns block of memory back to the pool.
145      * Deallocating pointer to a memory not returned by the pool has undefined behavior.
146      */
147     void dealloc(ref void* ptr) @system
148     in (ptr, "Null ptr provided")
149     in (pay, "Pool not initialized")
150     {
151         auto block = cast(Block*)ptr;
152         block.next = pay.pool;
153         pay.pool = block;
154         pay.numUsedBlocks--;
155         pay.numFreeBlocks++;
156         ptr = null;
157     }
158 
159     /// ditto
160     void dealloc(T)(ref T* p) if (!is(T == void))
161     {
162         import std.traits : hasElaborateDestructor;
163         static if (hasElaborateDestructor!T)
164             destroy(*p); // call possible destructors
165 
166         dealloc(*(cast(void**)&p));
167     }
168 
169     /**
170      * Clears all currently preallocated memory blocks.
171      */
172     void clear() @safe pure
173     in (pay, "Pool not initialized")
174     {
175         while (pay.pool)
176         {
177             auto n = pay.pool.next;
178             () @trusted { pureFree(pay.pool); }();
179             pay.pool = n;
180             pay.numFreeBlocks--;
181         }
182         assert(pay.numFreeBlocks == 0);
183     }
184 }
185 
186 ///
187 @("Usage tests")
188 unittest
189 {
190     DynamicPool!1024 pool; // each block is 1024B large
191     auto n = pool.alloc!int(42); // allocates whole 1024B block for just an 4B large number
192     assert(*n == 42);
193 
194     auto buf = pool.alloc!(ubyte[1024])(); // uses whole block and zeroes the array
195     foreach (i; 0..1024) assert((*buf)[i] == 0);
196 
197     void* vbuf = pool.alloc(1024); // uses whole block that we can use as we please - block memory is uninitialized
198     assert(vbuf !is null);
199 
200     // FixedPool over DynamicPool memory block
201     import mempooled.fixed : fixedPool;
202     auto fpblock = cast(ubyte*)pool.alloc(1024);
203     auto fpool = fixedPool!(8, 128)(fpblock[0..1024]);
204     auto x = fpool.alloc!int(666);
205     assert(*x == 666);
206     fpool.dealloc(x);
207 
208     assert(pool.pay.numUsedBlocks == 4);
209     pool.dealloc(n);
210     pool.dealloc(buf);
211     pool.dealloc(vbuf);
212     pool.dealloc(fpblock);
213 }
214 
215 @("Pool integrity tests")
216 @safe unittest
217 {
218     DynamicPool!1024 pool;
219 
220     assert(pool.pay is null);
221     void* block = pool.alloc(10);
222     assert(pool.pay !is null);
223     assert(pool.pay.refs == 1);
224     assert(pool.pay.numFreeBlocks == 0);
225     assert(pool.pay.numUsedBlocks == 1);
226     assert(block !is null);
227     () @trusted { (cast(ubyte*)block)[8] = 42; }(); // write 42 after Block next pointer
228     assert(pool.capacity == 0);
229     () @trusted { pool.dealloc(block); }();
230     assert(block is null);
231     assert(pool.pay.numFreeBlocks == 1);
232     assert(pool.pay.numUsedBlocks == 0);
233     assert(pool.capacity == 1);
234     block = pool.alloc(1024);
235     assert(pool.pay.numFreeBlocks == 0);
236     assert(pool.pay.numUsedBlocks == 1);
237     assert(block !is null);
238     assert(() @trusted { return (cast(ubyte*)block)[8]; }() == 42);
239     assert(pool.capacity == 0);
240     () @trusted { pool.dealloc(block); }();
241 
242     struct Foo { int n; }
243     Foo* f = pool.alloc!Foo(42);
244     assert(f.n == 42);
245     () @trusted { pool.dealloc(f); }();
246     assert(f is null);
247 
248     assert(pool.pay.numFreeBlocks == 1);
249     pool.clear();
250     assert(pool.pay.numFreeBlocks == 0);
251 }