/** Utility functions for memory management Note that this module currently is a big sand box for testing allocation related stuff. Nothing here, including the interfaces, is final but rather a lot of experimentation. Copyright: © 2012-2013 RejectedSoftware e.K. License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Sönke Ludwig */ module vibe.internal.memory; import vibe.internal.traits : synchronizedIsNothrow; import core.exception : OutOfMemoryError; import core.stdc.stdlib; import core.memory; import std.conv; import std.exception : enforceEx; import std.traits; import std.algorithm; Allocator defaultAllocator() nothrow { version(VibeManualMemoryManagement){ return manualAllocator(); } else { static __gshared Allocator alloc; if (!alloc) { alloc = new GCAllocator; //alloc = new AutoFreeListAllocator(alloc); //alloc = new DebugAllocator(alloc); alloc = new LockAllocator(alloc); } return alloc; } } Allocator manualAllocator() nothrow { static __gshared Allocator alloc; if( !alloc ){ alloc = new MallocAllocator; alloc = new AutoFreeListAllocator(alloc); //alloc = new DebugAllocator(alloc); alloc = new LockAllocator(alloc); } return alloc; } Allocator threadLocalAllocator() nothrow { static Allocator alloc; if (!alloc) { version(VibeManualMemoryManagement) alloc = new MallocAllocator; else alloc = new GCAllocator; alloc = new AutoFreeListAllocator(alloc); // alloc = new DebugAllocator(alloc); } return alloc; } Allocator threadLocalManualAllocator() nothrow { static Allocator alloc; if (!alloc) { alloc = new MallocAllocator; alloc = new AutoFreeListAllocator(alloc); // alloc = new DebugAllocator(alloc); } return alloc; } auto allocObject(T, bool MANAGED = true, ARGS...)(Allocator allocator, ARGS args) { auto mem = allocator.alloc(AllocSize!T); static if( MANAGED ){ static if( hasIndirections!T ) GC.addRange(mem.ptr, mem.length); return internalEmplace!T(mem, args); } else static if( is(T == class) ) return cast(T)mem.ptr; else return cast(T*)mem.ptr; } T[] allocArray(T, bool MANAGED = true)(Allocator allocator, size_t n) { auto mem = allocator.alloc(T.sizeof * n); auto ret = cast(T[])mem; static if( MANAGED ){ static if( hasIndirections!T ) GC.addRange(mem.ptr, mem.length); // TODO: use memset for class, pointers and scalars foreach (ref el; ret) { internalEmplace!T(cast(void[])((&el)[0 .. 1])); } } return ret; } void freeArray(T, bool MANAGED = true)(Allocator allocator, ref T[] array, bool call_destructors = true) { static if (MANAGED) { static if (hasIndirections!T) GC.removeRange(array.ptr); static if (hasElaborateDestructor!T) if (call_destructors) foreach_reverse (ref el; array) destroy(el); } allocator.free(cast(void[])array); array = null; } interface Allocator { nothrow: enum size_t alignment = 0x10; enum size_t alignmentMask = alignment-1; void[] alloc(size_t sz) out { assert((cast(size_t)__result.ptr & alignmentMask) == 0, "alloc() returned misaligned data."); } void[] realloc(void[] mem, size_t new_sz) in { assert(mem.ptr !is null, "realloc() called with null array."); assert((cast(size_t)mem.ptr & alignmentMask) == 0, "misaligned pointer passed to realloc()."); } out { assert((cast(size_t)__result.ptr & alignmentMask) == 0, "realloc() returned misaligned data."); } void free(void[] mem) in { assert(mem.ptr !is null, "free() called with null array."); assert((cast(size_t)mem.ptr & alignmentMask) == 0, "misaligned pointer passed to free()."); } } /** Simple proxy allocator protecting its base allocator with a mutex. */ class LockAllocator : Allocator { private { Allocator m_base; } this(Allocator base) nothrow { m_base = base; } void[] alloc(size_t sz) { static if (!synchronizedIsNothrow) scope (failure) assert(0, "Internal error: function should be nothrow"); synchronized (this) return m_base.alloc(sz); } void[] realloc(void[] mem, size_t new_sz) in { assert(mem.ptr !is null, "realloc() called with null array."); assert((cast(size_t)mem.ptr & alignmentMask) == 0, "misaligned pointer passed to realloc()."); } body { static if (!synchronizedIsNothrow) scope (failure) assert(0, "Internal error: function should be nothrow"); synchronized(this) return m_base.realloc(mem, new_sz); } void free(void[] mem) in { assert(mem.ptr !is null, "free() called with null array."); assert((cast(size_t)mem.ptr & alignmentMask) == 0, "misaligned pointer passed to free()."); } body { static if (!synchronizedIsNothrow) scope (failure) assert(0, "Internal error: function should be nothrow"); synchronized(this) m_base.free(mem); } } final class DebugAllocator : Allocator { import vibe.internal.hashmap : HashMap; private { Allocator m_baseAlloc; HashMap!(void*, size_t) m_blocks; size_t m_bytes; size_t m_maxBytes; } this(Allocator base_allocator) nothrow { m_baseAlloc = base_allocator; m_blocks = HashMap!(void*, size_t)(manualAllocator()); } @property size_t allocatedBlockCount() const { return m_blocks.length; } @property size_t bytesAllocated() const { return m_bytes; } @property size_t maxBytesAllocated() const { return m_maxBytes; } void[] alloc(size_t sz) { auto ret = m_baseAlloc.alloc(sz); assert(ret.length == sz, "base.alloc() returned block with wrong size."); assert(m_blocks.getNothrow(ret.ptr, size_t.max) == size_t.max, "base.alloc() returned block that is already allocated."); m_blocks[ret.ptr] = sz; m_bytes += sz; if( m_bytes > m_maxBytes ){ m_maxBytes = m_bytes; logDebug_("New allocation maximum: %d (%d blocks)", m_maxBytes, m_blocks.length); } return ret; } void[] realloc(void[] mem, size_t new_size) { auto sz = m_blocks.getNothrow(mem.ptr, size_t.max); assert(sz != size_t.max, "realloc() called with non-allocated pointer."); assert(sz == mem.length, "realloc() called with block of wrong size."); auto ret = m_baseAlloc.realloc(mem, new_size); assert(ret.length == new_size, "base.realloc() returned block with wrong size."); assert(ret.ptr is mem.ptr || m_blocks.getNothrow(ret.ptr, size_t.max) == size_t.max, "base.realloc() returned block that is already allocated."); m_bytes -= sz; m_blocks.remove(mem.ptr); m_blocks[ret.ptr] = new_size; m_bytes += new_size; return ret; } void free(void[] mem) { auto sz = m_blocks.getNothrow(mem.ptr, size_t.max); assert(sz != size_t.max, "free() called with non-allocated object."); assert(sz == mem.length, "free() called with block of wrong size."); m_baseAlloc.free(mem); m_bytes -= sz; m_blocks.remove(mem.ptr); } } final class MallocAllocator : Allocator { void[] alloc(size_t sz) { static err = new immutable OutOfMemoryError; auto ptr = .malloc(sz + Allocator.alignment); if (ptr is null) throw err; return adjustPointerAlignment(ptr)[0 .. sz]; } void[] realloc(void[] mem, size_t new_size) { size_t csz = min(mem.length, new_size); auto p = extractUnalignedPointer(mem.ptr); size_t oldmisalign = mem.ptr - p; auto pn = cast(ubyte*).realloc(p, new_size+Allocator.alignment); if (p == pn) return pn[oldmisalign .. new_size+oldmisalign]; auto pna = cast(ubyte*)adjustPointerAlignment(pn); auto newmisalign = pna - pn; // account for changed alignment after realloc (move memory back to aligned position) if (oldmisalign != newmisalign) { if (newmisalign > oldmisalign) { foreach_reverse (i; 0 .. csz) pn[i + newmisalign] = pn[i + oldmisalign]; } else { foreach (i; 0 .. csz) pn[i + newmisalign] = pn[i + oldmisalign]; } } return pna[0 .. new_size]; } void free(void[] mem) { .free(extractUnalignedPointer(mem.ptr)); } } final class GCAllocator : Allocator { void[] alloc(size_t sz) { auto mem = GC.malloc(sz+Allocator.alignment); auto alignedmem = adjustPointerAlignment(mem); assert(alignedmem - mem <= Allocator.alignment); auto ret = alignedmem[0 .. sz]; ensureValidMemory(ret); return ret; } void[] realloc(void[] mem, size_t new_size) { size_t csz = min(mem.length, new_size); auto p = extractUnalignedPointer(mem.ptr); size_t misalign = mem.ptr - p; assert(misalign <= Allocator.alignment); void[] ret; auto extended = GC.extend(p, new_size - mem.length, new_size - mem.length); if (extended) { assert(extended >= new_size+Allocator.alignment); ret = p[misalign .. new_size+misalign]; } else { ret = alloc(new_size); ret[0 .. csz] = mem[0 .. csz]; } ensureValidMemory(ret); return ret; } void free(void[] mem) { // For safety reasons, the GCAllocator should never explicitly free memory. //GC.free(extractUnalignedPointer(mem.ptr)); } } final class AutoFreeListAllocator : Allocator { import std.typetuple; private { enum minExponent = 5; enum freeListCount = 14; FreeListAlloc[freeListCount] m_freeLists; Allocator m_baseAlloc; } this(Allocator base_allocator) nothrow { m_baseAlloc = base_allocator; foreach (i; iotaTuple!freeListCount) m_freeLists[i] = new FreeListAlloc(nthFreeListSize!(i), m_baseAlloc); } void[] alloc(size_t sz) { auto idx = getAllocatorIndex(sz); return idx < freeListCount ? m_freeLists[idx].alloc()[0 .. sz] : m_baseAlloc.alloc(sz); } void[] realloc(void[] data, size_t sz) { auto curidx = getAllocatorIndex(data.length); auto newidx = getAllocatorIndex(sz); if (curidx == newidx) { if (curidx == freeListCount) { // forward large blocks to the base allocator return m_baseAlloc.realloc(data, sz); } else { // just grow the slice if it still fits into the free list slot return data.ptr[0 .. sz]; } } // otherwise re-allocate manually auto newd = alloc(sz); assert(newd.ptr+sz <= data.ptr || newd.ptr >= data.ptr+data.length, "New block overlaps old one!?"); auto len = min(data.length, sz); newd[0 .. len] = data[0 .. len]; free(data); return newd; } void free(void[] data) { //logTrace("AFL free %08X(%s)", data.ptr, data.length); auto idx = getAllocatorIndex(data.length); if (idx < freeListCount) m_freeLists[idx].free(data.ptr[0 .. 1 << (idx + minExponent)]); else m_baseAlloc.free(data); } // does a CT optimized binary search for the right allocater private int getAllocatorIndex(size_t sz) @safe nothrow @nogc { //pragma(msg, getAllocatorIndexStr!(0, freeListCount)); return mixin(getAllocatorIndexStr!(0, freeListCount)); } private template getAllocatorIndexStr(int low, int high) { static if (__VERSION__ <= 2066) import std.string : format; else import std.format : format; static if (low == high) enum getAllocatorIndexStr = format("%s", low); else { enum mid = (low + high) / 2; enum getAllocatorIndexStr = "sz > nthFreeListSize!%s ? %s : %s" .format(mid, getAllocatorIndexStr!(mid+1, high), getAllocatorIndexStr!(low, mid)); } } unittest { auto a = new AutoFreeListAllocator(null); assert(a.getAllocatorIndex(0) == 0); foreach (i; iotaTuple!freeListCount) { assert(a.getAllocatorIndex(nthFreeListSize!i-1) == i); assert(a.getAllocatorIndex(nthFreeListSize!i) == i); assert(a.getAllocatorIndex(nthFreeListSize!i+1) == i+1); } assert(a.getAllocatorIndex(size_t.max) == freeListCount); } private static pure size_t nthFreeListSize(size_t i)() { return 1 << (i + minExponent); } private template iotaTuple(size_t i) { static if (i > 1) alias iotaTuple = TypeTuple!(iotaTuple!(i-1), i-1); else alias iotaTuple = TypeTuple!(0); } } final class PoolAllocator : Allocator { static struct Pool { Pool* next; void[] data; void[] remaining; } static struct Destructor { Destructor* next; void function(void*) destructor; void* object; } private { Allocator m_baseAllocator; Pool* m_freePools; Pool* m_fullPools; Destructor* m_destructors; size_t m_poolSize; } this(size_t pool_size, Allocator base) nothrow { m_poolSize = pool_size; m_baseAllocator = base; } @property size_t totalSize() { size_t amt = 0; for (auto p = m_fullPools; p; p = p.next) amt += p.data.length; for (auto p = m_freePools; p; p = p.next) amt += p.data.length; return amt; } @property size_t allocatedSize() { size_t amt = 0; for (auto p = m_fullPools; p; p = p.next) amt += p.data.length; for (auto p = m_freePools; p; p = p.next) amt += p.data.length - p.remaining.length; return amt; } void[] alloc(size_t sz) { auto aligned_sz = alignedSize(sz); Pool* pprev = null; Pool* p = cast(Pool*)m_freePools; while( p && p.remaining.length < aligned_sz ){ pprev = p; p = p.next; } if( !p ){ auto pmem = m_baseAllocator.alloc(AllocSize!Pool); p = emplace!Pool(cast(Pool*)pmem.ptr); p.data = m_baseAllocator.alloc(max(aligned_sz, m_poolSize)); p.remaining = p.data; p.next = cast(Pool*)m_freePools; m_freePools = p; pprev = null; } auto ret = p.remaining[0 .. aligned_sz]; p.remaining = p.remaining[aligned_sz .. $]; if( !p.remaining.length ){ if( pprev ){ pprev.next = p.next; } else { m_freePools = p.next; } p.next = cast(Pool*)m_fullPools; m_fullPools = p; } return ret[0 .. sz]; } void[] realloc(void[] arr, size_t newsize) { auto aligned_sz = alignedSize(arr.length); auto aligned_newsz = alignedSize(newsize); if( aligned_newsz <= aligned_sz ) return arr[0 .. newsize]; // TODO: back up remaining auto pool = m_freePools; bool last_in_pool = pool && arr.ptr+aligned_sz == pool.remaining.ptr; if( last_in_pool && pool.remaining.length+aligned_sz >= aligned_newsz ){ pool.remaining = pool.remaining[aligned_newsz-aligned_sz .. $]; arr = arr.ptr[0 .. aligned_newsz]; assert(arr.ptr+arr.length == pool.remaining.ptr, "Last block does not align with the remaining space!?"); return arr[0 .. newsize]; } else { auto ret = alloc(newsize); assert(ret.ptr >= arr.ptr+aligned_sz || ret.ptr+ret.length <= arr.ptr, "New block overlaps old one!?"); ret[0 .. min(arr.length, newsize)] = arr[0 .. min(arr.length, newsize)]; return ret; } } void free(void[] mem) { } void freeAll() { version(VibeManualMemoryManagement){ // destroy all initialized objects for (auto d = m_destructors; d; d = d.next) d.destructor(cast(void*)d.object); m_destructors = null; // put all full Pools into the free pools list for (Pool* p = cast(Pool*)m_fullPools, pnext; p; p = pnext) { pnext = p.next; p.next = cast(Pool*)m_freePools; m_freePools = cast(Pool*)p; } // free up all pools for (Pool* p = cast(Pool*)m_freePools; p; p = p.next) p.remaining = p.data; } } void reset() { version(VibeManualMemoryManagement){ freeAll(); Pool* pnext; for (auto p = cast(Pool*)m_freePools; p; p = pnext) { pnext = p.next; m_baseAllocator.free(p.data); m_baseAllocator.free((cast(void*)p)[0 .. AllocSize!Pool]); } m_freePools = null; } } private static destroy(T)(void* ptr) { static if( is(T == class) ) .destroy(cast(T)ptr); else .destroy(*cast(T*)ptr); } } final class FreeListAlloc : Allocator { nothrow: private static struct FreeListSlot { FreeListSlot* next; } private { FreeListSlot* m_firstFree = null; size_t m_nalloc = 0; size_t m_nfree = 0; Allocator m_baseAlloc; immutable size_t m_elemSize; } this(size_t elem_size, Allocator base_allocator) { assert(elem_size >= size_t.sizeof); m_elemSize = elem_size; m_baseAlloc = base_allocator; logDebug_("Create FreeListAlloc %d", m_elemSize); } @property size_t elementSize() const { return m_elemSize; } void[] alloc(size_t sz) { assert(sz == m_elemSize, "Invalid allocation size."); return alloc(); } void[] alloc() { void[] mem; if( m_firstFree ){ auto slot = m_firstFree; m_firstFree = slot.next; slot.next = null; mem = (cast(void*)slot)[0 .. m_elemSize]; debug m_nfree--; } else { mem = m_baseAlloc.alloc(m_elemSize); //logInfo("Alloc %d bytes: alloc: %d, free: %d", SZ, s_nalloc, s_nfree); } debug m_nalloc++; //logInfo("Alloc %d bytes: alloc: %d, free: %d", SZ, s_nalloc, s_nfree); return mem; } void[] realloc(void[] mem, size_t sz) { assert(mem.length == m_elemSize); assert(sz == m_elemSize); return mem; } void free(void[] mem) { assert(mem.length == m_elemSize, "Memory block passed to free has wrong size."); auto s = cast(FreeListSlot*)mem.ptr; s.next = m_firstFree; m_firstFree = s; m_nalloc--; m_nfree++; } } struct FreeListObjectAlloc(T, bool USE_GC = true, bool INIT = true) { enum ElemSize = AllocSize!T; enum ElemSlotSize = max(AllocSize!T, Slot.sizeof); static if( is(T == class) ){ alias TR = T; } else { alias TR = T*; } struct Slot { Slot* next; } private static Slot* s_firstFree; static TR alloc(ARGS...)(ARGS args) { void[] mem; if (s_firstFree !is null) { auto ret = s_firstFree; s_firstFree = s_firstFree.next; ret.next = null; mem = (cast(void*)ret)[0 .. ElemSize]; } else { //logInfo("alloc %s/%d", T.stringof, ElemSize); mem = manualAllocator().alloc(ElemSlotSize); static if( hasIndirections!T ) GC.addRange(mem.ptr, ElemSlotSize); } static if (INIT) return cast(TR)internalEmplace!(Unqual!T)(mem, args); // FIXME: this emplace has issues with qualified types, but Unqual!T may result in the wrong constructor getting called. else return cast(TR)mem.ptr; } static void free(TR obj) { static if (INIT) { scope (failure) assert(0, "You shouldn't throw in destructors"); auto objc = obj; static if (is(TR == T*)) .destroy(*objc);//typeid(T).destroy(cast(void*)obj); else .destroy(objc); } auto sl = cast(Slot*)obj; sl.next = s_firstFree; s_firstFree = sl; //static if( hasIndirections!T ) GC.removeRange(cast(void*)obj); //manualAllocator().free((cast(void*)obj)[0 .. ElemSlotSize]); } } template AllocSize(T) { static if (is(T == class)) { // workaround for a strange bug where AllocSize!SSLStream == 0: TODO: dustmite! enum dummy = T.stringof ~ __traits(classInstanceSize, T).stringof; enum AllocSize = __traits(classInstanceSize, T); } else { enum AllocSize = T.sizeof; } } struct FreeListRef(T, bool INIT = true) { alias ObjAlloc = FreeListObjectAlloc!(T, true, INIT); enum ElemSize = AllocSize!T; static if( is(T == class) ){ alias TR = T; } else { alias TR = T*; } private TR m_object; private size_t m_magic = 0x1EE75817; // workaround for compiler bug static FreeListRef opCall(ARGS...)(ARGS args) { //logInfo("refalloc %s/%d", T.stringof, ElemSize); FreeListRef ret; ret.m_object = ObjAlloc.alloc(args); ret.refCount = 1; return ret; } ~this() { //if( m_object ) logInfo("~this!%s(): %d", T.stringof, this.refCount); //if( m_object ) logInfo("ref %s destructor %d", T.stringof, refCount); //else logInfo("ref %s destructor %d", T.stringof, 0); clear(); m_magic = 0; m_object = null; } this(this) { checkInvariants(); if( m_object ){ //if( m_object ) logInfo("this!%s(this): %d", T.stringof, this.refCount); this.refCount++; } } void opAssign(FreeListRef other) { clear(); m_object = other.m_object; if( m_object ){ //logInfo("opAssign!%s(): %d", T.stringof, this.refCount); refCount++; } } void clear() { checkInvariants(); if (m_object) { if (--this.refCount == 0) ObjAlloc.free(m_object); } m_object = null; m_magic = 0x1EE75817; } @property const(TR) get() const { checkInvariants(); return m_object; } @property TR get() { checkInvariants(); return m_object; } alias get this; private @property ref int refCount() const { auto ptr = cast(ubyte*)cast(void*)m_object; ptr += ElemSize; return *cast(int*)ptr; } private void checkInvariants() const { assert(m_magic == 0x1EE75817); assert(!m_object || refCount > 0); } } private void* extractUnalignedPointer(void* base) nothrow { ubyte misalign = *(cast(ubyte*)base-1); assert(misalign <= Allocator.alignment); return base - misalign; } private void* adjustPointerAlignment(void* base) nothrow { ubyte misalign = Allocator.alignment - (cast(size_t)base & Allocator.alignmentMask); base += misalign; *(cast(ubyte*)base-1) = misalign; return base; } unittest { void test_align(void* p, size_t adjustment) { void* pa = adjustPointerAlignment(p); assert((cast(size_t)pa & Allocator.alignmentMask) == 0, "Non-aligned pointer."); assert(*(cast(ubyte*)pa-1) == adjustment, "Invalid adjustment "~to!string(p)~": "~to!string(*(cast(ubyte*)pa-1))); void* pr = extractUnalignedPointer(pa); assert(pr == p, "Recovered base != original"); } void* ptr = .malloc(0x40); ptr += Allocator.alignment - (cast(size_t)ptr & Allocator.alignmentMask); test_align(ptr++, 0x10); test_align(ptr++, 0x0F); test_align(ptr++, 0x0E); test_align(ptr++, 0x0D); test_align(ptr++, 0x0C); test_align(ptr++, 0x0B); test_align(ptr++, 0x0A); test_align(ptr++, 0x09); test_align(ptr++, 0x08); test_align(ptr++, 0x07); test_align(ptr++, 0x06); test_align(ptr++, 0x05); test_align(ptr++, 0x04); test_align(ptr++, 0x03); test_align(ptr++, 0x02); test_align(ptr++, 0x01); test_align(ptr++, 0x10); } private size_t alignedSize(size_t sz) nothrow { return ((sz + Allocator.alignment - 1) / Allocator.alignment) * Allocator.alignment; } unittest { foreach( i; 0 .. 20 ){ auto ia = alignedSize(i); assert(ia >= i); assert((ia & Allocator.alignmentMask) == 0); assert(ia < i+Allocator.alignment); } } private void ensureValidMemory(void[] mem) nothrow { auto bytes = cast(ubyte[])mem; swap(bytes[0], bytes[$-1]); swap(bytes[0], bytes[$-1]); } /// See issue #14194 private T internalEmplace(T, Args...)(void[] chunk, auto ref Args args) if (is(T == class)) in { import std.string, std.format; assert(chunk.length >= T.sizeof, format("emplace: Chunk size too small: %s < %s size = %s", chunk.length, T.stringof, T.sizeof)); assert((cast(size_t) chunk.ptr) % T.alignof == 0, format("emplace: Misaligned memory block (0x%X): it must be %s-byte aligned for type %s", chunk.ptr, T.alignof, T.stringof)); } body { enum classSize = __traits(classInstanceSize, T); auto result = cast(T) chunk.ptr; // Initialize the object in its pre-ctor state chunk[0 .. classSize] = typeid(T).init[]; // Call the ctor if any static if (is(typeof(result.__ctor(args)))) { // T defines a genuine constructor accepting args // Go the classic route: write .init first, then call ctor result.__ctor(args); } else { static assert(args.length == 0 && !is(typeof(&T.__ctor)), "Don't know how to initialize an object of type " ~ T.stringof ~ " with arguments " ~ Args.stringof); } return result; } /// Dittor private auto internalEmplace(T, Args...)(void[] chunk, auto ref Args args) if (!is(T == class)) in { import std.string, std.format; assert(chunk.length >= T.sizeof, format("emplace: Chunk size too small: %s < %s size = %s", chunk.length, T.stringof, T.sizeof)); assert((cast(size_t) chunk.ptr) % T.alignof == 0, format("emplace: Misaligned memory block (0x%X): it must be %s-byte aligned for type %s", chunk.ptr, T.alignof, T.stringof)); } body { return emplace(cast(T*)chunk.ptr, args); } private void logDebug_(ARGS...)(string msg, ARGS args) {}