Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement faster HashMap #4551

Merged
merged 1 commit into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions runtime/JavaScript/spec/HashMapSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import HashMap from "../src/antlr4/misc/HashMap.js";
import HashCode from "../src/antlr4/misc/HashCode.js";

class Thing {

value1 = Math.random();
value2 = Math.random();

hashCode() {
return HashCode.hashStuff(this.value1);
}

equals(other) {
return other instanceof Thing
&& other.value1 === this.value1
&& other.value2 === this.value2;
}
}

describe('test HashMap', () => {

it("sets a thing", () => {
const t1 = new Thing();
const map = new HashMap();
map.set("abc", t1);
expect(map.containsKey("abc")).toBeTrue();
expect(map.containsKey("def")).toBeFalse();
expect(map.length).toEqual(1);
})

it("gets a thing", () => {
const t1 = new Thing();
const map = new HashMap();
map.set("abc", t1);
const t2 = map.get("abc");
expect(t2).toEqual(t1);
})

it("replaces a thing", () => {
const t1 = new Thing();
const t2 = new Thing();
const map = new HashMap();
map.set("abc", t1);
map.set("abc", t2);
const t3 = map.get("abc");
expect(t3).toEqual(t2);
})

it("returns correct length", () => {
const t1 = new Thing();
const t2 = new Thing();
const map = new HashMap();
expect(map.length).toEqual(0);
map.set("abc", t1);
expect(map.length).toEqual(1);
map.set("def", t2);
expect(map.length).toEqual(2);
})

});
106 changes: 68 additions & 38 deletions runtime/JavaScript/src/antlr4/misc/HashMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,80 +5,110 @@
import standardEqualsFunction from "../utils/standardEqualsFunction.js";
import standardHashCodeFunction from "../utils/standardHashCodeFunction.js";

const HASH_KEY_PREFIX = "h-";
const DEFAULT_LOAD_FACTOR = 0.75;
const INITIAL_CAPACITY = 16

export default class HashMap {

constructor(hashFunction, equalsFunction) {
this.data = {};
this.buckets = new Array(INITIAL_CAPACITY);
this.threshold = Math.floor(INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
this.itemCount = 0;
this.hashFunction = hashFunction || standardHashCodeFunction;
this.equalsFunction = equalsFunction || standardEqualsFunction;
}

set(key, value) {
const hashKey = HASH_KEY_PREFIX + this.hashFunction(key);
if (hashKey in this.data) {
const entries = this.data[hashKey];
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
if (this.equalsFunction(key, entry.key)) {
const oldValue = entry.value;
entry.value = value;
return oldValue;
}
}
entries.push({key:key, value:value});
this._expand();
const slot = this._getSlot(key);
let bucket = this.buckets[slot];
if (!bucket) {
bucket = [[key, value]];
this.buckets[slot] = bucket;
this.itemCount++;
return value;
}
const existing = bucket.find(pair => this.equalsFunction(pair[0], key), this);
if(existing) {
const result = existing[1];
existing[1] = value;
return result;
} else {
this.data[hashKey] = [{key:key, value:value}];
bucket.push([key, value]);
this.itemCount++;
return value;
}
}

containsKey(key) {
const hashKey = HASH_KEY_PREFIX + this.hashFunction(key);
if(hashKey in this.data) {
const entries = this.data[hashKey];
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
if (this.equalsFunction(key, entry.key))
return true;
}
const bucket = this._getBucket(key);
if(!bucket) {
return false;
}
return false;
const existing = bucket.find(pair => this.equalsFunction(pair[0], key), this);
return !!existing;
}

get(key) {
const hashKey = HASH_KEY_PREFIX + this.hashFunction(key);
if(hashKey in this.data) {
const entries = this.data[hashKey];
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
if (this.equalsFunction(key, entry.key))
return entry.value;
}
const bucket = this._getBucket(key);
if(!bucket) {
return null;
}
return null;
const existing = bucket.find(pair => this.equalsFunction(pair[0], key), this);
return existing ? existing[1] : null;
}

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

getKeys() {
return this.entries().map(e => e.key);
return this.entries().map(pair => pair[0]);
}

getValues() {
return this.entries().map(e => e.value);
return this.entries().map(pair => pair[1]);
}

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

get length() {
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);
return this.itemCount;
}

_getSlot(key) {
const hash = this.hashFunction(key);
return hash & this.buckets.length - 1;
}
_getBucket(key) {
return this.buckets[this._getSlot(key)];
}

_expand() {
if (this.itemCount <= this.threshold) {
return;
}
const old_buckets = this.buckets;
const newCapacity = this.buckets.length * 2;
this.buckets = new Array(newCapacity);
this.threshold = Math.floor(newCapacity * DEFAULT_LOAD_FACTOR);
for (const bucket of old_buckets) {
if (!bucket) {
continue;
}
for (const pair of bucket) {
const slot = this._getSlot(pair[0]);
let newBucket = this.buckets[slot];
if (!newBucket) {
newBucket = [];
this.buckets[slot] = newBucket;
}
newBucket.push(pair);
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
* can be found in the LICENSE.txt file in the project root.
*/
export default function standardEqualsFunction(a, b) {
return a ? a.equals(b) : a===b;
return a && a.equals ? a.equals(b) : a===b;
}
Loading