440 changes: 440 additions & 0 deletions src/core/aa.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,440 @@
/**
* Implementation of associative arrays.
*
* Copyright: Martin Nowak 2015 -.
* License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
* Authors: Martin Nowak
*/
module core.aa;

import core.memory : GC;

private
{
// grow threshold
enum GROW_NUM = 4;
enum GROW_DEN = 5;
// shrink threshold
enum SHRINK_NUM = 1;
enum SHRINK_DEN = 8;
// grow factor
enum GROW_FAC = 4;
// growing the AA doubles it's size, so the shrink threshold must be
// smaller than half the grow threshold to have a hysteresis
static assert(GROW_FAC * SHRINK_NUM * GROW_DEN < GROW_NUM * SHRINK_DEN);
// initial load factor (for literals), mean of both thresholds
enum INIT_NUM = (GROW_DEN * SHRINK_NUM + GROW_NUM * SHRINK_DEN) / 2;
enum INIT_DEN = SHRINK_DEN * GROW_DEN;

// magic hash constants to distinguish empty, deleted, and filled buckets
enum HASH_EMPTY = 0;
enum HASH_DELETED = 0x1;
enum HASH_FILLED_MARK = size_t(1) << 8 * size_t.sizeof - 1;
}

enum INIT_NUM_BUCKETS = 8;

struct AA(Key, Val)
{
this(size_t sz)
{
impl = new Impl(nextpow2(sz));
}

@property bool empty() const pure nothrow @safe @nogc
{
return !length;
}

@property size_t length() const pure nothrow @safe @nogc
{
return impl is null ? 0 : impl.length;
}

void opIndexAssign(Val val, in Key key)
{
// lazily alloc implementation
if (impl is null)
impl = new Impl(INIT_NUM_BUCKETS);

// get hash and bucket for key
immutable hash = calcHash(key);

// found a value => assignment
if (auto p = impl.findSlotLookup(hash, key))
{
p.entry.val = val;
return;
}

auto p = findSlotInsert(hash);
if (p.deleted)
--deleted;
// check load factor and possibly grow
else if (++used * GROW_DEN > dim * GROW_NUM)
{
grow();
p = findSlotInsert(hash);
assert(p.empty);
}

// update search cache and allocate entry
firstUsed = min(firstUsed, cast(uint)(p - buckets.ptr));
p.hash = hash;
p.entry = new Impl.Entry(key, val); // TODO: move
return;
}

ref inout(Val) opIndex(in Key key) inout @trusted
{
auto p = opIn_r(key);
assert(p !is null);
return *p;
}

inout(Val)* opIn_r(in Key key) inout @trusted
{
if (empty)
return null;

immutable hash = calcHash(key);
if (auto p = findSlotLookup(hash, key))
return &p.entry.val;
return null;
}

bool remove(in Key key)
{
if (empty)
return false;

immutable hash = calcHash(key);
if (auto p = findSlotLookup(hash, key))
{
// clear entry
p.hash = HASH_DELETED;
p.entry = null;

++deleted;
if (length * SHRINK_DEN < dim * SHRINK_NUM)
shrink();

return true;
}
return false;
}

Val get(in Key key, lazy Val val)
{
auto p = opIn_r(key);
return p is null ? val : *p;
}

ref Val getOrSet(in Key key, lazy Val val)
{
// lazily alloc implementation
if (impl is null)
impl = new Impl(INIT_NUM_BUCKETS);

// get hash and bucket for key
immutable hash = calcHash(key);

// found a value => assignment
if (auto p = impl.findSlotLookup(hash, key))
return p.entry.val;

auto p = findSlotInsert(hash);
if (p.deleted)
--deleted;
// check load factor and possibly grow
else if (++used * GROW_DEN > dim * GROW_NUM)
{
grow();
p = findSlotInsert(hash);
assert(p.empty);
}

// update search cache and allocate entry
firstUsed = min(firstUsed, cast(uint)(p - buckets.ptr));
p.hash = hash;
p.entry = new Impl.Entry(key, val);
return p.entry.val;
}

/**
Convert the AA to the type of the builtin language AA.
*/
Val[Key] toBuiltinAA() pure nothrow
{
return cast(Val[Key]) _aaFromCoreAA(impl, rtInterface);
}

private:

private this(inout(Impl)* impl) inout
{
this.impl = impl;
}

ref Val getLValue(in Key key)
{
// lazily alloc implementation
if (impl is null)
impl = new Impl(INIT_NUM_BUCKETS);

// get hash and bucket for key
immutable hash = calcHash(key);

// found a value => assignment
if (auto p = impl.findSlotLookup(hash, key))
return p.entry.val;

auto p = findSlotInsert(hash);
if (p.deleted)
--deleted;
// check load factor and possibly grow
else if (++used * GROW_DEN > dim * GROW_NUM)
{
grow();
p = findSlotInsert(hash);
assert(p.empty);
}

// update search cache and allocate entry
firstUsed = min(firstUsed, cast(uint)(p - buckets.ptr));
p.hash = hash;
p.entry = new Impl.Entry(key); // TODO: move
return p.entry.val;
}

static struct Impl
{
this(size_t sz)
{
buckets = allocBuckets(sz);
}

@property size_t length() const pure nothrow @nogc
{
assert(used >= deleted);
return used - deleted;
}

@property size_t dim() const pure nothrow @nogc
{
return buckets.length;
}

@property size_t mask() const pure nothrow @nogc
{
return dim - 1;
}

// find the first slot to insert a value with hash
inout(Bucket)* findSlotInsert(size_t hash) inout pure nothrow @nogc
{
for (size_t i = hash & mask, j = 1;; ++j)
{
if (!buckets[i].filled)
return &buckets[i];
i = (i + j) & mask;
}
}

// lookup a key
inout(Bucket)* findSlotLookup(size_t hash, in Key key) inout
{
for (size_t i = hash & mask, j = 1;; ++j)
{
if (buckets[i].hash == hash && key == buckets[i].entry.key)
return &buckets[i];
else if (buckets[i].empty)
return null;
i = (i + j) & mask;
}
}

void grow()
{
// If there are so many deleted entries, that growing would push us
// below the shrink threshold, we just purge deleted entries instead.
if (length * SHRINK_DEN < GROW_FAC * dim * SHRINK_NUM)
resize(dim);
else
resize(GROW_FAC * dim);
}

void shrink()
{
if (dim > INIT_NUM_BUCKETS)
resize(dim / GROW_FAC);
}

void resize(size_t ndim) pure nothrow
{
auto obuckets = buckets;
buckets = allocBuckets(ndim);

foreach (ref b; obuckets)
if (b.filled)
*findSlotInsert(b.hash) = b;

firstUsed = 0;
used -= deleted;
deleted = 0;
GC.free(obuckets.ptr); // safe to free b/c impossible to reference
}

static struct Entry
{
Key key;
Val val;
}

static struct Bucket
{
size_t hash;
Entry* entry;

@property bool empty() const
{
return hash == HASH_EMPTY;
}

@property bool deleted() const
{
return hash == HASH_DELETED;
}

@property bool filled() const
{
return cast(ptrdiff_t) hash < 0;
}
}

Bucket[] allocBuckets(size_t dim) @trusted pure nothrow
{
enum attr = GC.BlkAttr.NO_INTERIOR;
immutable sz = dim * Bucket.sizeof;
return (cast(Bucket*) GC.calloc(sz, attr))[0 .. dim];
}

Bucket[] buckets;
uint used;
uint deleted;
uint firstUsed;
}

RTInterface* rtInterface()() pure nothrow @nogc
{
static size_t aaLen(in void* pimpl) pure nothrow @nogc
{
auto aa = const(AA)(cast(const(Impl)*) pimpl);
return aa.length;
}

static void* aaGetY(void** pimpl, in void* pkey)
{
auto aa = AA(cast(Impl*)*pimpl);
auto res = &aa.getLValue(*cast(const(Key*)) pkey);
*pimpl = aa.impl; // might have changed
return res;
}

static inout(void)* aaInX(inout void* pimpl, in void* pkey)
{
auto aa = inout(AA)(cast(inout(Impl)*) pimpl);
return aa.opIn_r(*cast(const(Key*)) pkey);
}

static bool aaDelX(void* pimpl, in void* pkey)
{
auto aa = AA(cast(Impl*) pimpl);
return aa.remove(*cast(const(Key*)) pkey);
}

static immutable vtbl = RTInterface(&aaLen, &aaGetY, &aaInX, &aaDelX);
return cast(RTInterface*)&vtbl;
}

static size_t calcHash(in ref Key key)
{
return hashOf(key) | HASH_FILLED_MARK;
}

Impl* impl;
alias impl this;
}

package extern (C) void* _aaFromCoreAA(void* impl, RTInterface* rtIntf) pure nothrow;

private:

struct RTInterface
{
alias AA = void*;

size_t function(in AA aa) pure nothrow @nogc len;
void* function(AA* aa, in void* pkey) getY;
inout(void)* function(inout AA aa, in void* pkey) inX;
bool function(AA aa, in void* pkey) delX;
}

unittest
{
AA!(int, int) aa;
assert(aa.length == 0);
aa[0] = 1;
assert(aa.length == 1 && aa[0] == 1);
aa[1] = 2;
assert(aa.length == 2 && aa[1] == 2);
import core.stdc.stdio;

int[int] rtaa = aa.toBuiltinAA();
assert(rtaa.length == 2);
puts("length");
assert(rtaa[0] == 1);
assert(rtaa[1] == 2);
rtaa[2] = 3;

assert(aa[2] == 3);
}

unittest
{
auto aa = AA!(int, int)(3);
aa[0] = 0;
aa[1] = 1;
aa[2] = 2;
assert(aa.length == 3);
}

//==============================================================================
// Helper functions
//------------------------------------------------------------------------------

size_t nextpow2(in size_t n) pure nothrow @nogc
{
import core.bitop : bsr;

if (n < 2)
return 1;
return size_t(1) << bsr(n - 1) + 1;
}

pure nothrow @nogc unittest
{
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
foreach (const n, const pow2; [1, 1, 2, 4, 4, 8, 8, 8, 8, 16])
assert(nextpow2(n) == pow2);
}

T min(T)(T a, T b) pure nothrow @nogc
{
return a < b ? a : b;
}

T max(T)(T a, T b) pure nothrow @nogc
{
return b < a ? a : b;
}
68 changes: 60 additions & 8 deletions src/rt/aaA.d
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module rt.aaA;
extern (C) immutable int _aaVersion = 1;

import core.memory : GC;
import core.aa : RTInterface;

// grow threshold
private enum GROW_NUM = 4;
Expand Down Expand Up @@ -45,6 +46,27 @@ struct AA
}
}

private struct CoreAAImpl
{
this(void* impl, RTInterface* rtInterface) pure nothrow
{
this.impl = impl;
this._rtInterface = cast(RTInterface*)(cast(size_t) rtInterface | 0x1);
}

void* impl;
RTInterface* _rtInterface;
@property RTInterface* rtInterface() pure nothrow const @nogc
{
return cast(RTInterface*)(cast(size_t) _rtInterface & ~size_t(0x1));
}

@property bool isCoreAA() const pure nothrow @nogc
{
return cast(size_t) _rtInterface & 0x1;
}
}

private struct Impl
{
private:
Expand All @@ -65,7 +87,17 @@ private:
flags |= Flags.hasPointers;
}

Bucket[] buckets;
union
{
Bucket[] buckets;
CoreAAImpl coreAA;
}

@property bool isCoreAA() const pure nothrow @nogc
{
return coreAA.isCoreAA;
}

uint used;
uint deleted;
TypeInfo_Struct entryTI;
Expand Down Expand Up @@ -318,11 +350,9 @@ private size_t nextpow2(in size_t n) pure nothrow @nogc
{
import core.bitop : bsr;

if (!n)
if (n < 2)
return 1;

const isPowerOf2 = !((n - 1) & n);
return 1 << (bsr(n) + !isPowerOf2);
return size_t(1) << bsr(n - 1) + 1;
}

pure nothrow @nogc unittest
Expand All @@ -346,10 +376,22 @@ private T max(T)(T a, T b) pure nothrow @nogc
// API Implementation
//------------------------------------------------------------------------------

/// Create a runtime AA from a typed core.aa
extern (C) void* _aaFromCoreAA(void* impl, RTInterface* rtIntf) pure nothrow
{
return new CoreAAImpl(impl, rtIntf);
}

/// Determine number of entries in associative array.
extern (C) size_t _aaLen(in AA aa) pure nothrow @nogc
{
return aa ? aa.length : 0;
if (aa.impl is null)
return 0;

if (aa.isCoreAA)
return aa.coreAA.rtInterface.len(aa.coreAA.impl);
else
return aa.length;
}

/******************************
Expand All @@ -371,6 +413,8 @@ extern (C) void* _aaGetY(AA* aa, const TypeInfo_AssociativeArray ti, in size_t v
// lazily alloc implementation
if (aa.impl is null)
aa.impl = new Impl(ti);
else if (aa.isCoreAA)
return aa.coreAA.rtInterface.getY(&aa.coreAA.impl, pkey);

// get hash and bucket for key
immutable hash = calcHash(pkey, ti.key);
Expand Down Expand Up @@ -434,7 +478,11 @@ extern (C) inout(void)* _aaGetRvalueX(inout AA aa, in TypeInfo keyti, in size_t
*/
extern (C) inout(void)* _aaInX(inout AA aa, in TypeInfo keyti, in void* pkey)
{
if (aa.empty)
if (aa.impl is null)
return null;
else if (aa.isCoreAA)
return aa.coreAA.rtInterface.inX(aa.coreAA.impl, pkey);
else if (aa.empty)
return null;

immutable hash = calcHash(pkey, keyti);
Expand All @@ -446,7 +494,11 @@ extern (C) inout(void)* _aaInX(inout AA aa, in TypeInfo keyti, in void* pkey)
/// Delete entry in AA, return true if it was present
extern (C) bool _aaDelX(AA aa, in TypeInfo keyti, in void* pkey)
{
if (aa.empty)
if (aa.impl is null)
return false;
else if (aa.isCoreAA)
return aa.coreAA.rtInterface.delX(aa.coreAA.impl, pkey);
else if (aa.empty)
return false;

immutable hash = calcHash(pkey, keyti);
Expand Down
6 changes: 6 additions & 0 deletions win32.mak
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ doc: $(DOCS)
$(DOCDIR)\object.html : src\object.d
$(DMD) $(DDOCFLAGS) -Df$@ $(DOCFMT) $**

$(DOCDIR)\core_aa.html : src\core\aa.d
$(DMD) $(DDOCFLAGS) -Df$@ $(DOCFMT) $**

$(DOCDIR)\core_atomic.html : src\core\atomic.d
$(DMD) $(DDOCFLAGS) -Df$@ $(DOCFMT) $**

Expand Down Expand Up @@ -218,6 +221,9 @@ $(IMPDIR)\object.d : src\object.d
copy $** $@
if exist $(IMPDIR)\object.di del $(IMPDIR)\object.di

$(IMPDIR)\core\aa.d : src\core\aa.d
copy $** $@

$(IMPDIR)\core\atomic.d : src\core\atomic.d
copy $** $@

Expand Down
6 changes: 6 additions & 0 deletions win64.mak
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ doc: $(DOCS)
$(DOCDIR)\object.html : src\object.d
$(DMD) $(DDOCFLAGS) -Df$@ $(DOCFMT) $**

$(DOCDIR)\core_aa.html : src\core\aa.d
$(DMD) $(DDOCFLAGS) -Df$@ $(DOCFMT) $**

$(DOCDIR)\core_atomic.html : src\core\atomic.d
$(DMD) $(DDOCFLAGS) -Df$@ $(DOCFMT) $**

Expand Down Expand Up @@ -226,6 +229,9 @@ $(IMPDIR)\object.d : src\object.d
copy $** $@
if exist $(IMPDIR)\object.di del $(IMPDIR)\object.di

$(IMPDIR)\core\aa.d : src\core\aa.d
copy $** $@

$(IMPDIR)\core\atomic.d : src\core\atomic.d
copy $** $@

Expand Down