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 }