1 ///
2 module stdx.allocator.building_blocks.allocator_list;
3 
4 import stdx.allocator.building_blocks.null_allocator;
5 import stdx.allocator.common;
6 import stdx.allocator.gc_allocator;
7 version(unittest) import std.stdio;
8 
9 // Turn this on for debugging
10 // debug = allocator_list;
11 
12 /**
13 
14 Given an $(LINK2 https://en.wikipedia.org/wiki/Factory_(object-oriented_programming),
15 object factory) of type `Factory` or a factory function
16 `factoryFunction`, and optionally also `BookkeepingAllocator` as a supplemental
17 allocator for bookkeeping, `AllocatorList` creates an allocator that lazily
18 creates as many allocators are needed for satisfying client allocation requests.
19 
20 An embedded list builds a most-recently-used strategy: the most recent
21 allocators used in calls to either `allocate`, `owns` (successful calls
22 only), or `deallocate` are tried for new allocations in order of their most
23 recent use. Thus, although core operations take in theory $(BIGOH k) time for
24 $(D k) allocators in current use, in many workloads the factor is sublinear.
25 Details of the actual strategy may change in future releases.
26 
27 `AllocatorList` is primarily intended for coarse-grained handling of
28 allocators, i.e. the number of allocators in the list is expected to be
29 relatively small compared to the number of allocations handled by each
30 allocator. However, the per-allocator overhead is small so using
31 `AllocatorList` with a large number of allocators should be satisfactory as long
32 as the most-recently-used strategy is fast enough for the application.
33 
34 `AllocatorList` makes an effort to return allocated memory back when no
35 longer used. It does so by destroying empty allocators. However, in order to
36 avoid thrashing (excessive creation/destruction of allocators under certain use
37 patterns), it keeps unused allocators for a while.
38 
39 Params:
40 factoryFunction = A function or template function (including function literals).
41 New allocators are created by calling `factoryFunction(n)` with strictly
42 positive numbers `n`. Delegates that capture their enviroment are not created
43 amid concerns regarding garbage creation for the environment. When the factory
44 needs state, a `Factory` object should be used.
45 
46 BookkeepingAllocator = Allocator used for storing bookkeeping data. The size of
47 bookkeeping data is proportional to the number of allocators. If $(D
48 BookkeepingAllocator) is $(D NullAllocator), then $(D AllocatorList) is
49 "ouroboros-style", i.e. it keeps the bookkeeping data in memory obtained from
50 the allocators themselves. Note that for ouroboros-style management, the size
51 $(D n) passed to $(D make) will be occasionally different from the size
52 requested by client code.
53 
54 Factory = Type of a factory object that returns new allocators on a need
55 basis. For an object $(D sweatshop) of type $(D Factory), `sweatshop(n)` should
56 return an allocator able to allocate at least `n` bytes (i.e. `Factory` must
57 define `opCall(size_t)` to return an allocator object). Usually the capacity of
58 allocators created should be much larger than $(D n) such that an allocator can
59 be used for many subsequent allocations. $(D n) is passed only to ensure the
60 minimum necessary for the next allocation. The factory object is allowed to hold
61 state, which will be stored inside `AllocatorList` as a direct `public` member
62 called `factory`.
63 
64 */
65 struct AllocatorList(Factory, BookkeepingAllocator = GCAllocator)
66 {
67     import mir.conv : emplace;
68     import stdx.allocator.building_blocks.stats_collector
69         : StatsCollector, Options;
70     import stdx.allocator.internal : Ternary;
71 
72     private enum ouroboros = is(BookkeepingAllocator == NullAllocator);
73 
74     /**
75     Alias for `typeof(Factory()(1))`, i.e. the type of the individual
76     allocators.
77     */
78     alias Allocator = typeof(Factory.init(size_t(1)));
79     // Allocator used internally
80     private alias SAllocator = StatsCollector!(Allocator, Options.bytesUsed);
81 
82     private static struct Node
83     {
84         // Allocator in this node
85         SAllocator a;
86         Node* next;
87 
88         @disable this(this);
89 
90         // Is this node unused?
91         void setUnused() { next = &this; }
92         bool unused() const { return next is &this; }
93 
94         // Just forward everything to the allocator
95         alias a this;
96     }
97 
98     /**
99     If $(D BookkeepingAllocator) is not $(D NullAllocator), $(D bkalloc) is
100     defined and accessible.
101     */
102 
103     // State is stored in an array, but it has a list threaded through it by
104     // means of "nextIdx".
105 
106     // state
107     static if (!ouroboros)
108     {
109         static if (stateSize!BookkeepingAllocator) BookkeepingAllocator bkalloc;
110         else alias bkalloc = BookkeepingAllocator.instance;
111     }
112     static if (stateSize!Factory)
113     {
114         Factory factory;
115     }
116     private Node[] allocators;
117     private Node* root;
118 
119     static if (stateSize!Factory)
120     {
121         private auto make(size_t n) { return factory(n); }
122     }
123     else
124     {
125         private auto make(size_t n) { Factory f; return f(n); }
126     }
127 
128     /**
129     Constructs an `AllocatorList` given a factory object. This constructor is
130     defined only if `Factory` has state.
131     */
132     static if (stateSize!Factory)
133     this(ref Factory plant)
134     {
135         factory = plant;
136     }
137     /// Ditto
138     static if (stateSize!Factory)
139     this(Factory plant)
140     {
141         factory = plant;
142     }
143 
144     static if (__traits(hasMember, Allocator, "deallocateAll")
145         && __traits(hasMember, Allocator, "owns"))
146     ~this()
147     {
148         deallocateAll;
149     }
150 
151     /**
152     The alignment offered.
153     */
154     enum uint alignment = Allocator.alignment;
155 
156     /**
157     Allocate a block of size $(D s). First tries to allocate from the existing
158     list of already-created allocators. If neither can satisfy the request,
159     creates a new allocator by calling $(D make(s)) and delegates the request
160     to it. However, if the allocation fresh off a newly created allocator
161     fails, subsequent calls to $(D allocate) will not cause more calls to $(D
162     make).
163     */
164     void[] allocate(size_t s)
165     {
166         for (auto p = &root, n = *p; n; p = &n.next, n = *p)
167         {
168             auto result = n.allocate(s);
169             if (result.length != s) continue;
170             // Bring to front if not already
171             if (root != n)
172             {
173                 *p = n.next;
174                 n.next = root;
175                 root = n;
176             }
177             return result;
178         }
179         // Can't allocate from the current pool. Check if we just added a new
180         // allocator, in that case it won't do any good to add yet another.
181         if (root && root.empty == Ternary.yes)
182         {
183             // no can do
184             return null;
185         }
186         // Add a new allocator
187         if (auto a = addAllocator(s))
188         {
189             auto result = a.allocate(s);
190             assert(owns(result) == Ternary.yes || !result.ptr);
191             return result;
192         }
193         return null;
194     }
195 
196     private void moveAllocators(void[] newPlace)
197     {
198         assert(newPlace.ptr.alignedAt(Node.alignof));
199         assert(newPlace.length % Node.sizeof == 0);
200         auto newAllocators = cast(Node[]) newPlace;
201         assert(allocators.length <= newAllocators.length);
202 
203         // Move allocators
204         foreach (i, ref e; allocators)
205         {
206             if (e.unused)
207             {
208                 newAllocators[i].setUnused;
209                 continue;
210             }
211             import core.stdc..string : memcpy;
212             memcpy(&newAllocators[i].a, &e.a, e.a.sizeof);
213             if (e.next)
214             {
215                 newAllocators[i].next = newAllocators.ptr
216                     + (e.next - allocators.ptr);
217             }
218             else
219             {
220                 newAllocators[i].next = null;
221             }
222         }
223 
224         // Mark the unused portion as unused
225         foreach (i; allocators.length .. newAllocators.length)
226         {
227             newAllocators[i].setUnused;
228         }
229         auto toFree = allocators;
230 
231         // Change state
232         root = newAllocators.ptr + (root - allocators.ptr);
233         allocators = newAllocators;
234 
235         // Free the olden buffer
236         static if (ouroboros)
237         {
238             static if (__traits(hasMember, Allocator, "deallocate")
239                     && __traits(hasMember, Allocator, "owns"))
240                 deallocate(toFree);
241         }
242         else
243         {
244             bkalloc.deallocate(toFree);
245         }
246     }
247 
248     static if (ouroboros)
249     private Node* addAllocator(size_t atLeastBytes)
250     {
251         void[] t = allocators;
252         static if (__traits(hasMember, Allocator, "expand")
253             && __traits(hasMember, Allocator, "owns"))
254         {
255             immutable bool expanded = t && this.expand(t, Node.sizeof);
256         }
257         else
258         {
259             enum expanded = false;
260         }
261         if (expanded)
262         {
263             import core.stdc..string : memcpy;
264             assert(t.length % Node.sizeof == 0);
265             assert(t.ptr.alignedAt(Node.alignof));
266             allocators = cast(Node[]) t;
267             allocators[$ - 1].setUnused;
268             auto newAlloc = SAllocator(make(atLeastBytes));
269             memcpy(&allocators[$ - 1].a, &newAlloc, newAlloc.sizeof);
270             emplace(&newAlloc);
271         }
272         else
273         {
274             immutable toAlloc = (allocators.length + 1) * Node.sizeof
275                 + atLeastBytes + 128;
276             auto newAlloc = SAllocator(make(toAlloc));
277             auto newPlace = newAlloc.allocate(
278                 (allocators.length + 1) * Node.sizeof);
279             if (!newPlace) return null;
280             moveAllocators(newPlace);
281             import core.stdc..string : memcpy;
282             memcpy(&allocators[$ - 1].a, &newAlloc, newAlloc.sizeof);
283             emplace(&newAlloc);
284             assert(allocators[$ - 1].owns(allocators) == Ternary.yes);
285         }
286         // Insert as new root
287         if (root != &allocators[$ - 1])
288         {
289             allocators[$ - 1].next = root;
290             root = &allocators[$ - 1];
291         }
292         else
293         {
294             // This is the first one
295             root.next = null;
296         }
297         assert(!root.unused);
298         return root;
299     }
300 
301     static if (!ouroboros)
302     private Node* addAllocator(size_t atLeastBytes)
303     {
304         void[] t = allocators;
305         static if (__traits(hasMember, BookkeepingAllocator, "expand"))
306             immutable bool expanded = bkalloc.expand(t, Node.sizeof);
307         else
308             immutable bool expanded = false;
309         if (expanded)
310         {
311             assert(t.length % Node.sizeof == 0);
312             assert(t.ptr.alignedAt(Node.alignof));
313             allocators = cast(Node[]) t;
314             allocators[$ - 1].setUnused;
315         }
316         else
317         {
318             // Could not expand, create a new block
319             t = bkalloc.allocate((allocators.length + 1) * Node.sizeof);
320             assert(t.length % Node.sizeof == 0);
321             if (!t.ptr) return null;
322             moveAllocators(t);
323         }
324         assert(allocators[$ - 1].unused);
325         auto newAlloc = SAllocator(make(atLeastBytes));
326         import core.stdc..string : memcpy;
327         memcpy(&allocators[$ - 1].a, &newAlloc, newAlloc.sizeof);
328         emplace(&newAlloc);
329         // Creation succeeded, insert as root
330         if (allocators.length == 1)
331             allocators[$ - 1].next = null;
332         else
333             allocators[$ - 1].next = root;
334         assert(allocators[$ - 1].a.bytesUsed == 0);
335         root = &allocators[$ - 1];
336         return root;
337     }
338 
339     /**
340     Defined only if `Allocator` defines `owns`. Tries each allocator in
341     turn, in most-recently-used order. If the owner is found, it is moved to
342     the front of the list as a side effect under the assumption it will be used
343     soon.
344 
345     Returns: `Ternary.yes` if one allocator was found to return `Ternary.yes`,
346     `Ternary.no` if all component allocators returned `Ternary.no`, and
347     `Ternary.unknown` if no allocator returned `Ternary.yes` and at least one
348     returned  `Ternary.unknown`.
349     */
350     static if (__traits(hasMember, Allocator, "owns"))
351     Ternary owns(void[] b)
352     {
353         auto result = Ternary.no;
354         for (auto p = &root, n = *p; n; p = &n.next, n = *p)
355         {
356             immutable t = n.owns(b);
357             if (t != Ternary.yes)
358             {
359                 if (t == Ternary.unknown) result = t;
360                 continue;
361             }
362             // Move the owner to front, speculating it'll be used
363             if (n != root)
364             {
365                 *p = n.next;
366                 n.next = root;
367                 root = n;
368             }
369             return Ternary.yes;
370         }
371         return result;
372     }
373 
374     /**
375     Defined only if $(D Allocator.expand) is defined. Finds the owner of $(D b)
376     and calls $(D expand) for it. The owner is not brought to the head of the
377     list.
378     */
379     static if (__traits(hasMember, Allocator, "expand")
380         && __traits(hasMember, Allocator, "owns"))
381     bool expand(ref void[] b, size_t delta)
382     {
383         if (!b.ptr) return delta == 0;
384         for (auto p = &root, n = *p; n; p = &n.next, n = *p)
385         {
386             if (n.owns(b) == Ternary.yes) return n.expand(b, delta);
387         }
388         return false;
389     }
390 
391     /**
392     Defined only if $(D Allocator.reallocate) is defined. Finds the owner of
393     $(D b) and calls $(D reallocate) for it. If that fails, calls the global
394     $(D reallocate), which allocates a new block and moves memory.
395     */
396     static if (__traits(hasMember, Allocator, "reallocate"))
397     bool reallocate(ref void[] b, size_t s)
398     {
399         // First attempt to reallocate within the existing node
400         if (!b.ptr)
401         {
402             b = allocate(s);
403             return b.length == s;
404         }
405         for (auto p = &root, n = *p; n; p = &n.next, n = *p)
406         {
407             if (n.owns(b) == Ternary.yes) return n.reallocate(b, s);
408         }
409         // Failed, but we may find new memory in a new node.
410         return .reallocate(this, b, s);
411     }
412 
413     /**
414      Defined if $(D Allocator.deallocate) and $(D Allocator.owns) are defined.
415     */
416     static if (__traits(hasMember, Allocator, "deallocate")
417         && __traits(hasMember, Allocator, "owns"))
418     bool deallocate(void[] b)
419     {
420         if (!b.ptr) return true;
421         assert(allocators.length);
422         assert(owns(b) == Ternary.yes);
423         bool result;
424         for (auto p = &root, n = *p; ; p = &n.next, n = *p)
425         {
426             assert(n);
427             if (n.owns(b) != Ternary.yes) continue;
428             result = n.deallocate(b);
429             // Bring to front
430             if (n != root)
431             {
432                 *p = n.next;
433                 n.next = root;
434                 root = n;
435             }
436             if (n.empty != Ternary.yes) return result;
437             break;
438         }
439         // Hmmm... should we return this allocator back to the wild? Let's
440         // decide if there are TWO empty allocators we can release ONE. This
441         // is to avoid thrashing.
442         // Note that loop starts from the second element.
443         for (auto p = &root.next, n = *p; n; p = &n.next, n = *p)
444         {
445             if (n.unused || n.empty != Ternary.yes) continue;
446             // Used and empty baby, nuke it!
447             n.a.destroy;
448             *p = n.next;
449             n.setUnused;
450             break;
451         }
452         return result;
453     }
454 
455     /**
456     Defined only if $(D Allocator.owns) and $(D Allocator.deallocateAll) are
457     defined.
458     */
459     static if (ouroboros && __traits(hasMember, Allocator, "deallocateAll")
460         && __traits(hasMember, Allocator, "owns"))
461     bool deallocateAll()
462     {
463         Node* special;
464         foreach (ref n; allocators)
465         {
466             if (n.unused) continue;
467             if (n.owns(allocators) == Ternary.yes)
468             {
469                 special = &n;
470                 continue;
471             }
472             n.a.deallocateAll;
473             n.a.destroy;
474         }
475         assert(special || !allocators.ptr);
476         if (special)
477         {
478             special.deallocate(allocators);
479         }
480         allocators = null;
481         root = null;
482         return true;
483     }
484 
485     static if (!ouroboros && __traits(hasMember, Allocator, "deallocateAll")
486         && __traits(hasMember, Allocator, "owns"))
487     bool deallocateAll()
488     {
489         foreach (ref n; allocators)
490         {
491             if (n.unused) continue;
492             n.a.deallocateAll;
493             n.a.destroy;
494         }
495         bkalloc.deallocate(allocators);
496         allocators = null;
497         root = null;
498         return true;
499     }
500 
501     /**
502      Returns `Ternary.yes` if no allocators are currently active,
503     `Ternary.no` otherwise. This methods never returns `Ternary.unknown`.
504     */
505     Ternary empty() const
506     {
507         return Ternary(!allocators.length);
508     }
509 }
510 
511 /// Ditto
512 template AllocatorList(alias factoryFunction,
513     BookkeepingAllocator = GCAllocator)
514 {
515     alias A = typeof(factoryFunction(size_t(1)));
516     static assert(
517         // is a template function (including literals)
518         is(typeof({A function(size_t) @system x = factoryFunction!size_t;}))
519         ||
520         // or a function (including literals)
521         is(typeof({A function(size_t) @system x = factoryFunction;}))
522         ,
523         "Only function names and function literals that take size_t"
524             ~ " and return an allocator are accepted, not "
525             ~ typeof(factoryFunction).stringof
526     );
527     static struct Factory
528     {
529         A opCall(size_t n) { return factoryFunction(n); }
530     }
531     alias AllocatorList = .AllocatorList!(Factory, BookkeepingAllocator);
532 }
533 
534 ///
535 version(Posix) @system unittest
536 {
537     import mir.utility : max;
538     import stdx.allocator.building_blocks.free_list : ContiguousFreeList;
539     import stdx.allocator.building_blocks.null_allocator : NullAllocator;
540     import stdx.allocator.building_blocks.region : Region;
541     import stdx.allocator.building_blocks.segregator : Segregator;
542     import stdx.allocator.gc_allocator : GCAllocator;
543     import stdx.allocator.mmap_allocator : MmapAllocator;
544 
545     // Ouroboros allocator list based upon 4MB regions, fetched directly from
546     // mmap. All memory is released upon destruction.
547     alias A1 = AllocatorList!((n) => Region!MmapAllocator(max(n, 1024u * 4096u)),
548         NullAllocator);
549 
550     // Allocator list based upon 4MB regions, fetched from the garbage
551     // collector. All memory is released upon destruction.
552     alias A2 = AllocatorList!((n) => Region!GCAllocator(max(n, 1024u * 4096u)));
553 
554     // Ouroboros allocator list based upon 4MB regions, fetched from the garbage
555     // collector. Memory is left to the collector.
556     alias A3 = AllocatorList!(
557         (n) => Region!NullAllocator(new ubyte[max(n, 1024u * 4096u)]),
558         NullAllocator);
559 
560     // Allocator list that creates one freelist for all objects
561     alias A4 =
562         Segregator!(
563             64, AllocatorList!(
564                 (n) => ContiguousFreeList!(NullAllocator, 0, 64)(
565                     cast(ubyte[])(GCAllocator.instance.allocate(4096)))),
566             GCAllocator);
567 
568     A4 a;
569     auto small = a.allocate(64);
570     assert(small);
571     a.deallocate(small);
572     auto b1 = a.allocate(1024 * 8192);
573     assert(b1 !is null); // still works due to overdimensioning
574     b1 = a.allocate(1024 * 10);
575     assert(b1.length == 1024 * 10);
576 }
577 
578 @system unittest
579 {
580     // Create an allocator based upon 4MB regions, fetched from the GC heap.
581     import mir.utility : max;
582     import stdx.allocator.building_blocks.region : Region;
583     AllocatorList!((n) => Region!GCAllocator(new ubyte[max(n, 1024u * 4096u)]),
584         NullAllocator) a;
585     const b1 = a.allocate(1024 * 8192);
586     assert(b1 !is null); // still works due to overdimensioning
587     const b2 = a.allocate(1024 * 10);
588     assert(b2.length == 1024 * 10);
589     a.deallocateAll();
590 }
591 
592 @system unittest
593 {
594     // Create an allocator based upon 4MB regions, fetched from the GC heap.
595     import mir.utility : max;
596     import stdx.allocator.building_blocks.region : Region;
597     AllocatorList!((n) => Region!()(new ubyte[max(n, 1024u * 4096u)])) a;
598     auto b1 = a.allocate(1024 * 8192);
599     assert(b1 !is null); // still works due to overdimensioning
600     b1 = a.allocate(1024 * 10);
601     assert(b1.length == 1024 * 10);
602     a.deallocateAll();
603 }
604 
605 @system unittest
606 {
607     import mir.utility : max;
608     import stdx.allocator.building_blocks.region : Region;
609     import stdx.allocator.internal : Ternary;
610     AllocatorList!((n) => Region!()(new ubyte[max(n, 1024u * 4096u)])) a;
611     auto b1 = a.allocate(1024 * 8192);
612     assert(b1 !is null);
613     b1 = a.allocate(1024 * 10);
614     assert(b1.length == 1024 * 10);
615     a.allocate(1024 * 4095);
616     a.deallocateAll();
617     assert(a.empty == Ternary.yes);
618 }
619 
620 @system unittest
621 {
622     import stdx.allocator.building_blocks.region : Region;
623     enum bs = GCAllocator.alignment;
624     AllocatorList!((n) => Region!GCAllocator(256 * bs)) a;
625     auto b1 = a.allocate(192 * bs);
626     assert(b1.length == 192 * bs);
627     assert(a.allocators.length == 1);
628     auto b2 = a.allocate(64 * bs);
629     assert(b2.length == 64 * bs);
630     assert(a.allocators.length == 1);
631     auto b3 = a.allocate(192 * bs);
632     assert(b3.length == 192 * bs);
633     assert(a.allocators.length == 2);
634     a.deallocate(b1);
635     b1 = a.allocate(64 * bs);
636     assert(b1.length == 64 * bs);
637     assert(a.allocators.length == 2);
638     a.deallocateAll();
639 }