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