phobos/std/experimental/allocator/kernighan_ritchie.d
2015-10-02 07:35:08 -04:00

492 lines
15 KiB
D

/**
Contains a number of artifacts related to the
$(WEB stackoverflow.com/questions/13159564/explain-this-implementation-of-malloc-from-the-kr-book
famed allocator) described by Brian Kernighan and Dennis Ritchie in section 8.7
of the book "The C Programming Language" Second Edition, Prentice Hall, 1988.
*/
module std.experimental.allocator.kernighan_ritchie;
//debug = KRBlock;
debug(KRBlock) import std.stdio;
// KRBlock
/**
A $(D KRBlock) manages a single contiguous chunk of memory by embedding a free
blocks list onto it. It is a very simple allocator with low size overhead. Its
disadvantages include proneness to fragmentation and slow allocation and
deallocation times, in the worst case proportional to the number of free nodes.
So $(D KRBlock) should be used for simple allocation needs, or for
coarse-granular allocations in conjunction with specialized allocators for small
objects.
The smallest size that can be allocated is two words (16 bytes on 64-bit
systems). This is because the freelist management needs two words (one for the
length, the other for the next pointer in the singly-linked list).
Similarities with the Kernighan-Ritchie allocator:
$(UL
$(LI Free blocks have variable size and are linked in a singly-linked list.)
$(LI The freelist is maintained in increasing address order.)
$(LI The strategy for finding the next available block is first fit.)
)
Differences from the Kernighan-Ritchie allocator:
$(UL
$(LI Once the chunk is exhausted, the Kernighan-Ritchie allocator allocates
another chunk. The $(D KRBlock) just gets full (returns $(D null) on
new allocation requests). The decision to allocate more blocks is left to a
higher-level entity.)
$(LI The freelist in the Kernighan-Ritchie allocator is circular, with the last
node pointing back to the first; in $(D KRBlock), the last pointer is
$(D null).)
$(LI Allocated blocks do not have a size prefix. This is because in D the size
information is available in client code at deallocation time.)
$(LI The Kernighan-Ritchie allocator performs eager coalescing. $(D KRBlock)
coalesces lazily, i.e. only attempts it for allocation requests larger than
the memory available. This saves work if most allocations are of similar size
becase there's no repeated churn for coalescing followed by splitting. Also,
the coalescing work is proportional with allocation size, which is advantageous
because large allocations are likely to undergo relatively intensive work in
client code.)
)
Initially the freelist has only one element, which covers the entire chunk of
memory. The maximum amount that can be allocated is the full chunk (there is no
size overhead for allocated blocks). As memory gets allocated and deallocated,
the free list will evolve accordingly whilst staying sorted at all times.
*/
struct KRBlock
{
import std.format : format;
private static struct Node
{
import std.typecons;
Node* next;
size_t size;
//this(this) @disable;
void[] payload()
{
return (cast(ubyte*) &this)[0 .. size];
}
Tuple!(void[], Node*) allocateHere(size_t bytes)
{
while (size < bytes)
{
// Try to coalesce
if (!next || !adjacent(next))
{
// no honey
return typeof(return)();
}
size = cast(ubyte*) next + next.size - cast(ubyte*) &this;
next = next.next;
if (size >= bytes) break;
}
assert(size >= bytes);
auto leftover = size - bytes;
if (leftover > Node.sizeof)
{
// There's room for another node
auto newNode = cast(Node*) ((cast(ubyte*) &this) + bytes);
newNode.size = leftover;
newNode.next = next;
next = newNode;
return tuple(payload, newNode);
}
// No slack space, just return next node
return tuple(payload, next);
}
bool adjacent(in Node* right) const
{
assert(&this < right);
return cast(ubyte*) &this + size + Node.sizeof >=
cast(ubyte*) right;
}
}
private void[] payload;
Node root; // size for the root contains total bytes allocated
string toString()
{
string s = "KRBlock@";
s ~= format("%s-%s(0x%s[%s]", &this, &this + 1,
payload.ptr, payload.length);
for (auto j = root.next; j; j = j.next)
{
s ~= format(", free(0x%s[%s])", cast(void*) j, j.size);
}
s ~= ')';
assert(root.next != &root);
return s;
}
private void assertValid(string s)
{
if (!payload.ptr)
{
assert(!root.next, s);
return;
}
if (!root.next)
{
return;
}
assert(root.next >= payload.ptr, s);
assert(root.next < payload.ptr + payload.length, s);
// Check that the list terminates
size_t n;
for (auto i = &root, j = root.next; j; i = j, j = j.next)
{
assert(n++ < payload.length / Node.sizeof, s);
}
}
/**
Create a $(D KRBlock) managing a chunk of memory. Memory must be
larger than two words, word-aligned, and of size multiple of $(D
size_t.alignof).
*/
this(void[] b)
{
assert(b.length > Node.sizeof);
assert(b.length % alignment == 0);
assert(b.length >= 2 * Node.sizeof);
payload = b;
root.next = cast(Node*) b.ptr;
root.size = 0;
// Initialize the free list with all list
with (root.next)
{
next = null;
size = b.length;
}
debug(KRBlock) writefln("KRBlock@%s: init with %s[%s]", &this,
b.ptr, b.length);
}
/** Alignment is the same as for a $(D struct) consisting of two words (a
pointer followed by a $(D size_t).)
*/
enum alignment = Node.alignof;
/// Allocator primitives.
void[] allocate(size_t bytes)
{
auto actualBytes = goodAllocSize(bytes);
// First fit
for (auto i = &root, j = root.next; j; i = j, j = j.next)
{
assert(j != &root);
auto k = j.allocateHere(actualBytes);
if (k[0] is null) continue;
// Allocated, update freelist
i.next = k[1];
debug(KRBlock) writefln("KRBlock@%s: allocate returning %s[%s]",
&this,
k[0].ptr, bytes);
root.size += actualBytes;
return k[0][0 .. bytes];
}
debug(KRBlock) writefln("KRBlock@%s: allocate returning null", &this);
return null;
}
/// Ditto
void deallocate(void[] b)
{
debug(KRBlock) writefln("KRBlock@%s: deallocate(%s[%s])", &this,
b.ptr, b.length);
// Insert back in the freelist, keeping it sorted by address. Do not
// coalesce at this time. Instead, do it lazily during allocation.
if (!b.ptr) return;
assert(owns(b));
assert(b.ptr !is &root, format("This is weird @%s[%s]", b.ptr, b.length));
auto n = cast(Node*) b.ptr;
n.size = goodAllocSize(b.length);
root.size -= n.size;
// Linear search
for (auto i = &root, j = root.next; ; i = j, j = j.next)
{
if (!j)
{
// Insert at end
i.next = n;
n.next = null;
return;
}
if (j < n) continue;
// node is in between i and j
assert(i != n && j != n,
format("Double deallocation of block %s (%s bytes)",
b.ptr, b.length));
n.next = j;
i.next = n;
return;
}
}
/// Ditto
void[] allocateAll()
{
return allocate(payload.length);
}
/// Ditto
void deallocateAll()
{
root.next = cast(Node*) payload.ptr;
root.size = 0;
// Initialize the free list with all list
with (root.next)
{
next = null;
size = payload.length;
}
}
/// Ditto
bool owns(void[] b)
{
return b.ptr >= payload.ptr && b.ptr < payload.ptr + payload.length;
}
/// Ditto
static size_t goodAllocSize(size_t s)
{
import std.experimental.allocator.common : roundUpToMultipleOf;
return s <= Node.sizeof
? Node.sizeof : s.roundUpToMultipleOf(alignment);
}
/// Ditto
bool empty() const { return root.size == 0; }
}
///
unittest
{
import std.experimental.allocator.gc_allocator;
auto alloc = KRBlock(GCAllocator.it.allocate(1024 * 1024));
assert(alloc.empty);
void[][] array;
foreach (i; 1 .. 4)
{
array ~= alloc.allocate(i);
assert(!alloc.empty);
assert(array[$ - 1].length == i);
}
alloc.deallocate(array[1]);
alloc.deallocate(array[0]);
alloc.deallocate(array[2]);
assert(alloc.empty);
assert(alloc.allocateAll().length == 1024 * 1024);
assert(!alloc.empty);
}
unittest
{
import std.experimental.allocator.gc_allocator;
auto alloc = KRBlock(GCAllocator.it.allocate(1024 * 1024));
auto store = alloc.allocate(KRBlock.sizeof);
assert(alloc.root.next !is &alloc.root);
auto p = cast(KRBlock* ) store.ptr;
import std.algorithm : move;
alloc.move(*p);
assert(p.root.next !is &p.root);
//writeln(*p);
void[][] array;
foreach (i; 1 .. 4)
{
array ~= p.allocate(i);
assert(array[$ - 1].length == i);
}
p.deallocate(array[1]);
p.deallocate(array[0]);
p.deallocate(array[2]);
assert(p.allocateAll() is null);
}
unittest
{
import std.experimental.allocator.gc_allocator;
auto alloc = KRBlock(GCAllocator.it.allocate(1024 * 1024));
auto p = alloc.allocateAll();
assert(p.length == 1024 * 1024);
alloc.deallocateAll();
p = alloc.allocateAll();
assert(p.length == 1024 * 1024);
}
unittest
{
import std.experimental.allocator.mallocator;
import std.experimental.allocator.common;
auto m = Mallocator.it.allocate(1024 * 64);
testAllocator!(() => KRBlock(m));
}
// KRAllocator
/**
$(D KRAllocator) implements a full-fledged KR-style allocator based upon
Similarities with the Kernighan-Ritchie allocator:
$(UL
$(LI Free blocks have variable size and are linked in a singly-linked list.)
$(LI The freelist is maintained in increasing address order.)
$(LI The strategy for finding the next available block is first fit.)
)
Differences from the Kernighan-Ritchie allocator:
$(UL
$(LI Once the chunk is exhausted, the Kernighan-Ritchie allocator allocates
another chunk. The $(D KRBlock) just gets full (returns $(D null) on
new allocation requests). The decision to allocate more blocks is left to a
higher-level entity.)
$(LI The freelist in the Kernighan-Ritchie allocator is circular, with the last
node pointing back to the first; in $(D KRBlock), the last pointer is
$(D null).)
$(LI Allocated blocks do not have a size prefix. This is because in D the size
information is available in client code at deallocation time.)
$(LI The Kernighan-Ritchie allocator performs eager coalescing. $(D KRBlock)
coalesces lazily, i.e. only attempts it for allocation requests larger than
the memory available. This saves work if most allocations are of similar size
becase there's no repeated churn for coalescing followed by splitting. Also,
the coalescing work is proportional with allocation size, which is advantageous
because large allocations are likely to undergo relatively intensive work in
client code.)
)
*/
struct KRAllocator(ParentAllocator)
{
import std.experimental.allocator.common : stateSize;
import std.algorithm : isSorted, map;
// state {
static if (stateSize!ParentAllocator) ParentAllocator parent;
else alias parent = ParentAllocator.it;
private KRBlock[] blocks;
// } state
//@disable this(this);
private KRBlock* blockFor(void[] b)
{
import std.range : isInputRange;
static assert(isInputRange!(KRBlock[]));
import std.range : assumeSorted;
auto ub = blocks.map!((ref a) => a.payload.ptr)
.assumeSorted
.upperBound(b.ptr);
if (ub.length == blocks.length) return null;
auto result = &(blocks[$ - ub.length - 1]);
assert(result.payload.ptr <= b.ptr);
return result.payload.ptr + result.payload.length > b.ptr
? result : null;
}
/// Allocator primitives
enum alignment = KRBlock.alignment;
/// ditto
static size_t goodAllocSize(size_t n)
{
return KRBlock.goodAllocSize(n);
}
void[] allocate(size_t n)
{
foreach (ref alloc; blocks)
{
auto result = alloc.allocate(n);
if (result.ptr)
{
return result;
}
}
// Couldn't allocate using the current battery of allocators, get a
// new one
import std.conv : emplace;
import std.algorithm : max, move, swap;
void[] untypedBlocks = blocks;
auto n00b = KRBlock(parent.allocate(
max(1024 * 64, untypedBlocks.length + KRBlock.sizeof + n)));
untypedBlocks =
n00b.allocate(untypedBlocks.length + KRBlock.sizeof);
untypedBlocks[0 .. $ - KRBlock.sizeof] = cast(void[]) blocks[];
deallocate(blocks);
blocks = cast(KRBlock[]) untypedBlocks;
n00b.move(blocks[$ - 1]);
// Bubble the new element into the sorted array
for (auto i = blocks.length - 1; i > 0; --i)
{
if (blocks[i - 1].payload.ptr < blocks[i].payload.ptr)
{
return blocks[i].allocate(n);
}
swap(blocks[i], blocks[i - 1]);
}
assert(blocks.map!((ref a) => a.payload.ptr).isSorted);
return blocks[0].allocate(n);
}
bool owns(void[] b)
{
return blockFor(b) !is null;
}
void deallocate(void[] b)
{
if (!b.ptr) return;
if (auto block = blockFor(b))
{
assert(block.owns(b));
return block.deallocate(b);
}
assert(false, "KRAllocator.deallocate: invalid argument.");
}
void deallocateAll()
{
blocks = null;
}
}
///
unittest
{
import std.experimental.allocator.gc_allocator, std.algorithm, std.array,
std.stdio;
KRAllocator!GCAllocator alloc;
void[][] array;
foreach (i; 1 .. 4)
{
array ~= alloc.allocate(i);
assert(array[$ - 1].ptr);
assert(array.length == 1 || array[$ - 2].ptr != array[$ - 1].ptr);
assert(array[$ - 1].length == i);
assert(alloc.owns(array.back));
assert(alloc.blockFor(array.front) !is null);
assert(alloc.blockFor(array.back) !is null);
}
alloc.deallocate(array[1]);
alloc.deallocate(array[0]);
alloc.deallocate(array[2]);
import std.experimental.allocator.common;
testAllocator!(() => KRAllocator!GCAllocator());
}