diff --git a/source/vibe/container/hashmap.d b/source/vibe/container/hashmap.d new file mode 100644 index 0000000..d524c0a --- /dev/null +++ b/source/vibe/container/hashmap.d @@ -0,0 +1,508 @@ +/** + Internal hash map implementation. + + Copyright: © 2013-2023 Sönke Ludwig + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module vibe.container.hashmap; + +import vibe.container.seahash : seaHash; +import vibe.container.internal.utilallocator; + +import std.conv : emplace; +import std.traits; + + +struct DefaultHashMapTraits(Key) { + enum clearValue = Key.init; + static bool equals(in Key a, in Key b) + { + static if (__traits(isFinalClass, Key) && &Unqual!Key.init.opEquals is &Object.init.opEquals) + return a is b; + else static if (is(Key == class)) + // BUG: improperly casting away const + return () @trusted { return a is b ? true : (a !is null && (cast(Object) a).opEquals(cast(Object) b)); }(); + else return a == b; + } + + static size_t hashOf(const scope ref Key k) + @safe { + static if (__traits(isFinalClass, Key) && &Unqual!Key.init.toHash is &Object.init.toHash) + return () @trusted { return cast(size_t)cast(void*)k; } (); + else static if (__traits(compiles, Key.init.toHash())) + return () @trusted { return (cast(Key)k).toHash(); } (); + else static if (__traits(compiles, Key.init.toHashShared())) + return k.toHashShared(); + else static if (__traits(isScalar, Key)) + return cast(size_t)k; + else static if (isArray!Key && is(Key : E[], E) && __traits(isScalar, E)) + return cast(size_t)seaHash(cast(const(ubyte)[])k); + else { + // evil casts to be able to get the most basic operations of + // HashMap nothrow and @nogc + static size_t hashWrapper(const scope ref Key k) { + static typeinfo = typeid(Key); + return typeinfo.getHash(&k); + } + static @nogc nothrow size_t properlyTypedWrapper(const scope ref Key k) { return 0; } + return () @trusted { return (cast(typeof(&properlyTypedWrapper))&hashWrapper)(k); } (); + } + } +} + +unittest +{ + final class Integer : Object { + public const int value; + + this(int x) @nogc nothrow pure @safe { value = x; } + + override bool opEquals(Object rhs) const @nogc nothrow pure @safe { + if (auto r = cast(Integer) rhs) + return value == r.value; + return false; + } + + override size_t toHash() const @nogc nothrow pure @safe { + return value; + } + } + + auto hashMap = HashMap!(Object, int)(vibeThreadAllocator()); + foreach (x; [2, 4, 8, 16]) + hashMap[new Integer(x)] = x; + foreach (x; [2, 4, 8, 16]) + assert(hashMap[new Integer(x)] == x); +} + +struct HashMap(TKey, TValue, Traits = DefaultHashMapTraits!TKey, Allocator = IAllocator) + if (is(typeof(Traits.clearValue) : TKey)) +{ + import core.memory : GC; + import vibe.container.internal.traits : isOpApplyDg; + import std.algorithm.iteration : filter, map; + + alias Key = TKey; + alias Value = TValue; + + Allocator AW(Allocator a) { return a; } + alias AllocatorType = AffixAllocator!(Allocator, int); + static if (is(typeof(AllocatorType.instance))) + alias AllocatorInstanceType = typeof(AllocatorType.instance); + else alias AllocatorInstanceType = AllocatorType; + + struct TableEntry { + UnConst!Key key = Traits.clearValue; + Value value; + + this(ref Key key, ref Value value) + { + import std.algorithm.mutation : move; + this.key = cast(UnConst!Key)key; + static if (is(typeof(value.move))) + this.value = value.move; + else this.value = value; + } + } + private { + TableEntry[] m_table; // NOTE: capacity is always POT + size_t m_length; + static if (!is(typeof(Allocator.instance))) + AllocatorInstanceType m_allocator; + bool m_resizing; + } + + static if (!is(typeof(Allocator.instance))) { + this(Allocator allocator) + { + m_allocator = typeof(m_allocator)(AW(allocator)); + } + } + + ~this() + { + int rc; + try rc = m_table is null ? 1 : () @trusted { return --allocator.prefix(m_table); } (); + catch (Exception e) assert(false, e.msg); + + if (rc == 0) { + clear(); + if (m_table.ptr !is null) () @trusted { + static if (hasIndirections!TableEntry) GC.removeRange(m_table.ptr); + try allocator.dispose(m_table); + catch (Exception e) assert(false, e.msg); + } (); + } + } + + this(this) + @trusted { + if (m_table.ptr) { + try allocator.prefix(m_table)++; + catch (Exception e) assert(false, e.msg); + } + } + + @property size_t length() const { return m_length; } + + void remove(Key key) + { + import std.algorithm.mutation : move; + + auto idx = findIndex(key); + assert (idx != size_t.max, "Removing non-existent element."); + auto i = idx; + while (true) { + m_table[i].key = Traits.clearValue; + m_table[i].value = Value.init; + + size_t j = i, r; + do { + if (++i >= m_table.length) i -= m_table.length; + if (Traits.equals(m_table[i].key, Traits.clearValue)) { + m_length--; + return; + } + r = Traits.hashOf(m_table[i].key) & (m_table.length-1); + } while ((j= 1 && arity!del <= 2, + "isOpApplyDg should have prevented this"); + static if (arity!del == 1) { + if (int ret = del(m_table[i].value)) + return ret; + } else + if (int ret = del(m_table[i].key, m_table[i].value)) + return ret; + } + return 0; + } + + auto byKey() { return bySlot.map!(e => e.key); } + auto byKey() const { return bySlot.map!(e => e.key); } + auto byValue() { return bySlot.map!(e => e.value); } + auto byValue() const { return bySlot.map!(e => e.value); } + auto byKeyValue() { import std.typecons : Tuple; return bySlot.map!(e => Tuple!(Key, "key", Value, "value")(e.key, e.value)); } + auto byKeyValue() const { import std.typecons : Tuple; return bySlot.map!(e => Tuple!(const(Key), "key", const(Value), "value")(e.key, e.value)); } + + private auto bySlot() { return m_table[].filter!(e => !Traits.equals(e.key, Traits.clearValue)); } + private auto bySlot() const { return m_table[].filter!(e => !Traits.equals(e.key, Traits.clearValue)); } + + private @property AllocatorInstanceType allocator() + { + static if (is(typeof(Allocator.instance))) + return AllocatorType.instance; + else { + if (!m_allocator._parent) { + static if (is(Allocator == IAllocator)) { + try m_allocator = typeof(m_allocator)(AW(vibeThreadAllocator())); + catch (Exception e) assert(false, e.msg); + } else assert(false, "Allocator not initialized."); + } + return m_allocator; + } + } + + private size_t findIndex(Key key) + const { + if (m_length == 0) return size_t.max; + size_t start = Traits.hashOf(key) & (m_table.length-1); + auto i = start; + while (!Traits.equals(m_table[i].key, key)) { + if (Traits.equals(m_table[i].key, Traits.clearValue)) return size_t.max; + if (++i >= m_table.length) i -= m_table.length; + if (i == start) return size_t.max; + } + return i; + } + + private size_t findInsertIndex(Key key) + const { + auto hash = Traits.hashOf(key); + size_t target = hash & (m_table.length-1); + auto i = target; + while (!Traits.equals(m_table[i].key, Traits.clearValue) && !Traits.equals(m_table[i].key, key)) { + if (++i >= m_table.length) i -= m_table.length; + assert (i != target, "No free bucket found, HashMap full!?"); + } + return i; + } + + private void grow(size_t amount) + @trusted { + auto newsize = m_length + amount; + if (newsize < (m_table.length*2)/3) { + int rc; + try rc = allocator.prefix(m_table); + catch (Exception e) assert(false, e.msg); + if (rc > 1) { + // enforce copy-on-write + auto oldtable = m_table; + try { + m_table = allocator.makeArray!TableEntry(m_table.length); + m_table[] = oldtable; + allocator.prefix(oldtable)--; + assert(allocator.prefix(oldtable) > 0); + allocator.prefix(m_table) = 1; + } catch (Exception e) { + assert(false, e.msg); + } + } + return; + } + auto newcap = m_table.length ? m_table.length : 16; + while (newsize >= (newcap*2)/3) newcap *= 2; + resize(newcap); + } + + private void resize(size_t new_size) + @trusted { + assert(!m_resizing); + m_resizing = true; + scope(exit) m_resizing = false; + + uint pot = 0; + while (new_size > 1) { + pot++; + new_size /= 2; + } + new_size = 1 << pot; + + auto oldtable = m_table; + + // allocate the new array, automatically initializes with empty entries (Traits.clearValue) + try { + m_table = allocator.makeArray!TableEntry(new_size); + allocator.prefix(m_table) = 1; + } catch (Exception e) assert(false, e.msg); + static if (hasIndirections!TableEntry) GC.addRange(m_table.ptr, m_table.length * TableEntry.sizeof); + // perform a move operation of all non-empty elements from the old array to the new one + foreach (ref el; oldtable) + if (!Traits.equals(el.key, Traits.clearValue)) { + auto idx = findInsertIndex(el.key); + (cast(ubyte[])(&m_table[idx])[0 .. 1])[] = (cast(ubyte[])(&el)[0 .. 1])[]; + } + + // all elements have been moved to the new array, so free the old one without calling destructors + int rc; + try rc = oldtable is null ? 1 : --allocator.prefix(oldtable); + catch (Exception e) assert(false, e.msg); + if (rc == 0) { + static if (hasIndirections!TableEntry) GC.removeRange(oldtable.ptr); + try allocator.deallocate(oldtable); + catch (Exception e) assert(false, e.msg); + } + } +} + +nothrow unittest { + import std.conv; + + HashMap!(string, string) map; + + foreach (i; 0 .. 100) { + map[to!string(i)] = to!string(i) ~ "+"; + assert(map.length == i+1); + } + + foreach (i; 0 .. 100) { + auto str = to!string(i); + auto pe = str in map; + assert(pe !is null && *pe == str ~ "+"); + assert(map[str] == str ~ "+"); + } + + foreach (i; 0 .. 50) { + map.remove(to!string(i)); + assert(map.length == 100-i-1); + } + + foreach (i; 50 .. 100) { + auto str = to!string(i); + auto pe = str in map; + assert(pe !is null && *pe == str ~ "+"); + assert(map[str] == str ~ "+"); + } +} + +// test for nothrow/@nogc compliance +nothrow unittest { + HashMap!(int, int) map1; + HashMap!(string, string) map2; + map1[1] = 2; + map2["1"] = "2"; + + @nogc nothrow void performNoGCOps() + { + foreach (int v; map1) {} + foreach (int k, int v; map1) {} + assert(1 in map1); + assert(map1.length == 1); + assert(map1[1] == 2); + assert(map1.getNothrow(1, -1) == 2); + + foreach (string v; map2) {} + foreach (string k, string v; map2) {} + assert("1" in map2); + assert(map2.length == 1); + assert(map2["1"] == "2"); + assert(map2.getNothrow("1", "") == "2"); + } + + performNoGCOps(); +} + +unittest { // test for proper use of constructor/post-blit/destructor + static struct Test { + static size_t constructedCounter = 0; + bool constructed = false; + this(int) { constructed = true; constructedCounter++; } + this(this) nothrow { if (constructed) constructedCounter++; } + ~this() nothrow { if (constructed) constructedCounter--; } + } + + assert(Test.constructedCounter == 0); + + { // sanity check + Test t; + assert(Test.constructedCounter == 0); + t = Test(1); + assert(Test.constructedCounter == 1); + auto u = t; + assert(Test.constructedCounter == 2); + t = Test.init; + assert(Test.constructedCounter == 1); + } + assert(Test.constructedCounter == 0); + + { // basic insertion and hash map resizing + HashMap!(int, Test) map; + foreach (i; 1 .. 67) { + map[i] = Test(1); + assert(Test.constructedCounter == i); + } + } + + assert(Test.constructedCounter == 0); + + { // test clear() and overwriting existing entries + HashMap!(int, Test) map; + foreach (i; 1 .. 67) { + map[i] = Test(1); + assert(Test.constructedCounter == i); + } + map.clear(); + foreach (i; 1 .. 67) { + map[i] = Test(1); + assert(Test.constructedCounter == i); + } + foreach (i; 1 .. 67) { + map[i] = Test(1); + assert(Test.constructedCounter == 66); + } + } + + assert(Test.constructedCounter == 0); + + { // test removing entries and adding entries after remove + HashMap!(int, Test) map; + foreach (i; 1 .. 67) { + map[i] = Test(1); + assert(Test.constructedCounter == i); + } + foreach (i; 1 .. 33) { + map.remove(i); + assert(Test.constructedCounter == 66 - i); + } + foreach (i; 67 .. 130) { + map[i] = Test(1); + assert(Test.constructedCounter == i - 32); + } + } + + assert(Test.constructedCounter == 0); +} + +private template UnConst(T) { + static if (is(T U == const(U))) { + alias UnConst = U; + } else static if (is(T V == immutable(V))) { + alias UnConst = V; + } else alias UnConst = T; +} diff --git a/source/vibe/container/internal/traits.d b/source/vibe/container/internal/traits.d new file mode 100644 index 0000000..437267a --- /dev/null +++ b/source/vibe/container/internal/traits.d @@ -0,0 +1,32 @@ +module vibe.container.internal.traits; + +import std.traits; + + +/// Test if the type $(D DG) is a correct delegate for an opApply where the +/// key/index is of type $(D TKEY) and the value of type $(D TVALUE). +template isOpApplyDg(DG, TKEY, TVALUE) { + import std.traits; + static if (is(DG == delegate) && is(ReturnType!DG : int)) { + private alias PTT = ParameterTypeTuple!(DG); + private alias PSCT = ParameterStorageClassTuple!(DG); + private alias STC = ParameterStorageClass; + // Just a value + static if (PTT.length == 1) { + enum isOpApplyDg = (is(PTT[0] == TVALUE)); + } else static if (PTT.length == 2) { + enum isOpApplyDg = (is(PTT[0] == TKEY)) + && (is(PTT[1] == TVALUE)); + } else + enum isOpApplyDg = false; + } else { + enum isOpApplyDg = false; + } +} + +unittest { + static assert(isOpApplyDg!(int delegate(int, string), int, string)); + static assert(isOpApplyDg!(int delegate(ref int, ref string), int, string)); + static assert(isOpApplyDg!(int delegate(int, ref string), int, string)); + static assert(isOpApplyDg!(int delegate(ref int, string), int, string)); +} diff --git a/source/vibe/container/internal/utilallocator.d b/source/vibe/container/internal/utilallocator.d new file mode 100644 index 0000000..c4881a1 --- /dev/null +++ b/source/vibe/container/internal/utilallocator.d @@ -0,0 +1,183 @@ +module vibe.container.internal.utilallocator; + +public import stdx.allocator : allocatorObject, CAllocatorImpl, dispose, + expandArray, IAllocator, make, makeArray, shrinkArray, theAllocator; +public import stdx.allocator.mallocator; +public import stdx.allocator.building_blocks.affix_allocator; + +// NOTE: this needs to be used instead of theAllocator due to Phobos issue 17564 +@property IAllocator vibeThreadAllocator() +@safe nothrow @nogc { + import stdx.allocator.gc_allocator; + static IAllocator s_threadAllocator; + if (!s_threadAllocator) + s_threadAllocator = () @trusted { return allocatorObject(GCAllocator.instance); } (); + return s_threadAllocator; +} + +final class RegionListAllocator(Allocator, bool leak = false) : IAllocator { + import vibe.internal.memory_legacy : AllocSize, alignedSize; + import std.algorithm.comparison : min, max; + import std.conv : emplace; + + import std.typecons : Ternary; + + static struct Pool { Pool* next; void[] data; void[] remaining; } + private { + Allocator m_baseAllocator; + Pool* m_freePools; + Pool* m_fullPools; + size_t m_poolSize; + } + + this(size_t pool_size, Allocator base) @safe nothrow + { + m_poolSize = pool_size; + m_baseAllocator = base; + } + + ~this() + { + deallocateAll(); + } + + override @property uint alignment() const { return 0x10; } + + @property size_t totalSize() + @safe nothrow @nogc { + 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() + @safe nothrow @nogc { + 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; + } + + override void[] allocate(size_t sz, TypeInfo ti = null) + { + 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.allocate(AllocSize!Pool); + + p = emplace!Pool(cast(Pool*)pmem.ptr); + p.data = m_baseAllocator.allocate(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]; + } + + override void[] alignedAllocate(size_t n, uint a) { return null; } + override bool alignedReallocate(ref void[] b, size_t size, uint alignment) { return false; } + override void[] allocateAll() { return null; } + override @property Ternary empty() const { return m_fullPools !is null ? Ternary.no : Ternary.yes; } + override size_t goodAllocSize(size_t s) { return alignedSize(s); } + + import std.traits : Parameters; + static if (is(Parameters!(IAllocator.resolveInternalPointer)[0] == const(void*))) { + override Ternary resolveInternalPointer(const void* p, ref void[] result) { return Ternary.unknown; } + } else { + override Ternary resolveInternalPointer(void* p, ref void[] result) { return Ternary.unknown; } + } + static if (is(Parameters!(IAllocator.owns)[0] == const(void[]))) { + override Ternary owns(const void[] b) { return Ternary.unknown; } + } else { + override Ternary owns(void[] b) { return Ternary.unknown; } + } + + + override bool reallocate(ref void[] arr, size_t newsize) + { + return expand(arr, newsize); + } + + override bool expand(ref void[] arr, size_t newsize) + { + auto aligned_sz = alignedSize(arr.length); + auto aligned_newsz = alignedSize(newsize); + + if (aligned_newsz <= aligned_sz) { + arr = arr[0 .. newsize]; // TODO: back up remaining + return true; + } + + 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!?"); + arr = arr[0 .. newsize]; + } else { + auto ret = allocate(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)]; + arr = ret; + } + return true; + } + + override bool deallocate(void[] mem) + { + return false; + } + + override bool deallocateAll() + { + // 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; + + Pool* pnext; + for (auto p = cast(Pool*)m_freePools; p; p = pnext) { + pnext = p.next; + static if (!leak) { + m_baseAllocator.deallocate(p.data); + m_baseAllocator.deallocate((cast(void*)p)[0 .. AllocSize!Pool]); + } + } + m_freePools = null; + + return true; + } +}