Skip to content

Commit 69cfd8e

Browse files
committedMar 11, 2024··
implement faster HashMap and test spec
1 parent 1f5e64b commit 69cfd8e

File tree

3 files changed

+129
-39
lines changed

3 files changed

+129
-39
lines changed
 
+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import HashMap from "../src/antlr4/misc/HashMap.js";
2+
import HashCode from "../src/antlr4/misc/HashCode.js";
3+
4+
class Thing {
5+
6+
value1 = Math.random();
7+
value2 = Math.random();
8+
9+
hashCode() {
10+
return HashCode.hashStuff(this.value1);
11+
}
12+
13+
equals(other) {
14+
return other instanceof Thing
15+
&& other.value1 === this.value1
16+
&& other.value2 === this.value2;
17+
}
18+
}
19+
20+
describe('test HashMap', () => {
21+
22+
it("sets a thing", () => {
23+
const t1 = new Thing();
24+
const map = new HashMap();
25+
map.set("abc", t1);
26+
expect(map.containsKey("abc")).toBeTrue();
27+
expect(map.containsKey("def")).toBeFalse();
28+
expect(map.length).toEqual(1);
29+
})
30+
31+
it("gets a thing", () => {
32+
const t1 = new Thing();
33+
const map = new HashMap();
34+
map.set("abc", t1);
35+
const t2 = map.get("abc");
36+
expect(t2).toEqual(t1);
37+
})
38+
39+
it("replaces a thing", () => {
40+
const t1 = new Thing();
41+
const t2 = new Thing();
42+
const map = new HashMap();
43+
map.set("abc", t1);
44+
map.set("abc", t2);
45+
const t3 = map.get("abc");
46+
expect(t3).toEqual(t2);
47+
})
48+
49+
it("returns correct length", () => {
50+
const t1 = new Thing();
51+
const t2 = new Thing();
52+
const map = new HashMap();
53+
expect(map.length).toEqual(0);
54+
map.set("abc", t1);
55+
expect(map.length).toEqual(1);
56+
map.set("def", t2);
57+
expect(map.length).toEqual(2);
58+
})
59+
60+
});

‎runtime/JavaScript/src/antlr4/misc/HashMap.js

+68-38
Original file line numberDiff line numberDiff line change
@@ -5,80 +5,110 @@
55
import standardEqualsFunction from "../utils/standardEqualsFunction.js";
66
import standardHashCodeFunction from "../utils/standardHashCodeFunction.js";
77

8-
const HASH_KEY_PREFIX = "h-";
8+
const DEFAULT_LOAD_FACTOR = 0.75;
9+
const INITIAL_CAPACITY = 16
910

1011
export default class HashMap {
1112

1213
constructor(hashFunction, equalsFunction) {
13-
this.data = {};
14+
this.buckets = new Array(INITIAL_CAPACITY);
15+
this.threshold = Math.floor(INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
16+
this.itemCount = 0;
1417
this.hashFunction = hashFunction || standardHashCodeFunction;
1518
this.equalsFunction = equalsFunction || standardEqualsFunction;
1619
}
1720

1821
set(key, value) {
19-
const hashKey = HASH_KEY_PREFIX + this.hashFunction(key);
20-
if (hashKey in this.data) {
21-
const entries = this.data[hashKey];
22-
for (let i = 0; i < entries.length; i++) {
23-
const entry = entries[i];
24-
if (this.equalsFunction(key, entry.key)) {
25-
const oldValue = entry.value;
26-
entry.value = value;
27-
return oldValue;
28-
}
29-
}
30-
entries.push({key:key, value:value});
22+
this._expand();
23+
const slot = this._getSlot(key);
24+
let bucket = this.buckets[slot];
25+
if (!bucket) {
26+
bucket = [[key, value]];
27+
this.buckets[slot] = bucket;
28+
this.itemCount++;
3129
return value;
30+
}
31+
const existing = bucket.find(pair => this.equalsFunction(pair[0], key), this);
32+
if(existing) {
33+
const result = existing[1];
34+
existing[1] = value;
35+
return result;
3236
} else {
33-
this.data[hashKey] = [{key:key, value:value}];
37+
bucket.push([key, value]);
38+
this.itemCount++;
3439
return value;
3540
}
3641
}
3742

3843
containsKey(key) {
39-
const hashKey = HASH_KEY_PREFIX + this.hashFunction(key);
40-
if(hashKey in this.data) {
41-
const entries = this.data[hashKey];
42-
for (let i = 0; i < entries.length; i++) {
43-
const entry = entries[i];
44-
if (this.equalsFunction(key, entry.key))
45-
return true;
46-
}
44+
const bucket = this._getBucket(key);
45+
if(!bucket) {
46+
return false;
4747
}
48-
return false;
48+
const existing = bucket.find(pair => this.equalsFunction(pair[0], key), this);
49+
return !!existing;
4950
}
5051

5152
get(key) {
52-
const hashKey = HASH_KEY_PREFIX + this.hashFunction(key);
53-
if(hashKey in this.data) {
54-
const entries = this.data[hashKey];
55-
for (let i = 0; i < entries.length; i++) {
56-
const entry = entries[i];
57-
if (this.equalsFunction(key, entry.key))
58-
return entry.value;
59-
}
53+
const bucket = this._getBucket(key);
54+
if(!bucket) {
55+
return null;
6056
}
61-
return null;
57+
const existing = bucket.find(pair => this.equalsFunction(pair[0], key), this);
58+
return existing ? existing[1] : null;
6259
}
6360

6461
entries() {
65-
return Object.keys(this.data).filter(key => key.startsWith(HASH_KEY_PREFIX)).flatMap(key => this.data[key], this);
62+
return this.buckets.filter(b => b != null).flat(1);
6663
}
6764

6865
getKeys() {
69-
return this.entries().map(e => e.key);
66+
return this.entries().map(pair => pair[0]);
7067
}
7168

7269
getValues() {
73-
return this.entries().map(e => e.value);
70+
return this.entries().map(pair => pair[1]);
7471
}
7572

7673
toString() {
77-
const ss = this.entries().map(e => '{' + e.key + ':' + e.value + '}');
74+
const ss = this.entries().map(e => '{' + e[0] + ':' + e[1] + '}');
7875
return '[' + ss.join(", ") + ']';
7976
}
8077

8178
get length() {
82-
return Object.keys(this.data).filter(key => key.startsWith(HASH_KEY_PREFIX)).map(key => this.data[key].length, this).reduce((accum, item) => accum + item, 0);
79+
return this.itemCount;
80+
}
81+
82+
_getSlot(key) {
83+
const hash = this.hashFunction(key);
84+
return hash & this.buckets.length - 1;
85+
}
86+
_getBucket(key) {
87+
return this.buckets[this._getSlot(key)];
8388
}
89+
90+
_expand() {
91+
if (this.itemCount <= this.threshold) {
92+
return;
93+
}
94+
const old_buckets = this.buckets;
95+
const newCapacity = this.buckets.length * 2;
96+
this.buckets = new Array(newCapacity);
97+
this.threshold = Math.floor(newCapacity * DEFAULT_LOAD_FACTOR);
98+
for (const bucket of old_buckets) {
99+
if (!bucket) {
100+
continue;
101+
}
102+
for (const pair of bucket) {
103+
const slot = this._getSlot(pair[0]);
104+
let newBucket = this.buckets[slot];
105+
if (!newBucket) {
106+
newBucket = [];
107+
this.buckets[slot] = newBucket;
108+
}
109+
newBucket.push(pair);
110+
}
111+
}
112+
}
113+
84114
}

‎runtime/JavaScript/src/antlr4/utils/standardEqualsFunction.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
* can be found in the LICENSE.txt file in the project root.
44
*/
55
export default function standardEqualsFunction(a, b) {
6-
return a ? a.equals(b) : a===b;
6+
return a && a.equals ? a.equals(b) : a===b;
77
}

0 commit comments

Comments
 (0)
Please sign in to comment.