module std.experimental.allocator.free_list; import std.experimental.allocator.common; import std.typecons : Flag, Yes, No; /** $(WEB en.wikipedia.org/wiki/Free_list, Free list allocator), stackable on top of another allocator. Allocation requests between $(D min) and $(D max) bytes are rounded up to $(D max) and served from a singly-linked list of buffers deallocated in the past. All other allocations are directed to $(D ParentAllocator). Due to the simplicity of free list management, allocations from the free list are fast. One instantiation is of particular interest: $(D FreeList!(0, unbounded)) puts every deallocation in the freelist, and subsequently serves any allocation from the freelist (if not empty). There is no checking of size matching, which would be incorrect for a freestanding allocator but is both correct and fast when an owning allocator on top of the free list allocator (such as $(D Segregator)) is already in charge of handling size checking. The following methods are defined if $(D ParentAllocator) defines them, and forward to it: $(D expand), $(D owns), $(D reallocate). */ struct FreeList(ParentAllocator, size_t minSize, size_t maxSize = minSize, Flag!"adaptive" adaptive = No.adaptive) { import std.conv : text; import std.exception : enforce; import std.traits : hasMember; static assert(minSize != unbounded, "Use minSize = 0 for no low bound."); static assert(maxSize >= (void*).sizeof, "Maximum size must accommodate a pointer."); private enum hasTolerance = minSize != maxSize || maxSize == chooseAtRuntime; static if (minSize != chooseAtRuntime) { alias min = minSize; } else { size_t _min = chooseAtRuntime; @property size_t min() const { assert(_min != chooseAtRuntime); return _min; } @property void min(size_t x) { enforce(x <= _max); _min = x; } static if (maxSize == chooseAtRuntime) { // Both bounds can be set, provide one function for setting both in // one shot. void setBounds(size_t low, size_t high) { enforce(low <= high && high >= (void*).sizeof); _min = low; _max = high; } } } private bool tooSmall(size_t n) const { static if (minSize == 0) return false; else return n < min; } static if (maxSize != chooseAtRuntime) { alias max = maxSize; } else { size_t _max; @property size_t max() const { return _max; } @property void max(size_t x) { enforce(x >= _min && x >= (void*).sizeof); _max = x; } } private bool tooLarge(size_t n) const { static if (maxSize == unbounded) return false; else return n > max; } private bool inRange(size_t n) const { static if (minSize == maxSize && minSize != chooseAtRuntime) return n == maxSize; else return !tooSmall(n) && !tooLarge(n); } private void[] blockFor(Node* p) { assert(p); return (cast(void*) p)[0 .. max]; } //////////////////////////////////////////////////////////////////////////// // statistics { static if (adaptive == Yes.adaptive) { enum double windowLength = 1000.0; enum double tooFewMisses = 0.01; double probMiss = 1.0; // start with a high miss probability uint accumSamples, accumMisses; void updateStats() { assert(accumSamples >= accumMisses); /* Given that for the past windowLength samples we saw misses with estimated probability probMiss, and assuming the new sample wasMiss or not, what's the new estimated probMiss? */ probMiss = (probMiss * windowLength + accumMisses) / (windowLength + accumSamples); assert(probMiss <= 1.0); accumSamples = 0; accumMisses = 0; // If probability to miss is under x%, yank one off the freelist if (probMiss < tooFewMisses && _root) { auto b = blockFor(_root); _root = _root.next; parent.deallocate(b); } } } // } statistics //////////////////////////////////////////////////////////////////////////// version (StdDdoc) { /** Properties for getting and setting bounds. Setting a bound is only possible if the respective compile-time parameter has been set to $(D chooseAtRuntime). $(D setBounds) is defined only if both $(D minSize) and $(D maxSize) are set to $(D chooseAtRuntime). */ @property size_t min(); /// Ditto @property void min(size_t newMinSize); /// Ditto @property size_t max(); /// Ditto @property void max(size_t newMaxSize); /// Ditto void setBounds(size_t newMin, size_t newMax); /// unittest { FreeList!(Mallocator, chooseAtRuntime, chooseAtRuntime) a; // Set the maxSize first so setting the minSize doesn't throw a.max = 128; a.min = 64; a.setBounds(64, 128); // equivalent assert(a.max == 128); assert(a.min == 64); } } /** The parent allocator. Depending on whether $(D ParentAllocator) holds state or not, this is a member variable or an alias for $(D ParentAllocator.it). */ static if (stateSize!ParentAllocator) ParentAllocator parent; else alias parent = ParentAllocator.it; private struct Node { Node* next; } static assert(ParentAllocator.alignment >= Node.alignof); private Node* _root; private static void incNodes() { } private static void decNodes() { } private enum bool nodesFull = false; /** Alignment is defined as $(D parent.alignment). However, if $(D parent.alignment > maxSize), objects returned from the freelist will have a smaller _alignment, namely $(D maxSize) rounded up to the nearest multiple of 2. This allows $(D FreeList) to minimize internal fragmentation by allocating several small objects within an allocated block. Also, there is no disruption because no object has smaller size than its _alignment. */ enum uint alignment = ParentAllocator.alignment; /** Returns $(D max) for sizes in the interval $(D [min, max]), and $(D parent.goodAllocSize(bytes)) otherwise. */ size_t goodAllocSize(size_t bytes) { static if (maxSize != unbounded) { if (inRange(bytes)) { assert(parent.goodAllocSize(max) == max, text("Wrongly configured freelist: maximum should be ", parent.goodAllocSize(max), " instead of ", max)); return max; } } return parent.goodAllocSize(bytes); } /** Allocates memory either off of the free list or from the parent allocator. */ void[] allocate(size_t bytes) { static if (adaptive == Yes.adaptive) ++accumSamples; assert(bytes < size_t.max / 2); // fast path if (inRange(bytes)) { if (_root) { auto result = (cast(ubyte*) _root)[0 .. bytes]; _root = _root.next; decNodes(); return result; } // slower auto result = allocateFresh(bytes); static if (adaptive == Yes.adaptive) { ++accumMisses; updateStats; } } // slower static if (adaptive == Yes.adaptive) { updateStats; } return parent.allocate(bytes); } private void[] allocateFresh(const size_t bytes) { assert(!_root); assert(max > 0); assert(inRange(bytes)); static if (hasTolerance) { auto toAllocate = max; } else { alias toAllocate = bytes; } assert(toAllocate == max || max == unbounded); auto result = parent.allocate(bytes); static if (hasTolerance) { if (result) result = result.ptr[0 .. bytes]; } return result; } // Forwarding methods mixin(forwardToMember("parent", "expand", "owns", "reallocate")); /** Intercepts deallocations and caches those of the appropriate size in the freelist. For all others, forwards to $(D parent.deallocate) or does nothing if $(D Parent) does not define $(D deallocate). */ void deallocate(void[] block) { if (!nodesFull && inRange(block.length)) { if (min == 0) { // In this case a null pointer might have made it this far. if (block is null) return; } auto t = _root; _root = cast(Node*) block.ptr; _root.next = t; incNodes(); } else { static if (is(typeof(parent.deallocate(block)))) parent.deallocate(block); } } /** Defined only if $(D ParentAllocator) defines $(D deallocateAll). If so, forwards to it and resets the freelist. */ static if (hasMember!(ParentAllocator, "deallocateAll")) void deallocateAll() { parent.deallocateAll(); _root = null; } /** Nonstandard function that minimizes the memory usage of the freelist by freeing each element in turn. Defined only if $(D ParentAllocator) defines $(D deallocate). */ static if (hasMember!(ParentAllocator, "deallocate")) void minimize() { while (_root) { auto nuke = blockFor(_root); _root = _root.next; parent.deallocate(nuke); } } /// GC helper primitives. static if (hasMember!(ParentAllocator, "markAllAsUnused")) { void markAllAsUnused() { // Time to come clean about the stashed data. static if (hasMember!(ParentAllocator, "deallocate")) for (auto n = _root; n; n = n.next) { parent.deallocate(blockFor(n)); } _root = null; } // bool markAsUsed(void[] b) { return parent.markAsUsed(b); } // void doneMarking() { parent.doneMarking(); } } } unittest { import std.experimental.allocator.gc_allocator; FreeList!(GCAllocator, 0, 8) fl; assert(fl._root is null); auto b1 = fl.allocate(7); //assert(fl._root !is null); auto b2 = fl.allocate(8); assert(fl._root is null); fl.deallocate(b1); assert(fl._root !is null); auto b3 = fl.allocate(8); assert(fl._root is null); } /** FreeList shared across threads. Allocation and deallocation are lock-free. The parameters have the same semantics as for $(D FreeList). $(D expand) is defined to forward to $(ParentAllocator.expand) (it must be also $(D shared)). */ struct SharedFreeList(ParentAllocator, size_t minSize, size_t maxSize = minSize) { import std.conv : text; import std.exception : enforce; import std.traits : hasMember; static assert(minSize != unbounded, "Use minSize = 0 for no low bound."); static assert(maxSize >= (void*).sizeof, "Maximum size must accommodate a pointer."); private import core.atomic; static if (minSize != chooseAtRuntime) { alias min = minSize; } else { shared size_t _min = chooseAtRuntime; @property size_t min() const shared { assert(_min != chooseAtRuntime); return _min; } @property void min(size_t x) shared { enforce(x <= max); enforce(cas(&_min, chooseAtRuntime, x), "SharedFreeList.min must be initialized exactly once."); } static if (maxSize == chooseAtRuntime) { // Both bounds can be set, provide one function for setting both in // one shot. void setBounds(size_t low, size_t high) shared { enforce(low <= high && high >= (void*).sizeof); enforce(cas(&_min, chooseAtRuntime, low), "SharedFreeList.min must be initialized exactly once."); enforce(cas(&_max, chooseAtRuntime, high), "SharedFreeList.max must be initialized exactly once."); } } } private bool tooSmall(size_t n) const shared { static if (minSize == 0) return false; else static if (minSize == chooseAtRuntime) return n < _min; else return n < minSize; } static if (maxSize != chooseAtRuntime) { alias max = maxSize; } else { shared size_t _max = chooseAtRuntime; @property size_t max() const shared { return _max; } @property void max(size_t x) shared { enforce(x >= _min && x >= (void*).sizeof); enforce(cas(&_max, chooseAtRuntime, x), "SharedFreeList.max must be initialized exactly once."); } } private bool tooLarge(size_t n) const shared { static if (maxSize == unbounded) return false; else static if (maxSize == chooseAtRuntime) return n > _max; else return n > maxSize; } private bool inRange(size_t n) const shared { static if (minSize == maxSize && minSize != chooseAtRuntime) return n == maxSize; else return !tooSmall(n) && !tooLarge(n); } //static if (maxNodes != unbounded) //{ // private shared size_t nodes; // private void incNodes() shared // { // atomicOp!("+=")(nodes, 1); // } // private void decNodes() shared // { // assert(nodes); // atomicOp!("-=")(nodes, 1); // } // private bool nodesFull() shared // { // return nodes >= maxNodes; // } //} //else //{ private static void incNodes() { } private static void decNodes() { } private enum bool nodesFull = false; //} version (StdDdoc) { /** Properties for getting (and possibly setting) the bounds. Setting bounds is allowed only once , and before any allocation takes place. Otherwise, the primitives have the same semantics as those of $(D FreeList). */ @property size_t min(); /// Ditto @property void min(size_t newMinSize); /// Ditto @property size_t max(); /// Ditto @property void max(size_t newMaxSize); /// Ditto void setBounds(size_t newMin, size_t newMax); /// unittest { FreeList!(Mallocator, chooseAtRuntime, chooseAtRuntime) a; // Set the maxSize first so setting the minSize doesn't throw a.max = 128; a.min = 64; a.setBounds(64, 128); // equivalent assert(a.max == 128); assert(a.min == 64); } } /** The parent allocator. Depending on whether $(D ParentAllocator) holds state or not, this is a member variable or an alias for $(D ParentAllocator.it). */ static if (stateSize!ParentAllocator) shared ParentAllocator parent; else alias parent = ParentAllocator.it; mixin(forwardToMember("parent", "expand")); private struct Node { Node* next; } static assert(ParentAllocator.alignment >= Node.alignof); private Node* _root; /// Standard primitives. enum uint alignment = ParentAllocator.alignment; /// Ditto size_t goodAllocSize(size_t bytes) shared { if (inRange(bytes)) return maxSize == unbounded ? bytes : max; return parent.goodAllocSize(bytes); } /// Ditto bool owns(void[] b) shared const { if (inRange(b.length)) return true; static if (hasMember!(ParentAllocator, "owns")) return parent.owns(b); else return false; } /// Ditto static if (hasMember!(ParentAllocator, "reallocate")) bool reallocate(void[] b, size_t s) { return parent.reallocate(b, s); } /// Ditto void[] allocate(size_t bytes) shared { assert(bytes < size_t.max / 2); if (!inRange(bytes)) return parent.allocate(bytes); if (maxSize != unbounded) bytes = max; if (!_root) return allocateFresh(bytes); // Pop off the freelist shared Node* oldRoot = void, next = void; do { oldRoot = _root; // atomic load next = oldRoot.next; // atomic load } while (!cas(&_root, oldRoot, next)); // great, snatched the root decNodes(); return (cast(ubyte*) oldRoot)[0 .. bytes]; } private void[] allocateFresh(const size_t bytes) shared { assert(bytes == max || max == unbounded); return parent.allocate(bytes); } /// Ditto void deallocate(void[] b) shared { if (!nodesFull && inRange(b.length)) { auto newRoot = cast(shared Node*) b.ptr; shared Node* oldRoot; do { oldRoot = _root; newRoot.next = oldRoot; } while (!cas(&_root, oldRoot, newRoot)); incNodes(); } else { static if (is(typeof(parent.deallocate(block)))) parent.deallocate(block); } } /// Ditto void deallocateAll() shared { static if (hasMember!(ParentAllocator, "deallocateAll")) { parent.deallocateAll(); } else static if (hasMember!(ParentAllocator, "deallocate")) { for (auto n = _root; n; n = n.next) { parent.deallocate((cast(ubyte*)n)[0 .. max]); } } _root = null; } } unittest { import core.thread, std.algorithm, std.concurrency, std.range, std.experimental.allocator.mallocator; static shared SharedFreeList!(Mallocator, 64, 128) a; assert(a.goodAllocSize(1) == platformAlignment); auto b = a.allocate(100); a.deallocate(b); static void fun(Tid tid, int i) { scope(exit) tid.send(true); auto b = cast(ubyte[]) a.allocate(100); b[] = cast(ubyte) i; assert(b.equal(repeat(cast(ubyte) i, b.length))); a.deallocate(b); } Tid[] tids; foreach (i; 0 .. 1000) { tids ~= spawn(&fun, thisTid, i); } foreach (i; 0 .. 1000) { assert(receiveOnly!bool); } } unittest { import std.experimental.allocator.mallocator; shared SharedFreeList!(Mallocator, chooseAtRuntime, chooseAtRuntime) a; auto b = a.allocate(64); }