Associative Arrays need cleanout method or property to help garbage collection

Michael Rynn michaelrynn at optusnet.com.au
Tue Mar 16 20:07:57 PDT 2010


On Tue, 16 Mar 2010 16:12:25 +0000, Moritz Warning wrote:

> On Tue, 16 Mar 2010 04:41:55 +0000, Michael Rynn wrote:
> 
>> The use of built in Associative Array can have deleterious effects on
>> Garbage Collection.
>  :(
> 
> 
> Fwiw, here is the original Python Dictionary implementation:
> http://svn.python.org/projects/python/trunk/Objects/dictobject.c This is
> also a nice read:
> http://svn.python.org/projects/python/trunk/Objects/dictnotes.txt
> 
> As for difference to the D impl.:
> The decision logic for table resizing is a bit different (Python uses a
> load factor of 2/3 and table doubling for more as 50K entries, *4 else).
> Python also wraps everything in Objects and attaches the hash when
> computed.
> 

I am beginning to get a headache playing around with this. There must be 
better things to do.  I have refined the test program a bit more and 
added some missing bits to aaht by calling the implementation class.

The following summary statistics were obtained.
The no-cleanup averages represent more or less the value of the middle 
run number, due to slowdown, except for aapy.   Even so, doing a cleanup 
improved the aapy results,  for which insertion times are very 
impressive. I attribute this to not needing to allocate memory for each 
insertion. Lookups for aapy are on average about 7-8% slower.

The bits I find interesting are:-

When doing cleanups, the aaht library version of the builtin AA is about 
33% faster on insertions than the builtin AA, although a few percent 
slower on lookups. Using a Tangy heap Container for node allocation 
increases performance to 60% faster than builtin, and brings the lookups 
to par. It also allows a super fast cleanup.

Since the builtin has no builtin cleanup to call, I used the following 
template to inefficiently do the task. 

The aaht version with a custom clear did the task 1.5 to 2.5 times faster 
(no TangyHeap), or really super fast (TangyHeap).



void
clear(K,V)(ref V[K] aa)
{
    K[] keyset = aa.keys;
    foreach(ks; keyset)
    {
        aa.remove(ks);
    }
}

A call to aa.clear then cleans up in reasonable time.

summary average 10 runs of size 500_000

no-cleanup uint[uint]
		insert  lookup
builtin(K,V)	4.395	0.0388
aaht(K,V)	4.5906	0.0426
aapy(K,V)	0.139	0.0423

cleaned-up uint[uint]
		insert  lookup cleanup
builtin(K,V)	0.4601	0.0376	  0.1252
aaht(K,V)	0.3021	0.0399	  0.081
aapy(K,V)	0.0957	0.0438	  0.0002


no-cleanup uint[string]
builtin(K,V)	3.4727	0.1596
aaht(K,V)	3.6453	0.1646
aapy(K,V)	0.9753	0.1735

cleaned-up uint[string]
builtin(K,V)	0.668	0.1658	 0.2609
aaht(K,V)	0.4396	0.1621	 0.1001
aapy(K,V)	0.2286	0.1717	 0.0002


After recompile with aaht version=TangyHeap
uint[uint]
aaht(K,V)	0.188	0.0387	0.0004

uint[string]
aaht(K,V)	0.3391	0.1609	0.0001



I have no empirical or logical justifications for when to resize tables. 
I just copy the previous code and hope I got it right.

 There must have been some previous research on the number of times a 
lookup or insert needs to check another slot because of hash collision, 
causing slowdown. But wether the ratio is 3/5 (60%) 2/3 (66%) or 3/4(75%) 
has not yet made for me an observable effect in pydict.

> As for your modifications to the PyDict code: Why have you changed the
> length() implementation? As far as I remember, it's wrong to just return
> "used".

Formerly 'used' field was the number of active nodes in the table itself.
The relationship is mathematically seen in the size() property: 
---
length = used + is_dummy + is_unused;
---

where is_dummy and is_unused can be 1 or 0.

I have rewritten this an equivalent form
used_entries = length_ - is_dummy - is_unused.

I have now changed the 'used' to become length_, and added a comment to 
make it plain.

This length_ variable now includes the count of the usage of the 2 extra 
nodes for unused_hash(0) and dummy_hash(1).  The length_ count will get 
bumped up or down whenever these nodes are set or unset respectively, as 
if they were normal nodes.

'used_entries' is only calculated just before readjusting the table size.
This is a computationally a trivial no difference, only preferred because 
I thought that length_ will be accessed more than 'used_entries', giving 
it the possibility of no calculation property access.


I want to add that the problems of AA with garbage collection are not 
particular to the implementation of built-in AA. The number of nodes that 
builtin AA creates will increase GC scanning, but the AA slowdowns also 
occurs using the pyDict implementation with string keys.

Original PyDictD2 slowed down for both uint and string keys, unless I set 
NO_SCAN calloc.
Then I changed the implementation to use new Entry[] instead of a calloc. 
This helped the uint[uint] test, but not the uint[string] test.

For uint[string]  the pyDict still slows progressively after each run, 
not nearly as much as the for builtin AA. The only way I can prevent this 
is to forcefully clean up the AA after each use, for whatever 
implementation.

The testhash.d command line 3rd option for the test select shows this.

option 5 -  aapy uint[string] - no cleanup. -- slows down
option 15   aapy uint[string] - cleanup     -- no slow down.

ditto for aabuiltin -1,11(except that it always slows down, because no 
cleanup possible)  and aaht - 3,13.

For immutable(char)[] keys, uint values
The AA needs to be scanned because AA node holds a reference to the key. 
If the key is not reachable and referenced elsewhere, the key array will 
be deleted by the GC, and the AA key will then reference freed memory.

The testhash program is holding references to the original string keys in 
a dynamic array. Also tested a version that makes sure all the AA values 
are 0, to help the garbage collector, but it still slows down if no 
cleanup requested.  Another version flag calls the GC.collect before each 
run. Its as if bits in the string keys are being misinterpreted as 
pointers.

With the current D garbage collector, using any AA implementation, 
builtin or otherwise, requires the ability to enforce cleanup. I do not 
expect any harm from using enforced AA memory management.  

>From my reading of Hans Boehm garbage collector, it is possible to 
provide TypeInfo hints to it has to what to interpret as pointers, in a 
structure or array. It would be nice to be able to do this from TypeInfo 
in D. I do not know if it is done to any extent at all in D.  I suppose 
there is always both a development and a performance penalty for this 
sort of thing. There also appears to be a penalty for not doing it.

While the GC cannot be fixed real soon, or cannot be replaced with a more 
descriminating or enhanced with TypeInfo hints, then for many 
applications, D and its libraries interweave manual memory management 
with garbage collection, such as this little aa project.  Applications 
are able to delete and clear things using some degree safety 
encapsulation and stack scoping, and still let the garbage collector 
worry about what it can cope with.  

Mono dev is trying to wean off the Hans Boehm and use something more 
exact and generational. Given the failing of garbage collection for most 
AA implementations, all current D AA implementations need a manual 
cleanup method. Other alternatives include using being able to specify 
non-scanned memory implementation where the programmmer deems it safe. 

Have a cleanup method for the AA, makes memory management more flexible 
and accessible, and when used will nearly always improve overall 
performace.  Improve the AA or fix the GC. Which is easier?

The aaht version with the heap node allocation and fast cleanup seems to 
be a better AA than the built-in.  The aapy has by far the best insertion 
results.

But do not take this very restricted and rather extreme sampling of 
possible test parameters to be a reliable guide to relative performance 
when used in other applications with differing environments.
---
Michael Rynn.







More information about the Digitalmars-d mailing list