1 /* 2 Copyright (c) 2016-2021 Eugene Wissner 3 4 Boost Software License - Version 1.0 - August 17th, 2003 5 6 Permission is hereby granted, free of charge, to any person or organization 7 obtaining a copy of the software and accompanying documentation covered by 8 this license (the "Software") to use, reproduce, display, distribute, 9 execute, and transmit the Software, and to prepare derivative works of the 10 Software, and to permit third-parties to whom the Software is furnished to 11 do so, all subject to the following: 12 13 The copyright notices in the Software and this entire statement, including 14 the above license grant, this restriction and the following disclaimer, 15 must be included in all copies of the Software, in whole or in part, and 16 all derivative works of the Software, unless such copies or derivative 17 works are solely in the form of machine-executable object code generated by 18 a source language processor. 19 20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 23 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 24 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 25 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 DEALINGS IN THE SOFTWARE. 27 */ 28 29 /** 30 * Fast block-based allocator 31 * 32 * Copyright: Eugene Wissner 2016-2021. 33 * License: $(LINK2 boost.org/LICENSE_1_0.txt, Boost License 1.0). 34 * Authors: Eugene Wissner 35 */ 36 module dlib.memory.mmappool; 37 38 import dlib.memory.allocator; 39 import core.atomic; 40 import core.exception; 41 42 version (Posix) 43 { 44 import core.stdc.errno; 45 import core.sys.posix.sys.mman; 46 import core.sys.posix.unistd; 47 } 48 else version (Windows) 49 { 50 import core.sys.windows.winbase; 51 import core.sys.windows.windows; 52 } 53 54 /** 55 * This allocator allocates memory in regions (multiple of 4 KB for example). 56 * Each region is then splitted in blocks. So it doesn't request the memory 57 * from the operating system on each call, but only if there are no large 58 * enough free blocks in the available regions. 59 * Deallocation works in the same way. Deallocation doesn't immediately 60 * gives the memory back to the operating system, but marks the appropriate 61 * block as free and only if all blocks in the region are free, the complete 62 * region is deallocated. 63 * 64 * ---------------------------------------------------------------------------- 65 * | | | | | || | | | 66 * | |prev <----------- | || | | | 67 * | R | B | | B | || R | B | | 68 * | E | L | | L | next E | L | | 69 * | G | O | DATA | O | FREE ---> G | O | DATA | 70 * | I | C | | C | <--- I | C | | 71 * | O | K | | K | prev O | K | | 72 * | N | -----------> next| || N | | | 73 * | | | | | || | | | 74 * ---------------------------------------------------------------------------- 75 * 76 * TODO: 77 * $(UL 78 * $(LI Thread safety (core.atomic.cas)) 79 * $(LI If two neighbour blocks are free, they can be merged) 80 * $(LI Reallocation shoud check if there is enough free space in the 81 * next block instead of always moving the memory) 82 * $(LI Make 64 KB regions mininmal region size on Linux) 83 * ) 84 */ 85 class MmapPool : Allocator 86 { 87 //@disable this(); 88 89 static initialize() 90 { 91 version (Posix) 92 { 93 pageSize = sysconf(_SC_PAGE_SIZE); 94 } 95 else version (Windows) 96 { 97 SYSTEM_INFO si; 98 GetSystemInfo(&si); 99 pageSize = si.dwPageSize; 100 } 101 } 102 103 /** 104 * Allocates $(D_PARAM size) bytes of memory. 105 * 106 * Params: 107 * size = Amount of memory to allocate. 108 * 109 * Returns: The pointer to the new allocated memory. 110 */ 111 void[] allocate(size_t size) @nogc @trusted nothrow 112 { 113 if (!size) 114 { 115 return null; 116 } 117 immutable dataSize = addAlignment(size); 118 119 void* data = findBlock(dataSize); 120 if (data is null) 121 { 122 data = initializeRegion(dataSize); 123 } 124 125 return data is null ? null : data[0..size]; 126 } 127 128 /// 129 @nogc @safe nothrow unittest 130 { 131 auto p = MmapPool.instance.allocate(20); 132 133 assert(p); 134 135 MmapPool.instance.deallocate(p); 136 } 137 138 /** 139 * Search for a block large enough to keep $(D_PARAM size) and split it 140 * into two blocks if the block is too large. 141 * 142 * Params: 143 * size = Minimum size the block should have. 144 * 145 * Returns: Data the block points to or $(D_KEYWORD null). 146 */ 147 private void* findBlock(size_t size) @nogc nothrow 148 { 149 Block block1; 150 RegionLoop: for (auto r = head; r !is null; r = r.next) 151 { 152 block1 = cast(Block) (cast(void*) r + regionEntrySize); 153 do 154 { 155 if (block1.free && block1.size >= size) 156 { 157 break RegionLoop; 158 } 159 } 160 while ((block1 = block1.next) !is null); 161 } 162 if (block1 is null) 163 { 164 return null; 165 } 166 else if (block1.size >= size + alignment + blockEntrySize) 167 { // Split the block if needed 168 Block block2 = cast(Block) (cast(void*) block1 + blockEntrySize + size); 169 block2.prev = block1; 170 if (block1.next is null) 171 { 172 block2.next = null; 173 } 174 else 175 { 176 block2.next = block1.next.next; 177 } 178 block1.next = block2; 179 180 block1.free = false; 181 block2.free = true; 182 183 block2.size = block1.size - blockEntrySize - size; 184 block1.size = size; 185 186 block2.region = block1.region; 187 //atomicOp!"+="(block1.region.blocks, 1); // atomicOp works only with shared data 188 ++block1.region.blocks; 189 } 190 else 191 { 192 block1.free = false; 193 //atomicOp!"+="(block1.region.blocks, 1); // atomicOp works only with shared data 194 ++block1.region.blocks; 195 } 196 return cast(void*) block1 + blockEntrySize; 197 } 198 199 /** 200 * Deallocates a memory block. 201 * 202 * Params: 203 * p = A pointer to the memory block to be freed. 204 * 205 * Returns: Whether the deallocation was successful. 206 */ 207 bool deallocate(void[] p) @nogc @trusted nothrow 208 { 209 if (p is null) 210 { 211 return true; 212 } 213 214 Block block = cast(Block) (p.ptr - blockEntrySize); 215 if (block.region.blocks <= 1) 216 { 217 if (block.region.prev !is null) 218 { 219 block.region.prev.next = block.region.next; 220 } 221 else // Replace the list head. It is being deallocated 222 { 223 head = block.region.next; 224 } 225 if (block.region.next !is null) 226 { 227 block.region.next.prev = block.region.prev; 228 } 229 version (Posix) 230 { 231 return munmap(cast(void*) block.region, block.region.size) == 0; 232 } 233 version (Windows) 234 { 235 return VirtualFree(cast(void*) block.region, 0, MEM_RELEASE) == 0; 236 } 237 } 238 else 239 { 240 block.free = true; 241 //atomicOp!"-="(block.region.blocks, 1); // atomicOp works only with shared data 242 --block.region.blocks; 243 return true; 244 } 245 } 246 247 /// 248 @nogc @safe nothrow unittest 249 { 250 auto p = MmapPool.instance.allocate(20); 251 252 assert(MmapPool.instance.deallocate(p)); 253 } 254 255 /** 256 * Increases or decreases the size of a memory block. 257 * 258 * Params: 259 * p = A pointer to the memory block. 260 * size = Size of the reallocated block. 261 * 262 * Returns: Whether the reallocation was successful. 263 */ 264 bool reallocate(ref void[] p, size_t size) @nogc @trusted nothrow 265 { 266 void[] reallocP; 267 268 if (size == p.length) 269 { 270 return true; 271 } 272 else if (size > 0) 273 { 274 reallocP = allocate(size); 275 if (reallocP is null) 276 { 277 return false; 278 } 279 } 280 281 if (p !is null) 282 { 283 if (size > p.length) 284 { 285 reallocP[0..p.length] = p[0..$]; 286 } 287 else if (size > 0) 288 { 289 reallocP[0..size] = p[0..size]; 290 } 291 deallocate(p); 292 } 293 p = reallocP; 294 295 return true; 296 } 297 298 /// 299 @nogc nothrow unittest 300 { 301 void[] p; 302 MmapPool.instance.reallocate(p, 10 * int.sizeof); 303 (cast(int[]) p)[7] = 123; 304 305 assert(p.length == 40); 306 307 MmapPool.instance.reallocate(p, 8 * int.sizeof); 308 309 assert(p.length == 32); 310 assert((cast(int[]) p)[7] == 123); 311 312 MmapPool.instance.reallocate(p, 20 * int.sizeof); 313 (cast(int[]) p)[15] = 8; 314 315 assert(p.length == 80); 316 assert((cast(int[]) p)[15] == 8); 317 assert((cast(int[]) p)[7] == 123); 318 319 MmapPool.instance.reallocate(p, 8 * int.sizeof); 320 321 assert(p.length == 32); 322 assert((cast(int[]) p)[7] == 123); 323 324 MmapPool.instance.deallocate(p); 325 } 326 327 /** 328 * Static allocator instance and initializer. 329 * 330 * Returns: Global $(D_PSYMBOL MmapPool) instance. 331 */ 332 static @property MmapPool instance() @nogc @trusted nothrow 333 { 334 if (instance_ is null) 335 { 336 initialize(); 337 338 immutable instanceSize = addAlignment(__traits(classInstanceSize, MmapPool)); 339 340 Region head; // Will become soon our region list head 341 void* data = initializeRegion(instanceSize, head); 342 if (data !is null) 343 { 344 data[0..instanceSize] = typeid(MmapPool).initializer[]; 345 instance_ = cast(MmapPool) data; 346 instance_.head = head; 347 } 348 } 349 return instance_; 350 } 351 352 /// 353 @nogc @safe nothrow unittest 354 { 355 assert(instance is instance); 356 } 357 358 /** 359 * Initializes a region for one element. 360 * 361 * Params: 362 * size = Aligned size of the first data block in the region. 363 * head = Region list head. 364 * 365 * Returns: A pointer to the data. 366 */ 367 pragma(inline) 368 private static void* initializeRegion(size_t size, 369 ref Region head) @nogc nothrow 370 { 371 immutable regionSize = calculateRegionSize(size); 372 373 version (Posix) 374 { 375 void* p = mmap(null, 376 regionSize, 377 PROT_READ | PROT_WRITE, 378 MAP_PRIVATE | MAP_ANON, 379 -1, 380 0); 381 if (p is MAP_FAILED) 382 { 383 if (errno == ENOMEM) 384 { 385 onOutOfMemoryError(); 386 } 387 return null; 388 } 389 } 390 else version (Windows) 391 { 392 void* p = VirtualAlloc(null, 393 regionSize, 394 MEM_COMMIT, 395 PAGE_READWRITE); 396 if (p is null) 397 { 398 if (GetLastError() == ERROR_NOT_ENOUGH_MEMORY) 399 { 400 onOutOfMemoryError(); 401 } 402 return null; 403 } 404 } 405 406 Region region = cast(Region) p; 407 region.blocks = 1; 408 region.size = regionSize; 409 410 // Set the pointer to the head of the region list 411 if (head !is null) 412 { 413 head.prev = region; 414 } 415 region.next = head; 416 region.prev = null; 417 head = region; 418 419 // Initialize the data block 420 void* memoryPointer = p + regionEntrySize; 421 Block block1 = cast(Block) memoryPointer; 422 block1.size = size; 423 block1.free = false; 424 425 // It is what we want to return 426 void* data = memoryPointer + blockEntrySize; 427 428 // Free block after data 429 memoryPointer = data + size; 430 Block block2 = cast(Block) memoryPointer; 431 block1.prev = block2.next = null; 432 block1.next = block2; 433 block2.prev = block1; 434 block2.size = regionSize - size - regionEntrySize - blockEntrySize * 2; 435 block2.free = true; 436 block1.region = block2.region = region; 437 438 return data; 439 } 440 441 /// Ditto. 442 private void* initializeRegion(size_t size) @nogc nothrow 443 { 444 return initializeRegion(size, head); 445 } 446 447 /** 448 * Params: 449 * x = Space to be aligned. 450 * 451 * Returns: Aligned size of $(D_PARAM x). 452 */ 453 pragma(inline) 454 private static immutable(size_t) addAlignment(size_t x) 455 @nogc @safe pure nothrow 456 out (result) 457 { 458 assert(result > 0); 459 } 460 do 461 { 462 return (x - 1) / alignment_ * alignment_ + alignment_; 463 } 464 465 /** 466 * Params: 467 * x = Required space. 468 * 469 * Returns: Minimum region size (a multiple of $(D_PSYMBOL pageSize)). 470 */ 471 pragma(inline) 472 private static immutable(size_t) calculateRegionSize(size_t x) 473 @nogc nothrow 474 out (result) 475 { 476 assert(result > 0); 477 } 478 do 479 { 480 x += regionEntrySize + blockEntrySize * 2; 481 return x / pageSize * pageSize + pageSize; 482 } 483 484 @property immutable(uint) alignment() const @nogc @safe pure nothrow 485 { 486 return alignment_; 487 } 488 private enum alignment_ = 8; 489 490 private static __gshared MmapPool instance_; 491 492 private static __gshared size_t pageSize; 493 494 private struct RegionEntry 495 { 496 Region prev; 497 Region next; 498 uint blocks; 499 size_t size; 500 } 501 private alias Region = RegionEntry*; 502 private enum regionEntrySize = 32; 503 504 private Region head; 505 506 private struct BlockEntry 507 { 508 Block prev; 509 Block next; 510 bool free; 511 size_t size; 512 Region region; 513 } 514 private alias Block = BlockEntry*; 515 private enum blockEntrySize = 40; 516 }