diff --git a/ChangeLog.md b/ChangeLog.md index 9a9eb8b9066..fe4aadfde7f 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -9,6 +9,10 @@ ### Compiler +### JS + +- Improve performance of maps and sets + #### Analysis & diagnostics - Combination of 'open' and 'override' is no longer a warning diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/StandardClassesTest.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/StandardClassesTest.java index 1ae7d6a7d34..b0760aa501a 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/StandardClassesTest.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/StandardClassesTest.java @@ -78,4 +78,8 @@ public final class StandardClassesTest extends SingleFileTranslationTest { public void ignore_testThrowableImpl() throws Exception { checkFooBoxIsOk(); } + + public void testMutableMapRemoveWithCollision() throws Exception { + checkFooBoxIsOk(); + } } diff --git a/js/js.translator/testData/maps.js b/js/js.translator/testData/maps.js index 6dfea9dfbd0..b1c4bcf41c0 100644 --- a/js/js.translator/testData/maps.js +++ b/js/js.translator/testData/maps.js @@ -272,241 +272,247 @@ * @param {(function(Key, Key): boolean)=} equalityFunctionParam * @template Key, Value */ - var Hashtable = function (hashingFunctionParam, equalityFunctionParam) { - var that = this; - var buckets = []; - var bucketsByHash = {}; + function Hashtable(hashingFunctionParam, equalityFunctionParam) { + this.buckets = []; + this.bucketsByHash = {}; - var hashingFunction = (typeof hashingFunctionParam == FUNCTION) ? hashingFunctionParam : hashObject; - var equalityFunction = (typeof equalityFunctionParam == FUNCTION) ? equalityFunctionParam : null; + this.hashingFunction = (typeof hashingFunctionParam == FUNCTION) ? hashingFunctionParam : hashObject; + this.equalityFunction = (typeof equalityFunctionParam == FUNCTION) ? equalityFunctionParam : null; + this._size = 0; + } - this.put_wn2jw4$ = function (key, value) { - var hash = hashingFunction(key), bucket, bucketEntry, oldValue = null; + Hashtable.prototype.put_wn2jw4$ = function (key, value) { + var hash = this.hashingFunction(key), bucket, bucketEntry, oldValue = null; - // Check if a bucket exists for the bucket key - bucket = getBucketForHash(bucketsByHash, hash); - if (bucket) { - // Check this bucket to see if it already contains this key - bucketEntry = bucket.getEntryForKey(key); - if (bucketEntry) { - // This bucket entry is the current mapping of key to value, so replace old value and we're done. - oldValue = bucketEntry[1]; - bucketEntry[1] = value; - } - else { - // The bucket does not contain an entry for this key, so add one - bucket.addEntry(key, value); - } + // Check if a bucket exists for the bucket key + bucket = getBucketForHash(this.bucketsByHash, hash); + if (bucket) { + // Check this bucket to see if it already contains this key + bucketEntry = bucket.getEntryForKey(key); + if (bucketEntry) { + // This bucket entry is the current mapping of key to value, so replace old value and we're done. + oldValue = bucketEntry[1]; + bucketEntry[1] = value; } else { - // No bucket exists for the key, so create one and put our key/value mapping in - bucket = new Bucket(hash, key, value, equalityFunction); - buckets[buckets.length] = bucket; - bucketsByHash[hash] = bucket; + // The bucket does not contain an entry for this key, so add one + bucket.addEntry(key, value); + this._size++; } - return oldValue; - }; + } + else { + // No bucket exists for the key, so create one and put our key/value mapping in + bucket = new Bucket(hash, key, value, this.equalityFunction); + this.buckets[this.buckets.length] = bucket; + this.bucketsByHash[hash] = bucket; + this._size++; + } + return oldValue; + }; - this.get_za3rmp$ = function (key) { - var hash = hashingFunction(key); + Hashtable.prototype.get_za3rmp$ = function (key) { + var hash = this.hashingFunction(key); - // Check if a bucket exists for the bucket key - var bucket = getBucketForHash(bucketsByHash, hash); - if (bucket) { - // Check this bucket to see if it contains this key - var bucketEntry = bucket.getEntryForKey(key); - if (bucketEntry) { - // This bucket entry is the current mapping of key to value, so return the value. - return bucketEntry[1]; - } + // Check if a bucket exists for the bucket key + var bucket = getBucketForHash(this.bucketsByHash, hash); + if (bucket) { + // Check this bucket to see if it contains this key + var bucketEntry = bucket.getEntryForKey(key); + if (bucketEntry) { + // This bucket entry is the current mapping of key to value, so return the value. + return bucketEntry[1]; } - return null; - }; + } + return null; + }; - this.containsKey_za3rmp$ = function (key) { - var bucketKey = hashingFunction(key); + Hashtable.prototype.containsKey_za3rmp$ = function (key) { + var bucketKey = this.hashingFunction(key); - // Check if a bucket exists for the bucket key - var bucket = getBucketForHash(bucketsByHash, bucketKey); + // Check if a bucket exists for the bucket key + var bucket = getBucketForHash(this.bucketsByHash, bucketKey); - return bucket ? bucket.containsKey_za3rmp$(key) : false; - }; + return bucket ? bucket.containsKey_za3rmp$(key) : false; + }; - this.containsValue_za3rmp$ = function (value) { - var i = buckets.length; + Hashtable.prototype.containsValue_za3rmp$ = function (value) { + var i = this.buckets.length; + while (i--) { + if (this.buckets[i].containsValue_za3rmp$(value)) { + return true; + } + } + return false; + }; + + Hashtable.prototype.clear = function () { + this.buckets.length = 0; + this.bucketsByHash = {}; + this._size = 0; + }; + + Hashtable.prototype.isEmpty = function () { + return !this.buckets.length; + }; + + Hashtable.prototype._keys = function () { + var aggregated = [], i = this.buckets.length; + while (i--) { + this.buckets[i].keys(aggregated); + } + return aggregated; + }; + + Hashtable.prototype._values = function () { + var aggregated = [], i = this.buckets.length; + while (i--) { + this.buckets[i].values(aggregated); + } + return aggregated; + }; + + Hashtable.prototype._entries = function () { + var aggregated = [], i = this.buckets.length; + while (i--) { + this.buckets[i].getEntries(aggregated); + } + return aggregated; + }; + + Object.defineProperty(Hashtable.prototype, "values", { + get: function () { + var values = this._values(); + var i = values.length; + var result = new Kotlin.ArrayList(); while (i--) { - if (buckets[i].containsValue_za3rmp$(value)) { - return true; + result.add_za3rmp$(values[i]); + } + return result; + }, + configurable: true + }); + + Hashtable.prototype.remove_za3rmp$ = function (key) { + var hash = this.hashingFunction(key), bucketIndex, oldValue = null, result = null; + + // Check if a bucket exists for the bucket key + var bucket = getBucketForHash(this.bucketsByHash, hash); + + if (bucket) { + // Remove entry from this bucket for this key + result = bucket.removeEntryForKey(key); + if (result !== null) { + this._size--; + oldValue = result[1]; + + // Entry was removed, so check if bucket is empty + if (!bucket.entries.length) { + // Bucket is empty, so remove it from the bucket collections + bucketIndex = searchBuckets(this.buckets, hash); + arrayRemoveAt(this.buckets, bucketIndex); + delete this.bucketsByHash[hash]; } } - return false; - }; + } + return oldValue; + }; - this.clear = function () { - buckets.length = 0; - bucketsByHash = {}; - }; + Object.defineProperty(Hashtable.prototype, "size", { + get: function () { + return this._size; + } + }); - this.isEmpty = function () { - return !buckets.length; - }; + Hashtable.prototype.each = function (callback) { + var entries = this._entries(), i = entries.length, entry; + while (i--) { + entry = entries[i]; + callback(entry[0], entry[1]); + } + }; - var createBucketAggregator = function (bucketFuncName) { - return function () { - var aggregated = [], i = buckets.length; - while (i--) { - buckets[i][bucketFuncName](aggregated); - } - return aggregated; - }; - }; + Hashtable.prototype.putAll_r12sna$ = hashMapPutAll; - this._keys = createBucketAggregator("keys"); - this._values = createBucketAggregator("values"); - this._entries = createBucketAggregator("getEntries"); + Hashtable.prototype.clone = function () { + var clone = new Hashtable(this.hashingFunction, this.equalityFunction); + clone.putAll_r12sna$(this); + return clone; + }; - Object.defineProperty(this, "values", { - get: function () { - var values = this._values(); - var i = values.length; - var result = new Kotlin.ArrayList(); - while (i--) { - result.add_za3rmp$(values[i]); - } - return result; - }, - configurable: true - }); + Object.defineProperty(Hashtable.prototype, "keys", { + get: function () { + var res = new Kotlin.ComplexHashSet(); + var keys = this._keys(); + var i = keys.length; + while (i--) { + res.add_za3rmp$(keys[i]); + } + return res; + }, + configurable: true + }); + Object.defineProperty(Hashtable.prototype, "entries", { + get: function () { + var result = new Kotlin.ComplexHashSet(); + var entries = this._entries(); + var i = entries.length; + while (i--) { + var entry = entries[i]; + result.add_za3rmp$(new Entry(entry[0], entry[1])); + } - this.remove_za3rmp$ = function (key) { - var hash = hashingFunction(key), bucketIndex, oldValue = null, result = null; + return result; + }, + configurable: true + }); - // Check if a bucket exists for the bucket key - var bucket = getBucketForHash(bucketsByHash, hash); + Hashtable.prototype.hashCode = function() { + var h = 0; + var entries = this._entries(); + var i = entries.length; + while (i--) { + var entry = entries[i]; + h += mapEntryHashCode(entry[0], entry[1]); + } + return h; + }; - if (bucket) { - // Remove entry from this bucket for this key - result = bucket.removeEntryForKey(key); - if (result !== null) { - oldValue = result[1]; + Hashtable.prototype.equals_za3rmp$ = function(o) { + if (o == null || this.size !== o.size) return false; - // Entry was removed, so check if bucket is empty - if (!bucket.entries.length) { - // Bucket is empty, so remove it from the bucket collections - bucketIndex = searchBuckets(buckets, hash); - arrayRemoveAt(buckets, bucketIndex); - delete bucketsByHash[hash]; - } - } + var entries = this._entries(); + var i = entries.length; + while (i--) { + var entry = entries[i]; + var key = entry[0]; + var value = entry[1]; + if (value == null) { + if (!(o.get_za3rmp$(key) == null && o.contains_za3rmp$(key))) return false; } - return oldValue; - }; - - Object.defineProperty(this, "size", { - get: function () { - var total = 0, i = buckets.length; - while (i--) { - total += buckets[i].entries.length; - } - return total; + else { + if (!Kotlin.equals(value, o.get_za3rmp$(key))) return false; } - }); + } + return true; + }; - this.each = function (callback) { - var entries = that._entries(), i = entries.length, entry; - while (i--) { - entry = entries[i]; - callback(entry[0], entry[1]); - } - }; - - /** - * @param {Hashtable.} hashtable - */ - this.putAll_r12sna$ = hashMapPutAll; - - this.clone = function () { - var clone = new Hashtable(hashingFunctionParam, equalityFunctionParam); - clone.putAll_r12sna$(that); - return clone; - }; - - Object.defineProperty(this, "keys", { - get: function () { - var res = new Kotlin.ComplexHashSet(); - var keys = this._keys(); - var i = keys.length; - while (i--) { - res.add_za3rmp$(keys[i]); - } - return res; - }, - configurable: true - }); - - Object.defineProperty(this, "entries", { - get: function () { - var result = new Kotlin.ComplexHashSet(); - var entries = this._entries(); - var i = entries.length; - while (i--) { - var entry = entries[i]; - result.add_za3rmp$(new Entry(entry[0], entry[1])); - } - - return result; - }, - configurable: true - }); - - this.hashCode = function() { - var h = 0; - var entries = this._entries(); - var i = entries.length; - while (i--) { - var entry = entries[i]; - h += mapEntryHashCode(entry[0], entry[1]); - } - return h; - }; - - this.equals_za3rmp$ = function(o) { - if (o == null || this.size !== o.size) return false; - - var entries = this._entries(); - var i = entries.length; - while (i--) { - var entry = entries[i]; - var key = entry[0]; - var value = entry[1]; - if (value == null) { - if (!(o.get_za3rmp$(key) == null && o.contains_za3rmp$(key))) return false; - } - else { - if (!Kotlin.equals(value, o.get_za3rmp$(key))) return false; - } - } - return true; - }; - - this.toString = function() { - var entries = this._entries(); - var length = entries.length; - if (length === 0) return "{}"; - var builder = "{"; - for (var i = 0;;) { - var entry = entries[i]; - var key = entry[0]; - var value = entry[1]; - builder += - (key === this ? "(this Map)" : Kotlin.toString(key)) + - "=" + - (value === this ? "(this Map)" : Kotlin.toString(value)); - if (++i >= length) return builder + "}"; - builder += ", " - } - }; + Hashtable.prototype.toString = function() { + var entries = this._entries(); + var length = entries.length; + if (length === 0) return "{}"; + var builder = "{"; + for (var i = 0;;) { + var entry = entries[i]; + var key = entry[0]; + var value = entry[1]; + builder += + (key === this ? "(this Map)" : Kotlin.toString(key)) + + "=" + + (value === this ? "(this Map)" : Kotlin.toString(value)); + if (++i >= length) return builder + "}"; + builder += ", " + } }; Kotlin.HashTable = Hashtable; @@ -515,7 +521,7 @@ lazyInitClasses.HashMap = Kotlin.createClass( function () { - return [Kotlin.modules['builtins'].kotlin.collections.MutableMap]; + return [Kotlin.HashTable, Kotlin.modules['builtins'].kotlin.collections.MutableMap]; }, function () { Kotlin.HashTable.call(this); @@ -766,75 +772,55 @@ convertKeyToKeyType: convertKeyToBoolean }); - function LinkedHashMap() { - Kotlin.ComplexHashMap.call(this); - this.orderedKeys = []; - - this.super_put_wn2jw4$ = this.put_wn2jw4$; - this.put_wn2jw4$ = function(key, value) { - if (!this.containsKey_za3rmp$(key)) { - this.orderedKeys.push(key); - } - - return this.super_put_wn2jw4$(key, value); - }; - - this.super_remove_za3rmp$ = this.remove_za3rmp$; - this.remove_za3rmp$ = function(key) { - var i = this.orderedKeys.indexOf(key); - if (i != -1) { - this.orderedKeys.splice(i, 1); - } - - return this.super_remove_za3rmp$(key); - }; - - this.super_clear = this.clear; - this.clear = function() { - this.super_clear(); - this.orderedKeys = []; - }; - - Object.defineProperty(this, "keys", { - get: function () { - // TODO return special Set which unsupported adding - var set = new Kotlin.LinkedHashSet(); - set.map = this; - return set; - } - }); - - Object.defineProperty(this, "entries", { - get: function () { - var result = new Kotlin.ArrayList(); - - for (var i = 0, c = this.orderedKeys, l = c.length; i < l; i++) { - result.add_za3rmp$(this.get_za3rmp$(c[i])); - } - - return result; - } - }); - - Object.defineProperty(this, "entries", { - get: function () { - var set = new Kotlin.LinkedHashSet(); - - for (var i = 0, c = this.orderedKeys, l = c.length; i < l; i++) { - set.add_za3rmp$(new Entry(c[i], this.get_za3rmp$(c[i]))); - } - - return set; - } - }); - } - lazyInitClasses.LinkedHashMap = Kotlin.createClass( function () { return [Kotlin.ComplexHashMap]; }, + /** @constructs */ function () { - LinkedHashMap.call(this); + Kotlin.ComplexHashMap.call(this); + this.orderedKeys = []; + }, + /** @lends {Kotlin.LinkedHashMap.prototype} */ + { + put_wn2jw4$: function (key, value) { + if (!this.containsKey_za3rmp$(key)) { + this.orderedKeys.push(key); + } + + return Kotlin.ComplexHashMap.prototype.put_wn2jw4$.call(this, key, value); + }, + remove_za3rmp$: function (key) { + var i = this.orderedKeys.indexOf(key); + if (i != -1) { + this.orderedKeys.splice(i, 1); + } + + return Kotlin.ComplexHashMap.prototype.remove_za3rmp$.call(this, key); + }, + clear: function () { + Kotlin.ComplexHashMap.prototype.clear.call(this); + this.orderedKeys = []; + }, + keys: { + get: function () { + // TODO return special Set which unsupported adding + var set = new Kotlin.LinkedHashSet(); + set.map = this; + return set; + } + }, + entries: { + get: function () { + var set = new Kotlin.LinkedHashSet(); + + for (var i = 0, c = this.orderedKeys, l = c.length; i < l; i++) { + set.add_za3rmp$(new Entry(c[i], this.get_za3rmp$(c[i]))); + } + + return set; + } + } } ); diff --git a/js/js.translator/testData/standardClasses/cases/mutableMapRemoveWithCollision.kt b/js/js.translator/testData/standardClasses/cases/mutableMapRemoveWithCollision.kt new file mode 100644 index 00000000000..9c77506c555 --- /dev/null +++ b/js/js.translator/testData/standardClasses/cases/mutableMapRemoveWithCollision.kt @@ -0,0 +1,22 @@ +package foo + +class A { + override fun hashCode() = 23456 +} + +fun box(): String { + val x = A() + val y = A() + + val map = mutableMapOf() + map[x] = 1 + assertEquals(1, map.size) + + map.remove(y) + assertEquals(1, map.size) + + map.remove(x) + assertEquals(0, map.size) + + return "OK" +} \ No newline at end of file