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