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 }