From 1f5e64b642440f7a10c58d509efdd8fec765b639 Mon Sep 17 00:00:00 2001 From: Eric Vergnaud Date: Fri, 8 Mar 2024 20:24:41 +0100 Subject: [PATCH] improve HashSet performance Signed-off-by: Eric Vergnaud --- .../JavaScript/src/antlr4/atn/ATNConfigSet.js | 2 +- .../src/antlr4/atn/ParserATNSimulator.js | 4 +- runtime/JavaScript/src/antlr4/misc/HashSet.js | 105 +++++++++++++----- 3 files changed, 80 insertions(+), 31 deletions(-) diff --git a/runtime/JavaScript/src/antlr4/atn/ATNConfigSet.js b/runtime/JavaScript/src/antlr4/atn/ATNConfigSet.js index 0265d56bc5..9018ea1412 100644 --- a/runtime/JavaScript/src/antlr4/atn/ATNConfigSet.js +++ b/runtime/JavaScript/src/antlr4/atn/ATNConfigSet.js @@ -101,7 +101,7 @@ export default class ATNConfigSet { if (config.reachesIntoOuterContext > 0) { this.dipsIntoOuterContext = true; } - const existing = this.configLookup.add(config); + const existing = this.configLookup.getOrAdd(config); if (existing === config) { this.cachedHashCode = -1; this.configs.push(config); // track order here diff --git a/runtime/JavaScript/src/antlr4/atn/ParserATNSimulator.js b/runtime/JavaScript/src/antlr4/atn/ParserATNSimulator.js index d2153bf9f4..95b2096400 100644 --- a/runtime/JavaScript/src/antlr4/atn/ParserATNSimulator.js +++ b/runtime/JavaScript/src/antlr4/atn/ParserATNSimulator.js @@ -1287,7 +1287,7 @@ export default class ParserATNSimulator extends ATNSimulator { } c.reachesIntoOuterContext += 1; - if (closureBusy.add(c)!==c) { + if (closureBusy.getOrAdd(c)!==c) { // avoid infinite recursion for right-recursive rules continue; } @@ -1297,7 +1297,7 @@ export default class ParserATNSimulator extends ATNSimulator { console.log("dips into outer ctx: " + c); } } else { - if (!t.isEpsilon && closureBusy.add(c)!==c){ + if (!t.isEpsilon && closureBusy.getOrAdd(c)!==c){ // avoid infinite recursion for EOF* and EOF+ continue; } diff --git a/runtime/JavaScript/src/antlr4/misc/HashSet.js b/runtime/JavaScript/src/antlr4/misc/HashSet.js index 7fe7fc2407..11ca679650 100644 --- a/runtime/JavaScript/src/antlr4/misc/HashSet.js +++ b/runtime/JavaScript/src/antlr4/misc/HashSet.js @@ -6,52 +6,68 @@ import standardHashCodeFunction from "../utils/standardHashCodeFunction.js"; import standardEqualsFunction from "../utils/standardEqualsFunction.js"; import arrayToString from "../utils/arrayToString.js"; -const HASH_KEY_PREFIX = "h-"; +const DEFAULT_LOAD_FACTOR = 0.75; +const INITIAL_CAPACITY = 16 export default class HashSet { 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; } - add(value) { - const key = HASH_KEY_PREFIX + this.hashFunction(value); - if (key in this.data) { - const values = this.data[key]; - for (let i = 0; i < values.length; i++) { - if (this.equalsFunction(value, values[i])) { - return values[i]; - } - } - values.push(value); - return value; - } else { - this.data[key] = [value]; + get(value) { + if(value == null) { return value; } + const bucket = this._getBucket(value) + if (!bucket) { + return null; + } + for (const e of bucket) { + if (this.equalsFunction(e, value)) { + return e; + } + } + return null; } - has(value) { - return this.get(value) != null; + add(value) { + const existing = this.getOrAdd(value); + return existing === value; } - get(value) { - const key = HASH_KEY_PREFIX + this.hashFunction(value); - if (key in this.data) { - const values = this.data[key]; - for (let i = 0; i < values.length; i++) { - if (this.equalsFunction(value, values[i])) { - return values[i]; - } + getOrAdd(value) { + this._expand(); + const slot = this._getSlot(value); + let bucket = this.buckets[slot]; + if (!bucket) { + bucket = [value]; + this.buckets[slot] = bucket; + this.itemCount++; + return value; + } + for (const existing of bucket) { + if (this.equalsFunction(existing, value)) { + return existing; } } - return null; + bucket.push(value); + this.itemCount++; + return value; + + } + + has(value) { + return this.get(value) != null; } + values() { - 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); } toString() { @@ -59,6 +75,39 @@ export default class HashSet { } 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(value) { + const hash = this.hashFunction(value); + return hash & this.buckets.length - 1; + } + _getBucket(value) { + return this.buckets[this._getSlot(value)]; + } + + _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 o of bucket) { + const slot = this._getSlot(o); + let newBucket = this.buckets[slot]; + if (!newBucket) { + newBucket = []; + this.buckets[slot] = newBucket; + } + newBucket.push(o); + } + } + } }