The OpenD Programming Language

1 // Written in the D programming language.
2 /**
3 Source: $(PHOBOSSRC std/experimental/allocator/building_blocks/ascending_page_allocator.d)
4 */
5 module std.experimental.allocator.building_blocks.ascending_page_allocator;
6 
7 import core.memory : pageSize;
8 
9 import std.experimental.allocator.common;
10 
11 // Common implementations for shared and thread local AscendingPageAllocator
12 private mixin template AscendingPageAllocatorImpl(bool isShared)
13 {
14     bool deallocate(void[] buf) nothrow @nogc
15     {
16         size_t goodSize = goodAllocSize(buf.length);
17         version (Posix)
18         {
19             import core.sys.posix.sys.mman : mmap, MAP_FAILED, MAP_PRIVATE,
20                 MAP_ANON, MAP_FIXED, PROT_NONE, munmap;
21 
22             auto ptr = mmap(buf.ptr, goodSize, PROT_NONE, MAP_ANON | MAP_PRIVATE | MAP_FIXED, -1, 0);
23             if (ptr == MAP_FAILED)
24                  return false;
25         }
26         else version (Windows)
27         {
28             import core.sys.windows.winbase : VirtualFree;
29             import core.sys.windows.winnt : MEM_DECOMMIT;
30 
31             auto ret = VirtualFree(buf.ptr, goodSize, MEM_DECOMMIT);
32             if (ret == 0)
33                  return false;
34         }
35         else
36         {
37             static assert(0, "Unsupported OS");
38         }
39 
40         static if (!isShared)
41         {
42             pagesUsed -= goodSize / pageSize;
43         }
44 
45         return true;
46     }
47 
48     Ternary owns(void[] buf) nothrow @nogc @system
49     {
50         if (!data)
51             return Ternary.no;
52         return Ternary(buf.ptr >= data && buf.ptr < buf.ptr + numPages * pageSize);
53     }
54 
55     bool deallocateAll() nothrow @nogc
56     {
57         version (Posix)
58         {
59             import core.sys.posix.sys.mman : munmap;
60             auto ret = munmap(cast(void*) data, numPages * pageSize);
61             if (ret != 0)
62                 assert(0, "Failed to unmap memory, munmap failure");
63         }
64         else version (Windows)
65         {
66             import core.sys.windows.winbase : VirtualFree;
67             import core.sys.windows.winnt : MEM_RELEASE;
68             auto ret = VirtualFree(cast(void*) data, 0, MEM_RELEASE);
69             if (ret == 0)
70                 assert(0, "Failed to unmap memory, VirtualFree failure");
71         }
72         else
73         {
74             static assert(0, "Unsupported OS version");
75         }
76         data = null;
77         offset = null;
78         return true;
79     }
80 
81     size_t goodAllocSize(size_t n) nothrow @nogc
82     {
83         return n.roundUpToMultipleOf(cast(uint) pageSize);
84     }
85 
86     this(size_t n) nothrow @nogc
87     {
88         static if (isShared)
89         {
90             lock = SpinLock(SpinLock.Contention.brief);
91         }
92 
93         pageSize = .pageSize;
94         numPages = n.roundUpToMultipleOf(cast(uint) pageSize) / pageSize;
95 
96         version (Posix)
97         {
98             import core.sys.posix.sys.mman : mmap, MAP_ANON, PROT_NONE,
99                 MAP_PRIVATE, MAP_FAILED;
100 
101             data = cast(typeof(data)) mmap(null, pageSize * numPages,
102                 PROT_NONE, MAP_ANON | MAP_PRIVATE, -1, 0);
103             if (data == MAP_FAILED)
104                 assert(0, "Failed to mmap memory");
105         }
106         else version (Windows)
107         {
108             import core.sys.windows.winbase : VirtualAlloc;
109             import core.sys.windows.winnt : MEM_RESERVE, PAGE_NOACCESS;
110 
111             data = cast(typeof(data)) VirtualAlloc(null, pageSize * numPages,
112                 MEM_RESERVE, PAGE_NOACCESS);
113             if (!data)
114                 assert(0, "Failed to VirtualAlloc memory");
115         }
116         else
117         {
118             static assert(0, "Unsupported OS version");
119         }
120 
121         offset = data;
122         readWriteLimit = data;
123     }
124 
125     size_t getAvailableSize() nothrow @nogc @system
126     {
127         static if (isShared)
128         {
129             lock.lock();
130         }
131 
132         auto size = numPages * pageSize + data - offset;
133         static if (isShared)
134         {
135             lock.unlock();
136         }
137         return size;
138     }
139 
140     // Sets the protection of a memory range to read/write
141     private bool extendMemoryProtection(void* start, size_t size) nothrow @nogc
142     {
143         version (Posix)
144         {
145             import core.sys.posix.sys.mman : mprotect, PROT_WRITE, PROT_READ;
146 
147             auto ret = mprotect(start, size, PROT_WRITE | PROT_READ);
148             return ret == 0;
149         }
150         else version (Windows)
151         {
152             import core.sys.windows.winbase : VirtualAlloc;
153             import core.sys.windows.winnt : MEM_COMMIT, PAGE_READWRITE;
154 
155             auto ret = VirtualAlloc(start, size, MEM_COMMIT, PAGE_READWRITE);
156             return ret != null;
157         }
158         else
159         {
160             static assert(0, "Unsupported OS");
161         }
162     }
163 }
164 
165 /**
166 `AscendingPageAllocator` is a fast and safe allocator that rounds all allocations
167 to multiples of the system's page size. It reserves a range of virtual addresses
168 (using `mmap` on Posix and `VirtualAlloc` on Windows) and allocates memory at consecutive virtual
169 addresses.
170 
171 When a chunk of memory is requested, the allocator finds a range of
172 virtual pages that satisfy the requested size, changing their protection to
173 read/write using OS primitives (`mprotect` and `VirtualProtect`, respectively).
174 The physical memory is allocated on demand, when the pages are accessed.
175 
176 Deallocation removes any read/write permissions from the target pages
177 and notifies the OS to reclaim the physical memory, while keeping the virtual
178 memory.
179 
180 Because the allocator does not reuse memory, any dangling references to
181 deallocated memory will always result in deterministically crashing the process.
182 
183 See_Also:
184 $(HTTPS microsoft.com/en-us/research/wp-content/uploads/2017/03/kedia2017mem.pdf, Simple Fast and Safe Manual Memory Management) for the general approach.
185 */
186 struct AscendingPageAllocator
187 {
188     import std.typecons : Ternary;
189 
190     // Docs for mixin functions
191     version (StdDdoc)
192     {
193         /**
194         Rounds the mapping size to the next multiple of the page size and calls
195         the OS primitive responsible for creating memory mappings: `mmap` on POSIX and
196         `VirtualAlloc` on Windows.
197 
198         Params:
199         n = mapping size in bytes
200         */
201         this(size_t n) nothrow @nogc;
202 
203         /**
204         Rounds the requested size to the next multiple of the page size.
205         */
206         size_t goodAllocSize(size_t n) nothrow @nogc;
207 
208         /**
209         Decommit all physical memory associated with the buffer given as parameter,
210         but keep the range of virtual addresses.
211 
212         On POSIX systems `deallocate` calls `mmap` with `MAP_FIXED' a second time to decommit the memory.
213         On Windows, it uses `VirtualFree` with `MEM_DECOMMIT`.
214         */
215         void deallocate(void[] b) nothrow @nogc;
216 
217         /**
218         Returns `Ternary.yes` if the passed buffer is inside the range of virtual adresses.
219         Does not guarantee that the passed buffer is still valid.
220         */
221         Ternary owns(void[] buf) nothrow @nogc;
222 
223         /**
224         Removes the memory mapping causing all physical memory to be decommited and
225         the virtual address space to be reclaimed.
226         */
227         bool deallocateAll() nothrow @nogc;
228 
229         /**
230         Returns the available size for further allocations in bytes.
231         */
232         size_t getAvailableSize() nothrow @nogc;
233     }
234 
235 private:
236     size_t pageSize;
237     size_t numPages;
238 
239     // The start of the virtual address range
240     void* data;
241 
242     // Keeps track of there the next allocation should start
243     void* offset;
244 
245     // Number of pages which contain alive objects
246     size_t pagesUsed;
247 
248     // On allocation requests, we allocate an extra 'extraAllocPages' pages
249     // The address up to which we have permissions is stored in 'readWriteLimit'
250     void* readWriteLimit;
251     enum extraAllocPages = 1000;
252 
253 public:
254     enum uint alignment = 4096;
255 
256     // Inject common function implementations
257     mixin AscendingPageAllocatorImpl!false;
258 
259     /**
260     Rounds the allocation size to the next multiple of the page size.
261     The allocation only reserves a range of virtual pages but the actual
262     physical memory is allocated on demand, when accessing the memory.
263 
264     Params:
265     n = Bytes to allocate
266 
267     Returns:
268     `null` on failure or if the requested size exceeds the remaining capacity.
269     */
270     void[] allocate(size_t n) nothrow @nogc @system
271     {
272         import std.algorithm.comparison : min;
273 
274         immutable pagedBytes = numPages * pageSize;
275         size_t goodSize = goodAllocSize(n);
276 
277         // Requested exceeds the virtual memory range
278         if (goodSize > pagedBytes || offset - data > pagedBytes - goodSize)
279             return null;
280 
281         // Current allocation exceeds readable/writable memory area
282         if (offset + goodSize > readWriteLimit)
283         {
284             // Extend r/w memory range to new limit
285             void* newReadWriteLimit = min(data + pagedBytes,
286                 offset + goodSize + extraAllocPages * pageSize);
287             if (newReadWriteLimit != readWriteLimit)
288             {
289                 assert(newReadWriteLimit > readWriteLimit);
290                 if (!extendMemoryProtection(readWriteLimit, newReadWriteLimit - readWriteLimit))
291                     return null;
292 
293                 readWriteLimit = newReadWriteLimit;
294             }
295         }
296 
297         void* result = offset;
298         offset += goodSize;
299         pagesUsed += goodSize / pageSize;
300 
301         return cast(void[]) result[0 .. n];
302     }
303 
304     /**
305     Rounds the allocation size to the next multiple of the page size.
306     The allocation only reserves a range of virtual pages but the actual
307     physical memory is allocated on demand, when accessing the memory.
308 
309     The allocated memory is aligned to the specified alignment `a`.
310 
311     Params:
312     n = Bytes to allocate
313     a = Alignment
314 
315     Returns:
316     `null` on failure or if the requested size exceeds the remaining capacity.
317     */
318     void[] alignedAllocate(size_t n, uint a) nothrow @nogc
319     {
320         void* alignedStart = cast(void*) roundUpToMultipleOf(cast(size_t) offset, a);
321         assert(alignedStart.alignedAt(a));
322         immutable pagedBytes = numPages * pageSize;
323         size_t goodSize = goodAllocSize(n);
324         if (goodSize > pagedBytes ||
325             alignedStart - data > pagedBytes - goodSize)
326             return null;
327 
328         // Same logic as allocate, only that the buffer must be properly aligned
329         auto oldOffset = offset;
330         offset = alignedStart;
331         auto result = allocate(n);
332         if (!result)
333             offset = oldOffset;
334         return result;
335     }
336 
337     /**
338     If the passed buffer is not the last allocation, then `delta` can be
339     at most the number of bytes left on the last page.
340     Otherwise, we can expand the last allocation until the end of the virtual
341     address range.
342     */
343     bool expand(ref void[] b, size_t delta) nothrow @nogc @system
344     {
345         import std.algorithm.comparison : min;
346 
347         if (!delta) return true;
348         if (b is null) return false;
349 
350         size_t goodSize = goodAllocSize(b.length);
351         size_t bytesLeftOnPage = goodSize - b.length;
352 
353         // If this is not the last allocation, we can only expand until
354         // completely filling the last page covered by this buffer
355         if (b.ptr + goodSize != offset && delta > bytesLeftOnPage)
356             return false;
357 
358         size_t extraPages = 0;
359 
360         // If the extra `delta` bytes requested do not fit the last page
361         // compute how many extra pages are neeeded
362         if (delta > bytesLeftOnPage)
363         {
364             extraPages = goodAllocSize(delta - bytesLeftOnPage) / pageSize;
365         }
366         else
367         {
368             b = cast(void[]) b.ptr[0 .. b.length + delta];
369             return true;
370         }
371 
372         if (extraPages > numPages || offset - data > pageSize * (numPages - extraPages))
373             return false;
374 
375         void* newPtrEnd = b.ptr + goodSize + extraPages * pageSize;
376         if (newPtrEnd > readWriteLimit)
377         {
378             void* newReadWriteLimit = min(data + numPages * pageSize,
379                 newPtrEnd + extraAllocPages * pageSize);
380             if (newReadWriteLimit > readWriteLimit)
381             {
382                 if (!extendMemoryProtection(readWriteLimit, newReadWriteLimit - readWriteLimit))
383                     return false;
384 
385                 readWriteLimit = newReadWriteLimit;
386             }
387         }
388 
389         pagesUsed += extraPages;
390         offset += extraPages * pageSize;
391         b = cast(void[]) b.ptr[0 .. b.length + delta];
392         return true;
393     }
394 
395     /**
396     Returns `Ternary.yes` if the allocator does not contain any alive objects
397     and `Ternary.no` otherwise.
398     */
399     Ternary empty() nothrow @nogc
400     {
401         return Ternary(pagesUsed == 0);
402     }
403 
404     /**
405     Unmaps the whole virtual address range on destruction.
406     */
407     ~this() nothrow @nogc
408     {
409         if (data)
410             deallocateAll();
411     }
412 }
413 
414 ///
415 @system @nogc nothrow unittest
416 {
417     import core.memory : pageSize;
418 
419     size_t numPages = 100;
420     void[] buf;
421     void[] prevBuf = null;
422     AscendingPageAllocator a = AscendingPageAllocator(numPages * pageSize);
423 
424     foreach (i; 0 .. numPages)
425     {
426         // Allocation is rounded up to page size
427         buf = a.allocate(pageSize - 100);
428         assert(buf.length == pageSize - 100);
429 
430         // Allocations are served at increasing addresses
431         if (prevBuf)
432             assert(prevBuf.ptr + pageSize == buf.ptr);
433 
434         assert(a.deallocate(buf));
435         prevBuf = buf;
436     }
437 }
438 
439 /**
440 `SharedAscendingPageAllocator` is the threadsafe version of `AscendingPageAllocator`.
441 */
442 shared struct SharedAscendingPageAllocator
443 {
444     import std.typecons : Ternary;
445     import core.internal.spinlock : SpinLock;
446 
447     // Docs for mixin functions
448     version (StdDdoc)
449     {
450         /**
451         Rounds the mapping size to the next multiple of the page size and calls
452         the OS primitive responsible for creating memory mappings: `mmap` on POSIX and
453         `VirtualAlloc` on Windows.
454 
455         Params:
456         n = mapping size in bytes
457         */
458         this(size_t n) nothrow @nogc;
459 
460         /**
461         Rounds the requested size to the next multiple of the page size.
462         */
463         size_t goodAllocSize(size_t n) nothrow @nogc;
464 
465         /**
466         Decommit all physical memory associated with the buffer given as parameter,
467         but keep the range of virtual addresses.
468 
469         On POSIX systems `deallocate` calls `mmap` with `MAP_FIXED' a second time to decommit the memory.
470         On Windows, it uses `VirtualFree` with `MEM_DECOMMIT`.
471         */
472         void deallocate(void[] b) nothrow @nogc;
473 
474         /**
475         Returns `Ternary.yes` if the passed buffer is inside the range of virtual adresses.
476         Does not guarantee that the passed buffer is still valid.
477         */
478         Ternary owns(void[] buf) nothrow @nogc;
479 
480         /**
481         Removes the memory mapping causing all physical memory to be decommited and
482         the virtual address space to be reclaimed.
483         */
484         bool deallocateAll() nothrow @nogc;
485 
486         /**
487         Returns the available size for further allocations in bytes.
488         */
489         size_t getAvailableSize() nothrow @nogc;
490     }
491 
492 private:
493     size_t pageSize;
494     size_t numPages;
495 
496     // The start of the virtual address range
497     shared void* data;
498 
499     // Keeps track of there the next allocation should start
500     shared void* offset;
501 
502     // On allocation requests, we allocate an extra 'extraAllocPages' pages
503     // The address up to which we have permissions is stored in 'readWriteLimit'
504     shared void* readWriteLimit;
505     enum extraAllocPages = 1000;
506     SpinLock lock;
507 
508 public:
509     enum uint alignment = 4096;
510 
511     // Inject common function implementations
512     mixin AscendingPageAllocatorImpl!true;
513 
514     /**
515     Rounds the allocation size to the next multiple of the page size.
516     The allocation only reserves a range of virtual pages but the actual
517     physical memory is allocated on demand, when accessing the memory.
518 
519     Params:
520     n = Bytes to allocate
521 
522     Returns:
523     `null` on failure or if the requested size exceeds the remaining capacity.
524     */
525     void[] allocate(size_t n) nothrow @nogc
526     {
527         return allocateImpl(n, 1);
528     }
529 
530     /**
531     Rounds the allocation size to the next multiple of the page size.
532     The allocation only reserves a range of virtual pages but the actual
533     physical memory is allocated on demand, when accessing the memory.
534 
535     The allocated memory is aligned to the specified alignment `a`.
536 
537     Params:
538     n = Bytes to allocate
539     a = Alignment
540 
541     Returns:
542     `null` on failure or if the requested size exceeds the remaining capacity.
543     */
544     void[] alignedAllocate(size_t n, uint a) nothrow @nogc
545     {
546         // For regular `allocate` calls, `a` will be set to 1
547         return allocateImpl(n, a);
548     }
549 
550     private void[] allocateImpl(size_t n, uint a) nothrow @nogc @system
551     {
552         import std.algorithm.comparison : min;
553 
554         size_t localExtraAlloc;
555         void* localOffset;
556         immutable pagedBytes = numPages * pageSize;
557         size_t goodSize = goodAllocSize(n);
558 
559         if (goodSize > pagedBytes)
560             return null;
561 
562         lock.lock();
563         scope(exit) lock.unlock();
564 
565         localOffset = cast(void*) offset;
566         void* alignedStart = cast(void*) roundUpToMultipleOf(cast(size_t) localOffset, a);
567         assert(alignedStart.alignedAt(a));
568         if (alignedStart - data > pagedBytes - goodSize)
569             return null;
570 
571         localOffset = alignedStart + goodSize;
572         if (localOffset > readWriteLimit)
573         {
574             void* newReadWriteLimit = min(cast(void*) data + pagedBytes,
575                 cast(void*) localOffset + extraAllocPages * pageSize);
576             assert(newReadWriteLimit > readWriteLimit);
577             localExtraAlloc = newReadWriteLimit - readWriteLimit;
578             if (!extendMemoryProtection(cast(void*) readWriteLimit, localExtraAlloc))
579                 return null;
580             readWriteLimit = cast(shared(void*)) newReadWriteLimit;
581         }
582 
583         offset = cast(typeof(offset)) localOffset;
584         return cast(void[]) alignedStart[0 .. n];
585     }
586 
587     /**
588     If the passed buffer is not the last allocation, then `delta` can be
589     at most the number of bytes left on the last page.
590     Otherwise, we can expand the last allocation until the end of the virtual
591     address range.
592     */
593     bool expand(ref void[] b, size_t delta) nothrow @nogc @system
594     {
595         import std.algorithm.comparison : min;
596 
597         if (!delta) return true;
598         if (b is null) return false;
599 
600         void* localOffset;
601         size_t localExtraAlloc;
602         size_t goodSize = goodAllocSize(b.length);
603         size_t bytesLeftOnPage = goodSize - b.length;
604 
605         if (bytesLeftOnPage >= delta)
606         {
607             b = cast(void[]) b.ptr[0 .. b.length + delta];
608             return true;
609         }
610 
611         lock.lock();
612         scope(exit) lock.unlock();
613 
614         localOffset = cast(void*) offset;
615         if (b.ptr + goodSize != localOffset)
616             return false;
617 
618         size_t extraPages = goodAllocSize(delta - bytesLeftOnPage) / pageSize;
619         if (extraPages > numPages || localOffset - data > pageSize * (numPages - extraPages))
620             return false;
621 
622 
623         localOffset = b.ptr + goodSize + extraPages * pageSize;
624         if (localOffset > readWriteLimit)
625         {
626             void* newReadWriteLimit = min(cast(void*) data + numPages * pageSize,
627                 localOffset + extraAllocPages * pageSize);
628             assert(newReadWriteLimit > readWriteLimit);
629             localExtraAlloc = newReadWriteLimit - readWriteLimit;
630             if (!extendMemoryProtection(cast(void*) readWriteLimit, localExtraAlloc))
631                 return false;
632             readWriteLimit = cast(shared(void*)) newReadWriteLimit;
633         }
634 
635         offset = cast(typeof(offset)) localOffset;
636         b = cast(void[]) b.ptr[0 .. b.length + delta];
637         return true;
638     }
639 }
640 
641 ///
642 @system unittest
643 {
644     import core.memory : pageSize;
645     import core.thread : ThreadGroup;
646 
647     enum numThreads = 100;
648     shared SharedAscendingPageAllocator a = SharedAscendingPageAllocator(pageSize * numThreads);
649 
650     void fun()
651     {
652         void[] b = a.allocate(pageSize);
653         assert(b.length == pageSize);
654 
655         assert(a.deallocate(b));
656     }
657 
658     auto tg = new ThreadGroup;
659     foreach (i; 0 .. numThreads)
660     {
661         tg.create(&fun);
662     }
663     tg.joinAll();
664 }
665 
666 version (StdUnittest)
667 {
668     private static void testrw(void[] b) @nogc nothrow
669     {
670         ubyte* buf = cast(ubyte*) b.ptr;
671         buf[0] = 100;
672         assert(buf[0] == 100);
673         buf[b.length - 1] = 101;
674         assert(buf[b.length - 1] == 101);
675     }
676 }
677 
678 @system @nogc nothrow unittest
679 {
680     static void testAlloc(Allocator)(ref Allocator a) @nogc nothrow
681     {
682         void[] b1 = a.allocate(1);
683         assert(a.getAvailableSize() == 3 * pageSize);
684         testrw(b1);
685         void[] b2 = a.allocate(2);
686         assert(a.getAvailableSize() == 2 * pageSize);
687         testrw(b2);
688         void[] b3 = a.allocate(pageSize + 1);
689         assert(a.getAvailableSize() == 0);
690 
691         testrw(b3);
692         assert(b1.length == 1);
693         assert(b2.length == 2);
694         assert(b3.length == pageSize + 1);
695 
696         assert(a.offset - a.data == 4 * pageSize);
697         void[] b4 = a.allocate(4);
698         assert(!b4);
699 
700         a.deallocate(b1);
701         assert(a.data);
702         a.deallocate(b2);
703         assert(a.data);
704         a.deallocate(b3);
705     }
706 
707     AscendingPageAllocator a = AscendingPageAllocator(4 * pageSize);
708     shared SharedAscendingPageAllocator aa = SharedAscendingPageAllocator(4 * pageSize);
709 
710     testAlloc(a);
711     testAlloc(aa);
712 }
713 
714 @system @nogc nothrow unittest
715 {
716     size_t numPages = 26214;
717     AscendingPageAllocator a = AscendingPageAllocator(numPages * pageSize);
718     foreach (i; 0 .. numPages)
719     {
720         void[] buf = a.allocate(pageSize);
721         assert(buf.length == pageSize);
722         testrw(buf);
723         a.deallocate(buf);
724     }
725 
726     assert(!a.allocate(1));
727     assert(a.getAvailableSize() == 0);
728 }
729 
730 @system @nogc nothrow unittest
731 {
732     size_t numPages = 26214;
733     uint alignment = cast(uint) pageSize;
734     AscendingPageAllocator a = AscendingPageAllocator(numPages * pageSize);
735 
736     foreach (i; 0 .. numPages)
737     {
738         void[] buf = a.alignedAllocate(pageSize, alignment);
739         assert(buf.length == pageSize);
740         testrw(buf);
741         a.deallocate(buf);
742     }
743 
744     assert(!a.allocate(1));
745     assert(a.getAvailableSize() == 0);
746 }
747 
748 @system @nogc nothrow unittest
749 {
750     static void testAlloc(Allocator)(ref Allocator a) @nogc nothrow
751     {
752         import std.traits : hasMember;
753 
754         size_t numPages = 5;
755         uint alignment = cast(uint) pageSize;
756 
757         void[] b1 = a.allocate(pageSize / 2);
758         assert(b1.length == pageSize / 2);
759 
760         void[] b2 = a.alignedAllocate(pageSize / 2, alignment);
761         assert(a.expand(b1, pageSize / 2));
762         assert(a.expand(b1, 0));
763         assert(!a.expand(b1, 1));
764         testrw(b1);
765 
766         assert(a.expand(b2, pageSize / 2));
767         testrw(b2);
768         assert(b2.length == pageSize);
769 
770         assert(a.getAvailableSize() == pageSize * 3);
771 
772         void[] b3 = a.allocate(pageSize / 2);
773         assert(a.reallocate(b1, b1.length));
774         assert(a.reallocate(b2, b2.length));
775         assert(a.reallocate(b3, b3.length));
776 
777         assert(b3.length == pageSize / 2);
778         testrw(b3);
779         assert(a.expand(b3, pageSize / 4));
780         testrw(b3);
781         assert(a.expand(b3, 0));
782         assert(b3.length == pageSize / 2 + pageSize / 4);
783         assert(a.expand(b3, pageSize / 4 - 1));
784         testrw(b3);
785         assert(a.expand(b3, 0));
786         assert(b3.length == pageSize - 1);
787         assert(a.expand(b3, 2));
788         assert(a.expand(b3, 0));
789         assert(a.getAvailableSize() == pageSize);
790         assert(b3.length == pageSize + 1);
791         testrw(b3);
792 
793         assert(a.reallocate(b1, b1.length));
794         assert(a.reallocate(b2, b2.length));
795         assert(a.reallocate(b3, b3.length));
796 
797         assert(a.reallocate(b3, 2 * pageSize));
798         testrw(b3);
799         assert(a.reallocate(b1, pageSize - 1));
800         testrw(b1);
801         assert(a.expand(b1, 1));
802         testrw(b1);
803         assert(!a.expand(b1, 1));
804 
805         a.deallocate(b1);
806         a.deallocate(b2);
807         a.deallocate(b3);
808     }
809 
810     size_t numPages = 5;
811     uint alignment = cast(uint) pageSize;
812     AscendingPageAllocator a = AscendingPageAllocator(numPages * pageSize);
813     shared SharedAscendingPageAllocator aa = SharedAscendingPageAllocator(numPages * pageSize);
814 
815     testAlloc(a);
816     testAlloc(aa);
817 }
818 
819 @system @nogc nothrow unittest
820 {
821     size_t numPages = 21000;
822     enum testNum = 100;
823     enum allocPages = 10;
824     void[][testNum] buf;
825     AscendingPageAllocator a = AscendingPageAllocator(numPages * pageSize);
826 
827     for (int i = 0; i < numPages; i += testNum * allocPages)
828     {
829         foreach (j; 0 .. testNum)
830         {
831             buf[j] = a.allocate(pageSize * allocPages);
832             testrw(buf[j]);
833         }
834 
835         foreach (j; 0 .. testNum)
836         {
837             a.deallocate(buf[j]);
838         }
839     }
840 }
841 
842 @system @nogc nothrow unittest
843 {
844     size_t numPages = 21000;
845     enum testNum = 100;
846     enum allocPages = 10;
847     void[][testNum] buf;
848     shared SharedAscendingPageAllocator a = SharedAscendingPageAllocator(numPages * pageSize);
849 
850     for (int i = 0; i < numPages; i += testNum * allocPages)
851     {
852         foreach (j; 0 .. testNum)
853         {
854             buf[j] = a.allocate(pageSize * allocPages);
855             testrw(buf[j]);
856         }
857 
858         foreach (j; 0 .. testNum)
859         {
860             a.deallocate(buf[j]);
861         }
862     }
863 }
864 
865 @system @nogc nothrow unittest
866 {
867     enum numPages = 2;
868     AscendingPageAllocator a = AscendingPageAllocator(numPages * pageSize);
869     void[] b = a.allocate((numPages + 1) * pageSize);
870     assert(b is null);
871     b = a.allocate(1);
872     assert(b.length == 1);
873     assert(a.getAvailableSize() == pageSize);
874     a.deallocateAll();
875     assert(!a.data && !a.offset);
876 }
877 
878 @system @nogc nothrow unittest
879 {
880     enum numPages = 26;
881     AscendingPageAllocator a = AscendingPageAllocator(numPages * pageSize);
882     uint alignment = cast(uint) ((numPages / 2) * pageSize);
883     void[] b = a.alignedAllocate(pageSize, alignment);
884     assert(b.length == pageSize);
885     testrw(b);
886     assert(b.ptr.alignedAt(alignment));
887     a.deallocateAll();
888     assert(!a.data && !a.offset);
889 }
890 
891 @system @nogc nothrow unittest
892 {
893     enum numPages = 10;
894     AscendingPageAllocator a = AscendingPageAllocator(numPages * pageSize);
895     uint alignment = cast(uint) (2 * pageSize);
896 
897     void[] b1 = a.alignedAllocate(pageSize, alignment);
898     assert(b1.length == pageSize);
899     testrw(b1);
900     assert(b1.ptr.alignedAt(alignment));
901 
902     void[] b2 = a.alignedAllocate(pageSize, alignment);
903     assert(b2.length == pageSize);
904     testrw(b2);
905     assert(b2.ptr.alignedAt(alignment));
906 
907     void[] b3 = a.alignedAllocate(pageSize, alignment);
908     assert(b3.length == pageSize);
909     testrw(b3);
910     assert(b3.ptr.alignedAt(alignment));
911 
912     void[] b4 = a.allocate(pageSize);
913     assert(b4.length == pageSize);
914     testrw(b4);
915 
916     assert(a.deallocate(b1));
917     assert(a.deallocate(b2));
918     assert(a.deallocate(b3));
919     assert(a.deallocate(b4));
920 
921     a.deallocateAll();
922     assert(!a.data && !a.offset);
923 }
924 
925 @system unittest
926 {
927     import core.thread : ThreadGroup;
928     import std.algorithm.sorting : sort;
929     import core.internal.spinlock : SpinLock;
930 
931     enum numThreads = 100;
932     SpinLock lock = SpinLock(SpinLock.Contention.brief);
933     ulong[numThreads] ptrVals;
934     size_t count = 0;
935     shared SharedAscendingPageAllocator a = SharedAscendingPageAllocator(pageSize * numThreads);
936 
937     void fun()
938     {
939         void[] b = a.allocate(4000);
940         assert(b.length == 4000);
941 
942         assert(a.expand(b, 96));
943         assert(b.length == 4096);
944 
945         lock.lock();
946         ptrVals[count] = cast(ulong) b.ptr;
947         count++;
948         lock.unlock();
949     }
950 
951     auto tg = new ThreadGroup;
952     foreach (i; 0 .. numThreads)
953     {
954         tg.create(&fun);
955     }
956     tg.joinAll();
957 
958     ptrVals[].sort();
959     foreach (i; 0 .. numThreads - 1)
960     {
961         assert(ptrVals[i] + pageSize == ptrVals[i + 1]);
962     }
963 }
964 
965 @system unittest
966 {
967     import core.thread : ThreadGroup;
968     import std.algorithm.sorting : sort;
969     import core.internal.spinlock : SpinLock;
970 
971     SpinLock lock = SpinLock(SpinLock.Contention.brief);
972     enum numThreads = 100;
973     void[][numThreads] buf;
974     size_t count = 0;
975     shared SharedAscendingPageAllocator a = SharedAscendingPageAllocator(2 * pageSize * numThreads);
976 
977     void fun()
978     {
979         enum expand = 96;
980         void[] b = a.allocate(pageSize - expand);
981         assert(b.length == pageSize - expand);
982 
983         assert(a.expand(b, expand));
984         assert(b.length == pageSize);
985 
986         a.expand(b, pageSize);
987         assert(b.length == pageSize || b.length == pageSize * 2);
988 
989         lock.lock();
990         buf[count] = b;
991         count++;
992         lock.unlock();
993     }
994 
995     auto tg = new ThreadGroup;
996     foreach (i; 0 .. numThreads)
997     {
998         tg.create(&fun);
999     }
1000     tg.joinAll();
1001 
1002     sort!((a, b) => a.ptr < b.ptr)(buf[0 .. 100]);
1003     foreach (i; 0 .. numThreads - 1)
1004     {
1005         assert(buf[i].ptr + buf[i].length == buf[i + 1].ptr);
1006     }
1007 }