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 }