Copyright (C) 1992, Digital Equipment Corporation
All rights reserved.
See the file COPYRIGHT for a full description.
Created by Paul McJones on 28 May 92, based on LTbl.mg of 5 May 92
by Jorge Stolfi, based on Set.mg of 11 Feb 92 by Eric Muller.
This version is very similar to Table.mod of 16 Jan 90 by John Ellis.
Last modified on Thu Sep 22 11:06:06 PDT 1994 by heydon
modified on Thu Mar 3 08:42:56 PST 1994 by kalsow
modified on Wed Jun 23 15:30:15 PDT 1993 by mcjones
modified on Mon May 18 21:01:59 PDT 1992 by stolfi
modified on Tue Feb 11 20:48:05 PST 1992 by muller
GENERIC MODULE Table(Key, Value);
where Key.Hash(k: Key.T): Word.T,
and Key.Equal(k1, k2: Key.T): BOOLEAN.
IMPORT Word;
TYPE
Public = T OBJECT METHODS
init(sizeHint: CARDINAL := 0): Default;
keyEqual(READONLY k1, k2: Key.T): BOOLEAN;
keyHash(READONLY k: Key.T): Word.T
END;
REVEAL
Default = Public BRANDED DefaultBrand OBJECT
minLogBuckets: CARDINAL; (* minimum value for Log_2(initial size) *)
buckets: Buckets;
logBuckets: CARDINAL; (* CEILING(Log2(NUMBER(buckets^))) *)
maxEntries: CARDINAL; (* maximum number of entries *)
minEntries: CARDINAL; (* minimum number of entries *)
numEntries: CARDINAL (* current num of entries in table *)
OVERRIDES
get := Get;
put := Put;
delete := Delete;
size := Size;
iterate := Iterate;
init := Init;
keyEqual := KeyEqual;
keyHash := KeyHash
END;
TYPE
Buckets = REF ARRAY OF EntryList;
TYPE
EntryList = REF RECORD
key: Key.T;
value: Value.T;
tail: EntryList
END;
VAR (*CONST*)
Multiplier: INTEGER;
CONST
MaxLogBuckets = BITSIZE(Word.T) - 2;
MaxBuckets = Word.Shift(1, MaxLogBuckets);
MinLogBuckets = 4;
MinBuckets = Word.Shift(1, MinLogBuckets);
CONST
(* Thresholds for rehashing the table: *)
(* to avoid crazy oscillations, we must have MaxDensity > 2*MinDensity; *)
(* to avoid excessive probes, we must try to keep MaxDensity low. *)
MaxDensity = 0.75; (* max numEntries/NUMBER(buckets) *)
MinDensity = 0.20; (* min numEntries/NUMBER(buckets) *)
IdealDensity = 0.50;
TYPE DefaultIterator = Iterator BRANDED OBJECT
tbl : Default := NIL;
this : EntryList := NIL; (* next entry to visit if non-NIL *)
bucket : CARDINAL := 0; (* next bucket if < NUMBER(tbl.buckets^) *)
done : BOOLEAN := FALSE; (* TRUE if next() has returned FALSE *)
OVERRIDES
init := InitIterator;
next := Next
END;
*****************
Default methods
*****************
PROCEDURE Init(tbl: Default; n: CARDINAL := 0): Default =
BEGIN
WITH idealBuckets = CEILING(MIN(FLOAT(n) / IdealDensity,
FLOAT(MaxBuckets))),
minBuckets = MAX(MinBuckets, idealBuckets) DO
tbl.minLogBuckets := Log_2(minBuckets)
END;
NewBuckets(tbl, tbl.minLogBuckets);
tbl.numEntries := 0;
RETURN tbl
END Init;
PROCEDURE Get(tbl: Default; READONLY key: Key.T; VAR val: Value.T): BOOLEAN =
VAR this: EntryList;
BEGIN
this := tbl.buckets[
Word.RightShift(Word.Times(tbl.keyHash(key), Multiplier),
Word.Size - tbl.logBuckets)];
WHILE this # NIL AND NOT tbl.keyEqual(key, this.key) DO
this := this.tail
END;
IF this # NIL THEN val := this.value; RETURN TRUE ELSE RETURN FALSE END
END Get;
PROCEDURE Put(tbl: Default; READONLY key: Key.T; READONLY val: Value.T)
: BOOLEAN =
VAR this: EntryList;
BEGIN
WITH first = tbl.buckets[Word.RightShift(
Word.Times(tbl.keyHash(key), Multiplier),
Word.Size - tbl.logBuckets)] DO
this := first;
WHILE this # NIL AND NOT tbl.keyEqual(key, this.key) DO
this := this.tail
END;
IF this # NIL THEN
this.value := val;
RETURN TRUE
ELSE
first :=
NEW(EntryList, key := key, value := val, tail := first);
INC(tbl.numEntries);
IF tbl.logBuckets < MaxLogBuckets
AND tbl.numEntries > tbl.maxEntries THEN
Rehash(tbl, tbl.logBuckets + 1) (* too crowded *)
END;
RETURN FALSE
END
END
END Put;
PROCEDURE Delete(tbl: Default; READONLY key: Key.T; VAR val: Value.T)
: BOOLEAN =
VAR this, prev: EntryList;
BEGIN
WITH first = tbl.buckets[Word.RightShift(
Word.Times(tbl.keyHash(key), Multiplier),
Word.Size - tbl.logBuckets)] DO
this := first;
prev := NIL;
WHILE this # NIL AND NOT tbl.keyEqual(key, this.key) DO
prev := this;
this := this.tail
END;
IF this # NIL THEN
val := this.value;
IF prev = NIL THEN
first := this.tail
ELSE
prev.tail := this.tail
END;
DEC(tbl.numEntries);
IF tbl.logBuckets > tbl.minLogBuckets
AND tbl.numEntries < tbl.minEntries THEN
Rehash(tbl, tbl.logBuckets - 1) (* too sparse *)
END;
RETURN TRUE
ELSE
RETURN FALSE
END
END
END Delete;
PROCEDURE Size(tbl: Default): CARDINAL =
BEGIN
RETURN tbl.numEntries
END Size;
PROCEDURE Iterate(tbl: Default): Iterator =
BEGIN
RETURN NEW(DefaultIterator, tbl := tbl).init();
END Iterate;
PROCEDURE KeyHash(<*UNUSED*> tbl: Default; READONLY k: Key.T): Word.T =
BEGIN
RETURN Key.Hash(k)
END KeyHash;
PROCEDURE KeyEqual(<*UNUSED*> tbl: Default; READONLY k1, k2: Key.T): BOOLEAN =
BEGIN
RETURN Key.Equal(k1, k2)
END KeyEqual;
*********************
Internal procedures
*********************
PROCEDURE Log_2(x: CARDINAL): CARDINAL =
(* Return CEILING(LOG_2(x)) *)
VAR
log: CARDINAL := 0;
n: CARDINAL := 1;
BEGIN
<* ASSERT x # 0 *>
WHILE (log < MaxLogBuckets) AND (x > n) DO INC(log); n := n + n END;
RETURN log
END Log_2;
PROCEDURE NewBuckets(tbl: Default; logBuckets: CARDINAL) =
(* Allocate "2^logBuckets" buckets. *)
BEGIN
WITH numBuckets = Word.LeftShift(1, logBuckets) DO
tbl.buckets := NEW(Buckets, numBuckets);
WITH b = tbl.buckets^ DO
FOR i := FIRST(b) TO LAST(b) DO b[i] := NIL END
END;
tbl.logBuckets := logBuckets;
tbl.maxEntries := ROUND(MaxDensity * FLOAT(numBuckets));
tbl.minEntries := ROUND(MinDensity * FLOAT(numBuckets))
END
END NewBuckets;
PROCEDURE Rehash(tbl: Default; logBuckets: CARDINAL) =
(* Reallocate "2^logBuckets" buckets, and rehash the entries into the new
table. *)
BEGIN
<* ASSERT logBuckets <= MaxLogBuckets *>
<* ASSERT logBuckets >= tbl.minLogBuckets *>
WITH ob = tbl.buckets^ DO
NewBuckets(tbl, logBuckets);
WITH nb = tbl.buckets^ DO
FOR i := FIRST(ob) TO LAST(ob) DO
WITH obi = ob[i] DO
VAR
this: EntryList := obi;
tail: EntryList;
BEGIN
obi := NIL; (* ease collector's life *)
WHILE this # NIL DO
WITH nbh = nb[Word.RightShift(
Word.Times(
tbl.keyHash(this.key), Multiplier),
Word.Size - logBuckets)] DO
tail := this.tail;
this.tail := nbh;
nbh := this;
this := tail
END
END
END
END
END
END
END
END Rehash;
******************
Iterator methods
******************
PROCEDURE InitIterator (i: DefaultIterator): Iterator =
BEGIN
i.this := NIL;
i.bucket := 0;
i.done := FALSE;
RETURN i;
END InitIterator;
PROCEDURE Next(i: DefaultIterator; VAR key: Key.T; VAR val: Value.T): BOOLEAN =
BEGIN
BEGIN
WHILE i.this = NIL AND i.bucket < NUMBER(i.tbl.buckets^) DO
i.this := i.tbl.buckets^[i.bucket];
INC(i.bucket)
END;
IF i.this # NIL THEN
key := i.this.key;
val := i.this.value;
i.this := i.this.tail;
RETURN TRUE
ELSE
<* ASSERT NOT i.done *>
i.done := TRUE;
RETURN FALSE
END
END
END Next;
***********
PROCEDURE Dump (tbl: Default; key_dump: PROCEDURE (key: Key.T)) =
VAR e: EntryList; hash, slot: INTEGER;
BEGIN
IO.Put (DUMP min_log=
); IO.PutInt (tbl.minLogBuckets);
IO.Put ( log=
); IO.PutInt (tbl.logBuckets);
IO.Put ( max=
); IO.PutInt (tbl.maxEntries);
IO.Put ( min=
); IO.PutInt (tbl.minEntries);
IO.Put ( num=
); IO.PutInt (tbl.numEntries);
IO.Put ( mult=
); IO.PutInt (Multiplier);
IO.Put (\r\n
);
FOR i := 0 TO LAST (tbl.buckets^) DO
e := tbl.buckets[i];
IF e # NIL THEN
IO.Put ( bucket
);
IO.PutInt (i);
WHILE (e # NIL) DO
IO.Put ( (
);
key_dump (e.key);
IO.Put (
);
hash := tbl.keyHash (e.key);
IO.PutInt (hash);
slot := Word.Times (hash, Multiplier);
IO.Put (
);
IO.PutInt (slot);
slot := Word.RightShift (Word.Times (hash, Multiplier),
Word.Size - tbl.logBuckets);
IF (slot # i) THEN
IO.Put ( ***
);
IO.PutInt (slot);
END;
IO.Put ()
);
e := e.tail;
END;
IO.Put (\r\n
);
END;
END;
END Dump;
*************
BEGIN
(* The multiplier == 2^BITSIZE(Word.T) / phi *)
IF BITSIZE (Word.T) = 32 THEN
Multiplier := 16_9e3779b9;
ELSIF BITSIZE (Word.T) = 64 THEN
Multiplier := Word.Plus (Word.Shift (16_9e3779b9, 32), 16_7f4a7c15);
ELSE
<*ASSERT FALSE*>
END;
END Table.