From f880043731e98e2571bdc00afc3754d9a6e1deba Mon Sep 17 00:00:00 2001 From: "Kenneth J. Shackleton" Date: Sun, 2 Jun 2024 13:37:51 +0100 Subject: [PATCH 01/14] Optimised LRU cache. Signed-off-by: Kenneth J. Shackleton --- .../cache/benchmarks/LruCacheBenchmark.kt | 6 +- .../FastLinkedStringMapBenchmark.kt | 97 ++++++++++ .../map/benchmarks/FastStringMapBenchmark.kt | 58 ++++++ .../selekt/jdk/benchmarks/ArrayBenchmark.kt | 50 +++++ .../jdk/benchmarks/ArrayListBenchmark.kt | 53 +++++ .../selekt/jdk/benchmarks/HashMapBenchmark.kt | 66 +++++++ .../jdk/benchmarks/LinkedHashMapBenchmark.kt | 88 +++++++++ .../jdk/benchmarks/LinkedListBenchmark.kt | 60 ++++++ .../com/bloomberg/selekt/SQLConnection.kt | 18 +- .../com/bloomberg/selekt/cache/LruCache.kt | 23 +-- .../collections/map/FastLinkedStringMap.kt | 171 ++++++++++++++++ .../selekt/collections/map/FastStringMap.kt | 153 +++++++++++++++ .../bloomberg/selekt/cache/LruCacheTest.kt | 45 +++-- .../map/FastLinkedStringMapTest.kt | 182 ++++++++++++++++++ .../collections/map/FastStringMapTest.kt | 181 +++++++++++++++++ 15 files changed, 1200 insertions(+), 51 deletions(-) create mode 100644 selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLinkedStringMapBenchmark.kt create mode 100644 selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStringMapBenchmark.kt create mode 100644 selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/ArrayBenchmark.kt create mode 100644 selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/ArrayListBenchmark.kt create mode 100644 selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/HashMapBenchmark.kt create mode 100644 selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/LinkedHashMapBenchmark.kt create mode 100644 selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/LinkedListBenchmark.kt create mode 100644 selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt create mode 100644 selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt create mode 100644 selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMapTest.kt create mode 100644 selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastStringMapTest.kt diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LruCacheBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LruCacheBenchmark.kt index 9ae099b5e4..b07b4fded8 100644 --- a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LruCacheBenchmark.kt +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LruCacheBenchmark.kt @@ -39,13 +39,13 @@ open class LruCacheBenchmark { @Benchmark @BenchmarkMode(Mode.Throughput) fun getEntry(input: CacheInput) = input.cache.run { - this["1", {}] + get("1") {} } @Benchmark @BenchmarkMode(Mode.Throughput) fun getEntryWithEviction(input: CacheInput) = input.cache.run { - this["1", {}] - this["2", {}] + get("1") {} + get("2") {} } } diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLinkedStringMapBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLinkedStringMapBenchmark.kt new file mode 100644 index 0000000000..89e0e9ea7f --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLinkedStringMapBenchmark.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.collections.map.benchmarks + +import com.bloomberg.selekt.collections.map.FastLinkedStringMap +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +open class LinkedMapInput { + internal lateinit var smallMap: FastLinkedStringMap + internal lateinit var largeMap: FastLinkedStringMap + internal lateinit var largeAccessMap: FastLinkedStringMap + + @Setup(Level.Iteration) + fun setUp() { + smallMap = FastLinkedStringMap(1, 1) {} + largeMap = FastLinkedStringMap(64, 64, false) {} + largeAccessMap = FastLinkedStringMap(64, 64, true) {} + } +} + +open class FastLinkedStringMapBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntry(input: LinkedMapInput) = input.smallMap.run { + getElsePut("1") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntries(input: LinkedMapInput) = input.largeMap.run { + getElsePut("1") { "" } + getElsePut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntriesDifferentLengths(input: LinkedMapInput) = input.largeMap.run { + getElsePut("1") { "" } + getElsePut("23") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntriesAccessOrder(input: LinkedMapInput) = input.largeAccessMap.run { + getElsePut("1") { "" } + getElsePut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntriesWithCollision(input: LinkedMapInput) = input.smallMap.run { + getElsePut("1") { "" } + getElsePut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntriesDifferentLengthsWithCollision(input: LinkedMapInput) = input.smallMap.run { + getElsePut("1") { "" } + getElsePut("23") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getThenRemoveEntry(input: LinkedMapInput) = input.smallMap.run { + getElsePut("1") { "" } + removeEntry("1") + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getThenRemoveKey(input: LinkedMapInput) = input.smallMap.run { + getElsePut("1") { "" } + removeKey("1") + } +} diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStringMapBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStringMapBenchmark.kt new file mode 100644 index 0000000000..0d3690cf25 --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStringMapBenchmark.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.collections.map.benchmarks + +import com.bloomberg.selekt.collections.map.FastStringMap +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +open class MapInput { + internal lateinit var map: FastStringMap + + @Setup(Level.Iteration) + fun setUp() { + map = FastStringMap(1) + } +} + +open class FastStringMapBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntry(input: MapInput) = input.map.run { + getEntryElsePut("1") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntryWithCollision(input: MapInput) = input.map.run { + getEntryElsePut("1") { "" } + getEntryElsePut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getThenRemoveEntry(input: MapInput) = input.map.run { + getEntryElsePut("1") { "" } + removeEntry("1") + } +} diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/ArrayBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/ArrayBenchmark.kt new file mode 100644 index 0000000000..ce85ef01ae --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/ArrayBenchmark.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.jdk.benchmarks + +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +open class ArrayInput { + internal lateinit var array: Array + + @Setup(Level.Iteration) + fun setUp() { + array = Array(2) { Any() } + } +} + +open class ArrayBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getFirst(input: ArrayInput) = input.array.run { + firstOrNull() + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntries(input: ArrayInput) = input.array.run { + firstOrNull() + this[1] + } +} diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/ArrayListBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/ArrayListBenchmark.kt new file mode 100644 index 0000000000..3d98bb9e35 --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/ArrayListBenchmark.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.jdk.benchmarks + +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +open class ArrayListInput { + internal lateinit var list: ArrayList + + @Setup(Level.Iteration) + fun setUp() { + list = ArrayList(1).apply { + add(Any()) + add(Any()) + } + } +} + +open class ArrayListBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getFirst(input: ArrayListInput) = input.list.run { + firstOrNull() + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntries(input: ArrayListInput) = input.list.run { + firstOrNull() + this[1] + } +} diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/HashMapBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/HashMapBenchmark.kt new file mode 100644 index 0000000000..fa385eabaa --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/HashMapBenchmark.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.jdk.benchmarks + +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +open class HashMapInput { + internal lateinit var smallMap: HashMap + internal lateinit var largeMap: HashMap + + @Setup(Level.Iteration) + fun setUp() { + smallMap = HashMap(1) + largeMap = HashMap(64) + } +} + +open class FastStringMapBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntry(input: HashMapInput) = input.smallMap.run { + getOrPut("1") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntries(input: HashMapInput) = input.largeMap.run { + getOrPut("1") { "" } + getOrPut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntryWithCollision(input: HashMapInput) = input.smallMap.run { + getOrPut("1") { "" } + getOrPut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getThenRemoveEntry(input: HashMapInput) = input.smallMap.run { + getOrPut("1") { "" } + remove("1") + } +} diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/LinkedHashMapBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/LinkedHashMapBenchmark.kt new file mode 100644 index 0000000000..9b4d3ec210 --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/LinkedHashMapBenchmark.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.jdk.benchmarks + +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +open class LinkedHashMapInput { + internal lateinit var smallMap: LinkedHashMap + internal lateinit var largeMap: LinkedHashMap + internal lateinit var largeAccessOrderMap: LinkedHashMap + + @Setup(Level.Iteration) + fun setUp() { + smallMap = object : LinkedHashMap(1, 1.1f, false) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry) = size > 1 + } + largeMap = object : LinkedHashMap(64, 1.1f, false) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry) = size > 64 + } + largeAccessOrderMap = object : LinkedHashMap(64, 1.1f, true) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry) = size > 64 + } + } +} + +open class LinkedHashMapBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntry(input: LinkedHashMapInput) = input.smallMap.run { + getOrPut("1") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntries(input: LinkedHashMapInput) = input.largeMap.run { + getOrPut("1") { "" } + getOrPut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntriesDifferentLengths(input: LinkedHashMapInput) = input.largeMap.run { + getOrPut("1") { "" } + getOrPut("23") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntriesAccessOrder(input: LinkedHashMapInput) = input.largeAccessOrderMap.run { + getOrPut("1") { "" } + getOrPut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntryWithRemoval(input: LinkedHashMapInput) = input.smallMap.run { + getOrPut("1") { "" } + getOrPut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getThenRemoveEntry(input: LinkedHashMapInput) = input.smallMap.run { + getOrPut("1") { "" } + remove("1") + } +} diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/LinkedListBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/LinkedListBenchmark.kt new file mode 100644 index 0000000000..5c2a11e1bf --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/LinkedListBenchmark.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.jdk.benchmarks + +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State +import java.util.LinkedList + +@State(Scope.Thread) +open class LinkedListInput { + internal lateinit var list: LinkedList + + data class Node( + val value: Any, + val next: Any? + ) + + @Setup(Level.Iteration) + fun setUp() { + list = LinkedList().apply { + val next = Any() + add(Node(Any(), next)) + add(Node(Any(), null)) + } + } +} + +open class LinkedListBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getFirst(input: LinkedListInput) = input.list.run { + firstOrNull() + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntries(input: LinkedListInput) = input.list.run { + val first = firstOrNull() + first?.next + } +} diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/SQLConnection.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/SQLConnection.kt index 4e71010efd..ec5f57e62a 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/SQLConnection.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/SQLConnection.kt @@ -189,18 +189,16 @@ internal class SQLConnection( block() } - private fun acquirePreparedStatement(sql: String) = preparedStatements[ - sql, { - val pointer = sqlite.prepare(pointer, sql) - pooledPreparedStatement.let { - if (it != null) { - SQLPreparedStatement.recycle(it, pointer, sql).also { pooledPreparedStatement = null } - } else { - SQLPreparedStatement(pointer, sql, sqlite, random) - } + private fun acquirePreparedStatement(sql: String) = preparedStatements.get(sql) { + val pointer = sqlite.prepare(pointer, sql) + pooledPreparedStatement.let { + if (it != null) { + SQLPreparedStatement.recycle(it, pointer, sql).also { pooledPreparedStatement = null } + } else { + SQLPreparedStatement(pointer, sql, sqlite, random) } } - ] + } private fun releasePreparedStatement(preparedStatement: SQLPreparedStatement) { if (runCatching { preparedStatement.resetAndClearBindings() }.isFailure) { diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LruCache.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LruCache.kt index 06a50e3a14..dcc8e47a51 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LruCache.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LruCache.kt @@ -16,36 +16,25 @@ package com.bloomberg.selekt.cache +import com.bloomberg.selekt.collections.map.FastLinkedStringMap import javax.annotation.concurrent.NotThreadSafe -private const val NO_RESIZE_LOAD_FACTOR = 1.1f - @NotThreadSafe -class LruCache(private val maxSize: Int, private val disposal: (T) -> Unit) { +class LruCache(maxSize: Int, disposal: (T) -> Unit) { @PublishedApi @JvmField @JvmSynthetic - internal val store = object : LinkedHashMap(maxSize, NO_RESIZE_LOAD_FACTOR, true) { - override fun removeEldestEntry(eldest: MutableMap.MutableEntry) = (size > maxSize).also { - if (it) { - disposal(eldest.value) - } - } - - override fun remove(key: String): T? = super.remove(key)?.also { disposal(it) } - } + internal val store = FastLinkedStringMap(maxSize, maxSize, false, disposal) fun evict(key: String) { - store.remove(key) + store.removeKey(key) } fun evictAll() { - store.values.toList() - .also { store.clear() } - .forEach { disposal(it) } + store.clear() } - inline operator fun get(key: String, supplier: () -> T): T = store.getOrPut(key, supplier) + inline fun get(key: String, supplier: () -> T): T = store.getElsePut(key, supplier) fun containsKey(key: String) = store.containsKey(key) } diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt new file mode 100644 index 0000000000..9ae7b46217 --- /dev/null +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt @@ -0,0 +1,171 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.collections.map + +import javax.annotation.concurrent.NotThreadSafe + +@NotThreadSafe +class FastLinkedStringMap( + @PublishedApi + @JvmField + internal val maxSize: Int, + capacity: Int = maxSize, + @PublishedApi + @JvmField + internal val accessOrder: Boolean = false, + private val disposal: (T) -> Unit +) : FastStringMap(capacity) { + private var head: LinkedEntry? = null + private var tail: LinkedEntry? = null + + @PublishedApi + @JvmField + internal var spare: LinkedEntry? = null + + inline fun getElsePut( + key: String, + supplier: () -> T + ): T { + val hashCode = hash(key) + val index = hashIndex(hashCode) + entryMatching(index, hashCode, key)?.let { + if (accessOrder) { + putFirst(it as LinkedEntry) + } + return it.value!! + } + if (size >= maxSize) { + spare = removeLastEntry() + } + return addAssociation(index, hashCode, key, supplier()).value!! + } + + fun removeKey(key: String) { + disposal((super.removeEntry(key) as LinkedEntry).unlink().value!!) + } + + override fun clear() { + super.clear() + spare = null + var entry = tail + while (entry != null) { + val previous = entry.previous + disposal(entry.unlink().value!!) + entry.key = "" + entry.value = null + entry = previous + } + } + + private fun LinkedEntry.unlink(): Entry = apply { + previous?.let { it.next = next } + next?.let { it.previous = previous } + if (this === head) { + head = next + } + if (this === tail) { + tail = previous + } + previous = null + next = null + } + + @PublishedApi + @JvmSynthetic + internal fun putFirst(node: LinkedEntry): Unit = node.run { + if (this === head) { + return + } + previous?.let { it.next = next } + next?.let { it.previous = previous } + if (this === tail) { + tail = previous + } + next = head + previous = null + head?.let { it.previous = this } + head = this + if (tail == null) { + tail = this + } + } + + @PublishedApi + override fun addAssociation( + index: Int, + hashCode: Int, + key: String, + value: T + ): Entry = (super.addAssociation(index, hashCode, key, value) as LinkedEntry).also { + putFirst(it) + } + + override fun createEntry( + index: Int, + hashCode: Int, + key: String, + value: T + ): Entry { + spare?.let { + spare = null + return it.update(index, hashCode, key, value, store[index]) + } + return LinkedEntry(index, hashCode, key, value, store[index]) + } + + @PublishedApi + @JvmSynthetic + internal fun removeLastEntry(): LinkedEntry = tail!!.apply { + previous?.let { it.next = null } ?: run { head = null } + tail = previous + previous = null + super.removeEntry(key) + key = "" + disposal(value!!) + value = null + } + + @PublishedApi + internal class LinkedEntry( + index: Int, + hashCode: Int, + key: String, + value: T, + after: Entry? + ) : Entry(index, hashCode, key, value, after) { + @JvmField + var previous: LinkedEntry? = null + + @JvmField + var next: LinkedEntry? = null + + @Suppress("NOTHING_TO_INLINE") + inline fun update( + index: Int, + hashCode: Int, + key: String, + value: T, + after: Entry? + ) = apply { + this.index = index + this.hashCode = hashCode + this.key = key + this.value = value + this.after = after + } + } +} diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt new file mode 100644 index 0000000000..dd5e68492f --- /dev/null +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt @@ -0,0 +1,153 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.collections.map + +import javax.annotation.concurrent.NotThreadSafe + +/** + * @param capacity a power of two. + */ +@NotThreadSafe +open class FastStringMap(capacity: Int) { + @JvmField + var size: Int = 0 + + @JvmField + @PublishedApi + internal val store = arrayOfNulls>(capacity) + private val hashLimit = capacity - 1 + + fun isEmpty() = 0 == size + + fun containsKey(key: String): Boolean { + val hashCode = hash(key) + val index = hashIndex(hashCode) + var entry = store[index] + while (entry != null) { + if (entry.hashCode == hashCode && entry.key == key) { + return true + } + entry = entry.after + } + return false + } + + inline fun getEntryElsePut( + key: String, + supplier: () -> T + ): Entry { + val hashCode = hash(key) + val index = hashIndex(hashCode) + var entry = store[index] + while (entry != null) { + if (entry.hashCode == hashCode && entry.key == key) { + return entry + } + entry = entry.after + } + return addAssociation(index, hashCode, key, supplier()) + } + + fun removeEntry(key: String): Entry { + val hashCode = hash(key) + val index = hashIndex(hashCode) + var entry = store[index] + var previous: Entry? = null + while (entry != null) { + if (entry.hashCode == hashCode && entry.key == key) { + return removeAssociation(entry, previous) + } + previous = entry + entry = entry.after + } + throw NoSuchElementException() + } + + @PublishedApi + internal open fun addAssociation( + index: Int, + hashCode: Int, + key: String, + value: T + ): Entry = createEntry(index, hashCode, key, value).also { + store[index] = it + size += 1 + } + + protected open fun createEntry( + index: Int, + hashCode: Int, + key: String, + value: T + ): Entry = Entry(index, hashCode, key, value, store[index]) + + open fun clear() { + store.fill(null) + size = 0 + } + + @Suppress("NOTHING_TO_INLINE") + @PublishedApi + internal inline fun entryMatching(index: Int, hashCode: Int, key: String): Entry? { + var entry = store[index] + while (entry != null) { + if (entry.hashCode == hashCode && entry.key == key) { + return entry + } + entry = entry.after + } + return null + } + + private fun removeAssociation( + entry: Entry, + previousEntry: Entry? + ): Entry { + if (previousEntry == null) { + store[entry.index] = entry.after + } else { + previousEntry.after = entry.after + } + size -= 1 + return entry + } + + @PublishedApi + internal fun hash(key: String): Int = key.hashCode() + + @PublishedApi + internal fun hashIndex(hashCode: Int): Int = hashCode and hashLimit + + open class Entry( + @JvmField + var index: Int, + @JvmField + var hashCode: Int, + @JvmField + var key: String, + @JvmField + var value: T?, + @JvmField + var after: Entry? + ) { + internal fun reset(): T? = value.also { _ -> + key = "" + value = null + after = null + } + } +} diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/LruCacheTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/LruCacheTest.kt index 7679080144..5c84815ef2 100644 --- a/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/LruCacheTest.kt +++ b/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/LruCacheTest.kt @@ -17,6 +17,7 @@ package com.bloomberg.selekt.cache import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doReturn import org.mockito.kotlin.inOrder @@ -37,8 +38,8 @@ internal class LruCacheTest { val first = Any() val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } val cache = LruCache(1, disposal) - cache["1", { first }] - assertSame(first, cache["1", { fail() }]) + cache.get("1") { first } + assertSame(first, cache.get("1") { fail() }) } @Test @@ -47,10 +48,10 @@ internal class LruCacheTest { val second = Any() val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } val cache = LruCache(2, disposal) - cache["1", { first }] - cache["2", { second }] - assertSame(first, cache["1", { fail() }]) - assertSame(second, cache["2", { fail() }]) + cache.get("1") { first } + cache.get("2") { second } + assertSame(first, cache.get("1") { fail() }) + assertSame(second, cache.get("2") { fail() }) } @Test @@ -59,10 +60,10 @@ internal class LruCacheTest { val second = Any() val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } val cache = LruCache(1, disposal) - cache["1", { first }] - cache["2", { second }] + cache.get("1") { first } + cache.get("2") { second } assertFalse(cache.containsKey("1")) - assertSame(second, cache["2", { fail() }]) + assertSame(second, cache.get("2") { fail() }) } @Test @@ -71,13 +72,13 @@ internal class LruCacheTest { val second = Any() val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } val cache = LruCache(2, disposal) - cache["1", { first }] - cache["2", { second }] + cache.get("1") { first } + cache.get("2") { second } cache.evict("1") inOrder(disposal) { verify(disposal, times(1)).invoke(same(first)) } - assertSame(second, cache["2", { fail() }]) + assertSame(second, cache.get("2") { fail() }) } @Test @@ -86,8 +87,8 @@ internal class LruCacheTest { val second = Any() val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } val cache = LruCache(2, disposal) - cache["1", { first }] - cache["2", { second }] + cache.get("1") { first } + cache.get("2") { second } cache.evictAll() inOrder(disposal) { verify(disposal, times(1)).invoke(same(first)) @@ -99,7 +100,9 @@ internal class LruCacheTest { fun evictWhenEmpty() { val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } val cache = LruCache(1, disposal) - cache.evict("1") + assertThrows { + cache.evict("1") + } verify(disposal, never()).invoke(anyOrNull()) } @@ -109,8 +112,8 @@ internal class LruCacheTest { val second = Any() val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } val cache = LruCache(1, disposal) - cache["1", { first }] - cache["2", { second }] + cache.get("1") { first } + cache.get("2") { second } inOrder(disposal) { verify(disposal, times(1)).invoke(same(first)) } @@ -122,9 +125,9 @@ internal class LruCacheTest { val supplier = mock<() -> Any>() whenever(supplier.invoke()) doReturn Any() val cache = LruCache(1, disposal) - val item = cache["1", supplier] + val item = cache.get("1", supplier) verify(supplier, times(1)).invoke() - assertSame(item, cache["1", supplier]) + assertSame(item, cache.get("1", supplier)) } @Test @@ -133,7 +136,7 @@ internal class LruCacheTest { val supplier = mock<() -> Any>() whenever(supplier.invoke()) doReturn Any() val cache = LruCache(1, disposal) - cache["1", supplier] + cache.get("1", supplier) assertFalse(cache.containsKey("2")) } @@ -143,7 +146,7 @@ internal class LruCacheTest { val supplier = mock<() -> Any>() whenever(supplier.invoke()) doReturn Any() val cache = LruCache(1, disposal) - cache["1", supplier] + cache.get("1", supplier) assertTrue(cache.containsKey("1")) } } diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMapTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMapTest.kt new file mode 100644 index 0000000000..bf7a6de421 --- /dev/null +++ b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMapTest.kt @@ -0,0 +1,182 @@ +/* + * Copyright 2020 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.collections.map + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.inOrder +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.same +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue +import kotlin.test.fail + +internal class FastLinkedStringMapTest { + @Test + fun get() { + val first = Any() + val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + val map = FastLinkedStringMap(1, 1, false, disposal) + assertSame(first, map.getElsePut("1") { first }) + } + + @Test + fun getTwo() { + val first = Any() + val second = Any() + val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + val map = FastLinkedStringMap(2, 64, false, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + assertSame(first, map.getElsePut("1") { fail() }) + assertSame(second, map.getElsePut("2") { fail() }) + } + + @Test + fun getAfterEvict() { + val first = Any() + val second = Any() + val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + val map = FastLinkedStringMap(1, 1, false, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + assertFalse(map.containsKey("1")) + assertSame(second, map.getElsePut("2") { fail() }) + } + + @Test + fun remove() { + val first = Any() + val second = Any() + val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + val map = FastLinkedStringMap(2, 64, false, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + map.removeKey("1") + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(first)) + } + assertSame(second, map.getElsePut("2") { fail() }) + } + + @Test + fun clear() { + val first = Any() + val second = Any() + val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + val map = FastLinkedStringMap(2, 64, false, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + map.clear() + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(first)) + verify(disposal, times(1)).invoke(same(second)) + } + } + + @Test + fun removeWhenEmpty() { + val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + val map = FastLinkedStringMap(1, 1, false, disposal) + assertThrows { + map.removeKey("1") + } + verify(disposal, never()).invoke(anyOrNull()) + } + + @Test + fun removeLastEntryAccessed() { + val first = Any() + val second = Any() + val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + val map = FastLinkedStringMap(2, 2, true, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + map.getElsePut("1") { first } + map.removeLastEntry() + assertEquals(1, map.size) + assertTrue(map.containsKey("1")) + assertFalse(map.containsKey("2")) + } + + @Test + fun removeLastEntryInserted() { + val first = Any() + val second = Any() + val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + val map = FastLinkedStringMap(2, 2, false, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + map.getElsePut("1") { first } + map.removeLastEntry() + assertEquals(1, map.size) + assertFalse(map.containsKey("1")) + assertTrue(map.containsKey("2")) + } + + @Test + fun evictLeastRecentlyUsed() { + val first = Any() + val second = Any() + val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + val map = FastLinkedStringMap(1, 1, false, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(first)) + } + } + + @Test + fun getWhenAbsent() { + val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + val supplier = mock<() -> Any>() + whenever(supplier.invoke()) doReturn Any() + val map = FastLinkedStringMap(1, 1, false, disposal) + val item = map.getElsePut("1", supplier) + verify(supplier, times(1)).invoke() + assertSame(item, map.getElsePut("1", supplier)) + } + + @Test + fun containsFalse() { + val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + val supplier = mock<() -> Any>() + whenever(supplier.invoke()) doReturn Any() + val map = FastLinkedStringMap(1, 1, false, disposal) + map.getElsePut("1", supplier) + assertFalse(map.containsKey("2")) + } + + @Test + fun containsTrue() { + val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + val supplier = mock<() -> Any>() + whenever(supplier.invoke()) doReturn Any() + val map = FastLinkedStringMap(1, 1, false, disposal) + map.getElsePut("1", supplier) + assertTrue(map.containsKey("1")) + } +} diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastStringMapTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastStringMapTest.kt new file mode 100644 index 0000000000..7db457cb34 --- /dev/null +++ b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastStringMapTest.kt @@ -0,0 +1,181 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.collections.map + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue +import kotlin.test.fail + +internal class FastStringMapTest { + @Test + fun get() { + val first = Any() + val map = FastStringMap(1) + assertSame(first, map.getEntryElsePut("1") { first }.value) + } + + @Test + fun sizeOne() { + val first = Any() + val map = FastStringMap(1) + map.getEntryElsePut("1") { first } + assertEquals(1, map.size) + } + + @Test + fun getTwice() { + val first = Any() + val map = FastStringMap(1) + map.getEntryElsePut("1") { first } + assertSame(first, map.getEntryElsePut("1") { fail() }.value) + } + + @Test + fun getWhenAbsent() { + val supplier = mock<() -> Any>() + whenever(supplier.invoke()) doReturn Any() + val map = FastStringMap(1) + val item = map.getEntryElsePut("1", supplier) + verify(supplier, times(1)).invoke() + assertSame(item, map.getEntryElsePut("1", supplier)) + verifyNoMoreInteractions(supplier) + } + + @Test + fun getTwo() { + val first = Any() + val second = Any() + val map = FastStringMap(64) + map.getEntryElsePut("1") { first } + map.getEntryElsePut("2") { second } + assertEquals(2, map.size) + } + + @Test + fun getTwoWithCollisions() { + val first = Any() + val second = Any() + val map = FastStringMap(1) + map.getEntryElsePut("1") { first } + map.getEntryElsePut("2") { second } + assertSame(first, map.getEntryElsePut("1") { fail() }.value) + assertSame(second, map.getEntryElsePut("2") { fail() }.value) + } + + @Test + fun sizeTwo() { + val first = Any() + val second = Any() + val map = FastStringMap(1) + map.getEntryElsePut("1") { first } + map.getEntryElsePut("2") { second } + assertSame(first, map.getEntryElsePut("1") { fail() }.value) + assertSame(second, map.getEntryElsePut("2") { fail() }.value) + } + + @Test + fun removeOne() { + val first = Any() + val map = FastStringMap(1) + map.getEntryElsePut("1") { first } + assertSame(first, map.removeEntry("1").value) + } + + @Test + fun removeTwo() { + val first = Any() + val second = Any() + val map = FastStringMap(2) + map.getEntryElsePut("1") { first } + map.getEntryElsePut("2") { second } + assertSame(first, map.removeEntry("1").value) + assertSame(second, map.getEntryElsePut("2") { fail() }.value) + } + + @Test + fun removeTwoWithCollisions() { + val first = Any() + val second = Any() + val map = FastStringMap(1) + map.getEntryElsePut("1") { first } + map.getEntryElsePut("2") { second } + assertSame(first, map.removeEntry("1").value) + assertSame(second, map.getEntryElsePut("2") { fail() }.value) + } + + @Test + fun removeThenSize() { + val first = Any() + val map = FastStringMap(1) + map.getEntryElsePut("1") { first } + map.removeEntry("1") + assertEquals(0, map.size) + } + + @Test + fun removeWhenEmpty() { + val map = FastStringMap(1) + assertThrows { + map.removeEntry("1") + } + assertEquals(0, map.size) + } + + @Test + fun clear() { + val map = FastStringMap(1) + map.getEntryElsePut("1") { Any() } + assertEquals(1, map.size) + map.clear() + assertTrue(map.isEmpty()) + } + + @Test + fun clearWhenEmpty() { + val map = FastStringMap(1) + map.clear() + assertTrue(map.isEmpty()) + } + + @Test + fun containsFalse() { + val supplier = mock<() -> Any>() + whenever(supplier.invoke()) doReturn Any() + val map = FastStringMap(1) + map.getEntryElsePut("1", supplier) + assertFalse(map.containsKey("2")) + } + + @Test + fun containsTrue() { + val supplier = mock<() -> Any>() + whenever(supplier.invoke()) doReturn Any() + val map = FastStringMap(1) + map.getEntryElsePut("1", supplier) + assertTrue(map.containsKey("1")) + } +} From b02fa100a646da42599a3026e4b168613b6cfa2d Mon Sep 17 00:00:00 2001 From: "Kenneth J. Shackleton" Date: Fri, 21 Jun 2024 13:21:50 +0100 Subject: [PATCH 02/14] Add a map that's fast for adding and reading, slow for removal of least recently used. --- .../benchmarks/FastLruStringMapBenchmark.kt | 58 ++++++ .../collections/map/FastLruStringMap.kt | 110 +++++++++++ .../collections/map/FastLruStringMapTest.kt | 181 ++++++++++++++++++ 3 files changed, 349 insertions(+) create mode 100644 selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLruStringMapBenchmark.kt create mode 100644 selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLruStringMap.kt create mode 100644 selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastLruStringMapTest.kt diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLruStringMapBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLruStringMapBenchmark.kt new file mode 100644 index 0000000000..bcad48d43b --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLruStringMapBenchmark.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.collections.map.benchmarks + +import com.bloomberg.selekt.collections.map.FastLruStringMap +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +open class LruMapInput { + internal lateinit var map: FastLruStringMap + + @Setup(Level.Iteration) + fun setUp() { + map = FastLruStringMap(1) + } +} + +open class FastLruStringMapBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntry(input: LruMapInput) = input.map.run { + getEntryElsePut("1") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntryWithCollision(input: LruMapInput) = input.map.run { + getEntryElsePut("1") { "" } + getEntryElsePut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getThenRemoveEntry(input: LruMapInput) = input.map.run { + getEntryElsePut("1") { "" } + removeEntry("1") + } +} diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLruStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLruStringMap.kt new file mode 100644 index 0000000000..1b323701b6 --- /dev/null +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLruStringMap.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.collections.map + +import javax.annotation.concurrent.NotThreadSafe + +@NotThreadSafe +class FastLruStringMap( + capacity: Int +) : FastStringMap(capacity) { + private var accessCount = Int.MIN_VALUE + private var spare: LruEntry? = null + + inline fun getElsePut( + key: String, + supplier: () -> T + ): T { + val hashCode = hash(key) + val index = hashIndex(hashCode) + entryMatching(index, hashCode, key)?.let { + (it as LruEntry).accessCount = nextAccessCount() + return it.value!! + } + return addAssociation(index, hashCode, key, supplier()).value!! + } + + override fun createEntry( + index: Int, + hashCode: Int, + key: String, + value: T + ): Entry { + spare?.let { + spare = null + return it.update(index, hashCode, key, value, nextAccessCount(), store[index]) + } + return LruEntry(index, hashCode, key, value, nextAccessCount(), store[index]) + } + + override fun clear() { + super.clear() + spare = null + } + + @PublishedApi + internal fun nextAccessCount(): Int { + accessCount += 1 + if (accessCount == Int.MIN_VALUE) { + resetAllAccessCounts() + } + return accessCount + } + + private fun resetAllAccessCounts() { + store.flatMap?, Entry> { + sequence { + var current = it + while (current != null) { + yield(current) + current = current.after + } + }.toList() + }.sortedBy { + (it as LruEntry).accessCount + }.forEachIndexed { index, it -> + (it as LruEntry).accessCount = Int.MIN_VALUE + index + } + } + + @PublishedApi + internal class LruEntry( + index: Int, + hashCode: Int, + key: String, + value: T, + var accessCount: Int, + after: Entry? + ) : Entry(index, hashCode, key, value, after) { + @Suppress("NOTHING_TO_INLINE") + inline fun update( + index: Int, + hashCode: Int, + key: String, + value: T, + accessCount: Int, + after: Entry? + ) = apply { + this.index = index + this.hashCode = hashCode + this.key = key + this.value = value + this.accessCount = accessCount + this.after = after + } + } +} diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastLruStringMapTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastLruStringMapTest.kt new file mode 100644 index 0000000000..f006ef2eb3 --- /dev/null +++ b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastLruStringMapTest.kt @@ -0,0 +1,181 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.collections.map + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue +import kotlin.test.fail + +internal class FastLruStringMapTest { + @Test + fun get() { + val first = Any() + val map = FastLruStringMap(1) + assertSame(first, map.getElsePut("1") { first }) + } + + @Test + fun sizeOne() { + val first = Any() + val map = FastLruStringMap(1) + map.getElsePut("1") { first } + assertEquals(1, map.size) + } + + @Test + fun getTwice() { + val first = Any() + val map = FastLruStringMap(1) + map.getElsePut("1") { first } + assertSame(first, map.getElsePut("1") { fail() }) + } + + @Test + fun getWhenAbsent() { + val supplier = mock<() -> Any>() + whenever(supplier.invoke()) doReturn Any() + val map = FastLruStringMap(1) + val item = map.getElsePut("1", supplier) + verify(supplier, times(1)).invoke() + assertSame(item, map.getElsePut("1", supplier)) + verifyNoMoreInteractions(supplier) + } + + @Test + fun getTwo() { + val first = Any() + val second = Any() + val map = FastLruStringMap(64) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + assertEquals(2, map.size) + } + + @Test + fun getTwoWithCollisions() { + val first = Any() + val second = Any() + val map = FastLruStringMap(1) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + assertSame(first, map.getElsePut("1") { fail() }) + assertSame(second, map.getElsePut("2") { fail() }) + } + + @Test + fun sizeTwo() { + val first = Any() + val second = Any() + val map = FastLruStringMap(1) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + assertSame(first, map.getElsePut("1") { fail() }) + assertSame(second, map.getElsePut("2") { fail() }) + } + + @Test + fun removeOne() { + val first = Any() + val map = FastLruStringMap(1) + map.getElsePut("1") { first } + assertSame(first, map.removeEntry("1").value) + } + + @Test + fun removeTwo() { + val first = Any() + val second = Any() + val map = FastLruStringMap(2) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + assertSame(first, map.removeEntry("1").value) + assertSame(second, map.getElsePut("2") { fail() }) + } + + @Test + fun removeTwoWithCollisions() { + val first = Any() + val second = Any() + val map = FastLruStringMap(1) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + assertSame(first, map.removeEntry("1").value) + assertSame(second, map.getElsePut("2") { fail() }) + } + + @Test + fun removeThenSize() { + val first = Any() + val map = FastLruStringMap(1) + map.getElsePut("1") { first } + map.removeEntry("1") + assertEquals(0, map.size) + } + + @Test + fun removeWhenEmpty() { + val map = FastLruStringMap(1) + assertThrows { + map.removeEntry("1") + } + assertEquals(0, map.size) + } + + @Test + fun clear() { + val map = FastLruStringMap(1) + map.getElsePut("1") { Any() } + assertEquals(1, map.size) + map.clear() + assertTrue(map.isEmpty()) + } + + @Test + fun clearWhenEmpty() { + val map = FastLruStringMap(1) + map.clear() + assertTrue(map.isEmpty()) + } + + @Test + fun containsFalse() { + val supplier = mock<() -> Any>() + whenever(supplier.invoke()) doReturn Any() + val map = FastLruStringMap(1) + map.getElsePut("1", supplier) + assertFalse(map.containsKey("2")) + } + + @Test + fun containsTrue() { + val supplier = mock<() -> Any>() + whenever(supplier.invoke()) doReturn Any() + val map = FastLruStringMap(1) + map.getElsePut("1", supplier) + assertTrue(map.containsKey("1")) + } +} From fbe82ea973d3f329e2c87beecbb36a9276fd172b Mon Sep 17 00:00:00 2001 From: "Kenneth J. Shackleton" Date: Fri, 21 Jun 2024 14:45:01 +0100 Subject: [PATCH 03/14] Rename to FastLruStringMap. --- .../benchmarks/FastLruStringMapBenchmark.kt | 6 +- ...ngMap.kt => FastAccessOrderedStringMap.kt} | 56 +++++++++++-------- ...t.kt => FastAccessOrderedStringMapTest.kt} | 34 +++++------ 3 files changed, 54 insertions(+), 42 deletions(-) rename selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/{FastLruStringMap.kt => FastAccessOrderedStringMap.kt} (65%) rename selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/{FastLruStringMapTest.kt => FastAccessOrderedStringMapTest.kt} (82%) diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLruStringMapBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLruStringMapBenchmark.kt index bcad48d43b..8c0019cc29 100644 --- a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLruStringMapBenchmark.kt +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLruStringMapBenchmark.kt @@ -16,7 +16,7 @@ package com.bloomberg.selekt.collections.map.benchmarks -import com.bloomberg.selekt.collections.map.FastLruStringMap +import com.bloomberg.selekt.collections.map.FastAccessOrderedStringMap import org.openjdk.jmh.annotations.Benchmark import org.openjdk.jmh.annotations.BenchmarkMode import org.openjdk.jmh.annotations.Level @@ -27,11 +27,11 @@ import org.openjdk.jmh.annotations.State @State(Scope.Thread) open class LruMapInput { - internal lateinit var map: FastLruStringMap + internal lateinit var map: FastAccessOrderedStringMap @Setup(Level.Iteration) fun setUp() { - map = FastLruStringMap(1) + map = FastAccessOrderedStringMap(1) } } diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLruStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastAccessOrderedStringMap.kt similarity index 65% rename from selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLruStringMap.kt rename to selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastAccessOrderedStringMap.kt index 1b323701b6..e222bc0f03 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLruStringMap.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastAccessOrderedStringMap.kt @@ -19,11 +19,11 @@ package com.bloomberg.selekt.collections.map import javax.annotation.concurrent.NotThreadSafe @NotThreadSafe -class FastLruStringMap( +class FastAccessOrderedStringMap( capacity: Int ) : FastStringMap(capacity) { - private var accessCount = Int.MIN_VALUE - private var spare: LruEntry? = null + private var accessNumber = Int.MIN_VALUE + private var spare: AOEntry? = null inline fun getElsePut( key: String, @@ -32,7 +32,7 @@ class FastLruStringMap( val hashCode = hash(key) val index = hashIndex(hashCode) entryMatching(index, hashCode, key)?.let { - (it as LruEntry).accessCount = nextAccessCount() + (it as AOEntry).accessNumber = nextAccessCount() return it.value!! } return addAssociation(index, hashCode, key, supplier()).value!! @@ -48,7 +48,7 @@ class FastLruStringMap( spare = null return it.update(index, hashCode, key, value, nextAccessCount(), store[index]) } - return LruEntry(index, hashCode, key, value, nextAccessCount(), store[index]) + return AOEntry(index, hashCode, key, value, nextAccessCount(), store[index]) } override fun clear() { @@ -56,38 +56,50 @@ class FastLruStringMap( spare = null } + internal fun asLinkedMap( + capacity: Int = size + ) = FastLinkedStringMap(capacity = capacity).apply { + entries().sortedBy { + (it as AOEntry).accessNumber + }.forEach { + addAssociation(it.index, it.hashCode, it.key, it.value!!) + } + } + @PublishedApi internal fun nextAccessCount(): Int { - accessCount += 1 - if (accessCount == Int.MIN_VALUE) { + accessNumber += 1 + if (accessNumber == Int.MIN_VALUE) { resetAllAccessCounts() } - return accessCount + return accessNumber } + private fun entries(): Iterable> = store.flatMap { + sequence { + var current = it + while (current != null) { + yield(current) + current = current.after + } + } + }.asIterable() + private fun resetAllAccessCounts() { - store.flatMap?, Entry> { - sequence { - var current = it - while (current != null) { - yield(current) - current = current.after - } - }.toList() - }.sortedBy { - (it as LruEntry).accessCount + entries().sortedBy { + (it as AOEntry).accessNumber }.forEachIndexed { index, it -> - (it as LruEntry).accessCount = Int.MIN_VALUE + index + (it as AOEntry).accessNumber = Int.MIN_VALUE + index } } @PublishedApi - internal class LruEntry( + internal class AOEntry( index: Int, hashCode: Int, key: String, value: T, - var accessCount: Int, + var accessNumber: Int, after: Entry? ) : Entry(index, hashCode, key, value, after) { @Suppress("NOTHING_TO_INLINE") @@ -103,7 +115,7 @@ class FastLruStringMap( this.hashCode = hashCode this.key = key this.value = value - this.accessCount = accessCount + this.accessNumber = accessCount this.after = after } } diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastLruStringMapTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastAccessOrderedStringMapTest.kt similarity index 82% rename from selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastLruStringMapTest.kt rename to selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastAccessOrderedStringMapTest.kt index f006ef2eb3..07c397068d 100644 --- a/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastLruStringMapTest.kt +++ b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastAccessOrderedStringMapTest.kt @@ -30,18 +30,18 @@ import kotlin.test.assertSame import kotlin.test.assertTrue import kotlin.test.fail -internal class FastLruStringMapTest { +internal class FastAccessOrderedStringMapTest { @Test fun get() { val first = Any() - val map = FastLruStringMap(1) + val map = FastAccessOrderedStringMap(1) assertSame(first, map.getElsePut("1") { first }) } @Test fun sizeOne() { val first = Any() - val map = FastLruStringMap(1) + val map = FastAccessOrderedStringMap(1) map.getElsePut("1") { first } assertEquals(1, map.size) } @@ -49,7 +49,7 @@ internal class FastLruStringMapTest { @Test fun getTwice() { val first = Any() - val map = FastLruStringMap(1) + val map = FastAccessOrderedStringMap(1) map.getElsePut("1") { first } assertSame(first, map.getElsePut("1") { fail() }) } @@ -58,7 +58,7 @@ internal class FastLruStringMapTest { fun getWhenAbsent() { val supplier = mock<() -> Any>() whenever(supplier.invoke()) doReturn Any() - val map = FastLruStringMap(1) + val map = FastAccessOrderedStringMap(1) val item = map.getElsePut("1", supplier) verify(supplier, times(1)).invoke() assertSame(item, map.getElsePut("1", supplier)) @@ -69,7 +69,7 @@ internal class FastLruStringMapTest { fun getTwo() { val first = Any() val second = Any() - val map = FastLruStringMap(64) + val map = FastAccessOrderedStringMap(64) map.getElsePut("1") { first } map.getElsePut("2") { second } assertEquals(2, map.size) @@ -79,7 +79,7 @@ internal class FastLruStringMapTest { fun getTwoWithCollisions() { val first = Any() val second = Any() - val map = FastLruStringMap(1) + val map = FastAccessOrderedStringMap(1) map.getElsePut("1") { first } map.getElsePut("2") { second } assertSame(first, map.getElsePut("1") { fail() }) @@ -90,7 +90,7 @@ internal class FastLruStringMapTest { fun sizeTwo() { val first = Any() val second = Any() - val map = FastLruStringMap(1) + val map = FastAccessOrderedStringMap(1) map.getElsePut("1") { first } map.getElsePut("2") { second } assertSame(first, map.getElsePut("1") { fail() }) @@ -100,7 +100,7 @@ internal class FastLruStringMapTest { @Test fun removeOne() { val first = Any() - val map = FastLruStringMap(1) + val map = FastAccessOrderedStringMap(1) map.getElsePut("1") { first } assertSame(first, map.removeEntry("1").value) } @@ -109,7 +109,7 @@ internal class FastLruStringMapTest { fun removeTwo() { val first = Any() val second = Any() - val map = FastLruStringMap(2) + val map = FastAccessOrderedStringMap(2) map.getElsePut("1") { first } map.getElsePut("2") { second } assertSame(first, map.removeEntry("1").value) @@ -120,7 +120,7 @@ internal class FastLruStringMapTest { fun removeTwoWithCollisions() { val first = Any() val second = Any() - val map = FastLruStringMap(1) + val map = FastAccessOrderedStringMap(1) map.getElsePut("1") { first } map.getElsePut("2") { second } assertSame(first, map.removeEntry("1").value) @@ -130,7 +130,7 @@ internal class FastLruStringMapTest { @Test fun removeThenSize() { val first = Any() - val map = FastLruStringMap(1) + val map = FastAccessOrderedStringMap(1) map.getElsePut("1") { first } map.removeEntry("1") assertEquals(0, map.size) @@ -138,7 +138,7 @@ internal class FastLruStringMapTest { @Test fun removeWhenEmpty() { - val map = FastLruStringMap(1) + val map = FastAccessOrderedStringMap(1) assertThrows { map.removeEntry("1") } @@ -147,7 +147,7 @@ internal class FastLruStringMapTest { @Test fun clear() { - val map = FastLruStringMap(1) + val map = FastAccessOrderedStringMap(1) map.getElsePut("1") { Any() } assertEquals(1, map.size) map.clear() @@ -156,7 +156,7 @@ internal class FastLruStringMapTest { @Test fun clearWhenEmpty() { - val map = FastLruStringMap(1) + val map = FastAccessOrderedStringMap(1) map.clear() assertTrue(map.isEmpty()) } @@ -165,7 +165,7 @@ internal class FastLruStringMapTest { fun containsFalse() { val supplier = mock<() -> Any>() whenever(supplier.invoke()) doReturn Any() - val map = FastLruStringMap(1) + val map = FastAccessOrderedStringMap(1) map.getElsePut("1", supplier) assertFalse(map.containsKey("2")) } @@ -174,7 +174,7 @@ internal class FastLruStringMapTest { fun containsTrue() { val supplier = mock<() -> Any>() whenever(supplier.invoke()) doReturn Any() - val map = FastLruStringMap(1) + val map = FastAccessOrderedStringMap(1) map.getElsePut("1", supplier) assertTrue(map.containsKey("1")) } From 67f9198f9f3687b27f4febe0ed906b10ef301e25 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Shackleton" Date: Fri, 21 Jun 2024 19:06:26 +0100 Subject: [PATCH 04/14] Null head and tail in clear. --- .../com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt index 9ae7b46217..89baf52b38 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt @@ -61,6 +61,7 @@ class FastLinkedStringMap( override fun clear() { super.clear() spare = null + head = null var entry = tail while (entry != null) { val previous = entry.previous @@ -69,6 +70,7 @@ class FastLinkedStringMap( entry.value = null entry = previous } + tail = null } private fun LinkedEntry.unlink(): Entry = apply { From 025afc7855a649877f6a8aca38daecbbe3248d2d Mon Sep 17 00:00:00 2001 From: "Kenneth J. Shackleton" Date: Fri, 21 Jun 2024 19:10:11 +0100 Subject: [PATCH 05/14] Rename to Stamped. --- ...rk.kt => FastStampedStringMapBenchmark.kt} | 14 ++--- .../collections/map/FastLinkedStringMap.kt | 2 +- ...edStringMap.kt => FastStampedStringMap.kt} | 51 ++++++++----------- ...MapTest.kt => FastStampedStringMapTest.kt} | 34 ++++++------- 4 files changed, 47 insertions(+), 54 deletions(-) rename selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/{FastLruStringMapBenchmark.kt => FastStampedStringMapBenchmark.kt} (78%) rename selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/{FastAccessOrderedStringMap.kt => FastStampedStringMap.kt} (66%) rename selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/{FastAccessOrderedStringMapTest.kt => FastStampedStringMapTest.kt} (82%) diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLruStringMapBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStampedStringMapBenchmark.kt similarity index 78% rename from selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLruStringMapBenchmark.kt rename to selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStampedStringMapBenchmark.kt index 8c0019cc29..6fb0f98fa2 100644 --- a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLruStringMapBenchmark.kt +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStampedStringMapBenchmark.kt @@ -16,7 +16,7 @@ package com.bloomberg.selekt.collections.map.benchmarks -import com.bloomberg.selekt.collections.map.FastAccessOrderedStringMap +import com.bloomberg.selekt.collections.map.FastStampedStringMap import org.openjdk.jmh.annotations.Benchmark import org.openjdk.jmh.annotations.BenchmarkMode import org.openjdk.jmh.annotations.Level @@ -26,32 +26,32 @@ import org.openjdk.jmh.annotations.Setup import org.openjdk.jmh.annotations.State @State(Scope.Thread) -open class LruMapInput { - internal lateinit var map: FastAccessOrderedStringMap +open class StampedMapInput { + internal lateinit var map: FastStampedStringMap @Setup(Level.Iteration) fun setUp() { - map = FastAccessOrderedStringMap(1) + map = FastStampedStringMap(1) } } open class FastLruStringMapBenchmark { @Benchmark @BenchmarkMode(Mode.Throughput) - fun getEntry(input: LruMapInput) = input.map.run { + fun getEntry(input: StampedMapInput) = input.map.run { getEntryElsePut("1") { "" } } @Benchmark @BenchmarkMode(Mode.Throughput) - fun getEntryWithCollision(input: LruMapInput) = input.map.run { + fun getEntryWithCollision(input: StampedMapInput) = input.map.run { getEntryElsePut("1") { "" } getEntryElsePut("2") { "" } } @Benchmark @BenchmarkMode(Mode.Throughput) - fun getThenRemoveEntry(input: LruMapInput) = input.map.run { + fun getThenRemoveEntry(input: StampedMapInput) = input.map.run { getEntryElsePut("1") { "" } removeEntry("1") } diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt index 89baf52b38..6c3f9bbda7 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt @@ -63,6 +63,7 @@ class FastLinkedStringMap( spare = null head = null var entry = tail + tail = null while (entry != null) { val previous = entry.previous disposal(entry.unlink().value!!) @@ -70,7 +71,6 @@ class FastLinkedStringMap( entry.value = null entry = previous } - tail = null } private fun LinkedEntry.unlink(): Entry = apply { diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastAccessOrderedStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt similarity index 66% rename from selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastAccessOrderedStringMap.kt rename to selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt index e222bc0f03..ba478ad9a4 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastAccessOrderedStringMap.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt @@ -19,11 +19,11 @@ package com.bloomberg.selekt.collections.map import javax.annotation.concurrent.NotThreadSafe @NotThreadSafe -class FastAccessOrderedStringMap( +class FastStampedStringMap( capacity: Int ) : FastStringMap(capacity) { - private var accessNumber = Int.MIN_VALUE - private var spare: AOEntry? = null + private var currentStamp = Int.MIN_VALUE + private var spare: StampedEntry? = null inline fun getElsePut( key: String, @@ -32,7 +32,7 @@ class FastAccessOrderedStringMap( val hashCode = hash(key) val index = hashIndex(hashCode) entryMatching(index, hashCode, key)?.let { - (it as AOEntry).accessNumber = nextAccessCount() + (it as StampedEntry).stamp = nextStamp() return it.value!! } return addAssociation(index, hashCode, key, supplier()).value!! @@ -46,9 +46,9 @@ class FastAccessOrderedStringMap( ): Entry { spare?.let { spare = null - return it.update(index, hashCode, key, value, nextAccessCount(), store[index]) + return it.update(index, hashCode, key, value, nextStamp(), store[index]) } - return AOEntry(index, hashCode, key, value, nextAccessCount(), store[index]) + return StampedEntry(index, hashCode, key, value, nextStamp(), store[index]) } override fun clear() { @@ -56,23 +56,13 @@ class FastAccessOrderedStringMap( spare = null } - internal fun asLinkedMap( - capacity: Int = size - ) = FastLinkedStringMap(capacity = capacity).apply { - entries().sortedBy { - (it as AOEntry).accessNumber - }.forEach { - addAssociation(it.index, it.hashCode, it.key, it.value!!) - } - } - @PublishedApi - internal fun nextAccessCount(): Int { - accessNumber += 1 - if (accessNumber == Int.MIN_VALUE) { - resetAllAccessCounts() + internal fun nextStamp(): Int { + if (Int.MAX_VALUE == currentStamp) { + resetAllStamps() } - return accessNumber + currentStamp += 1 + return currentStamp } private fun entries(): Iterable> = store.flatMap { @@ -85,21 +75,24 @@ class FastAccessOrderedStringMap( } }.asIterable() - private fun resetAllAccessCounts() { + private fun resetAllStamps() { entries().sortedBy { - (it as AOEntry).accessNumber - }.forEachIndexed { index, it -> - (it as AOEntry).accessNumber = Int.MIN_VALUE + index + (it as StampedEntry).stamp + }.run { + currentStamp = Int.MIN_VALUE + maxOf(0, size - 1) + forEachIndexed { index, it -> + (it as StampedEntry).stamp = Int.MIN_VALUE + index + } } } @PublishedApi - internal class AOEntry( + internal class StampedEntry( index: Int, hashCode: Int, key: String, value: T, - var accessNumber: Int, + var stamp: Int, after: Entry? ) : Entry(index, hashCode, key, value, after) { @Suppress("NOTHING_TO_INLINE") @@ -108,14 +101,14 @@ class FastAccessOrderedStringMap( hashCode: Int, key: String, value: T, - accessCount: Int, + stamp: Int, after: Entry? ) = apply { this.index = index this.hashCode = hashCode this.key = key this.value = value - this.accessNumber = accessCount + this.stamp = stamp this.after = after } } diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastAccessOrderedStringMapTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMapTest.kt similarity index 82% rename from selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastAccessOrderedStringMapTest.kt rename to selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMapTest.kt index 07c397068d..c0cb7fe458 100644 --- a/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastAccessOrderedStringMapTest.kt +++ b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMapTest.kt @@ -30,18 +30,18 @@ import kotlin.test.assertSame import kotlin.test.assertTrue import kotlin.test.fail -internal class FastAccessOrderedStringMapTest { +internal class FastStampedStringMapTest { @Test fun get() { val first = Any() - val map = FastAccessOrderedStringMap(1) + val map = FastStampedStringMap(1) assertSame(first, map.getElsePut("1") { first }) } @Test fun sizeOne() { val first = Any() - val map = FastAccessOrderedStringMap(1) + val map = FastStampedStringMap(1) map.getElsePut("1") { first } assertEquals(1, map.size) } @@ -49,7 +49,7 @@ internal class FastAccessOrderedStringMapTest { @Test fun getTwice() { val first = Any() - val map = FastAccessOrderedStringMap(1) + val map = FastStampedStringMap(1) map.getElsePut("1") { first } assertSame(first, map.getElsePut("1") { fail() }) } @@ -58,7 +58,7 @@ internal class FastAccessOrderedStringMapTest { fun getWhenAbsent() { val supplier = mock<() -> Any>() whenever(supplier.invoke()) doReturn Any() - val map = FastAccessOrderedStringMap(1) + val map = FastStampedStringMap(1) val item = map.getElsePut("1", supplier) verify(supplier, times(1)).invoke() assertSame(item, map.getElsePut("1", supplier)) @@ -69,7 +69,7 @@ internal class FastAccessOrderedStringMapTest { fun getTwo() { val first = Any() val second = Any() - val map = FastAccessOrderedStringMap(64) + val map = FastStampedStringMap(64) map.getElsePut("1") { first } map.getElsePut("2") { second } assertEquals(2, map.size) @@ -79,7 +79,7 @@ internal class FastAccessOrderedStringMapTest { fun getTwoWithCollisions() { val first = Any() val second = Any() - val map = FastAccessOrderedStringMap(1) + val map = FastStampedStringMap(1) map.getElsePut("1") { first } map.getElsePut("2") { second } assertSame(first, map.getElsePut("1") { fail() }) @@ -90,7 +90,7 @@ internal class FastAccessOrderedStringMapTest { fun sizeTwo() { val first = Any() val second = Any() - val map = FastAccessOrderedStringMap(1) + val map = FastStampedStringMap(1) map.getElsePut("1") { first } map.getElsePut("2") { second } assertSame(first, map.getElsePut("1") { fail() }) @@ -100,7 +100,7 @@ internal class FastAccessOrderedStringMapTest { @Test fun removeOne() { val first = Any() - val map = FastAccessOrderedStringMap(1) + val map = FastStampedStringMap(1) map.getElsePut("1") { first } assertSame(first, map.removeEntry("1").value) } @@ -109,7 +109,7 @@ internal class FastAccessOrderedStringMapTest { fun removeTwo() { val first = Any() val second = Any() - val map = FastAccessOrderedStringMap(2) + val map = FastStampedStringMap(2) map.getElsePut("1") { first } map.getElsePut("2") { second } assertSame(first, map.removeEntry("1").value) @@ -120,7 +120,7 @@ internal class FastAccessOrderedStringMapTest { fun removeTwoWithCollisions() { val first = Any() val second = Any() - val map = FastAccessOrderedStringMap(1) + val map = FastStampedStringMap(1) map.getElsePut("1") { first } map.getElsePut("2") { second } assertSame(first, map.removeEntry("1").value) @@ -130,7 +130,7 @@ internal class FastAccessOrderedStringMapTest { @Test fun removeThenSize() { val first = Any() - val map = FastAccessOrderedStringMap(1) + val map = FastStampedStringMap(1) map.getElsePut("1") { first } map.removeEntry("1") assertEquals(0, map.size) @@ -138,7 +138,7 @@ internal class FastAccessOrderedStringMapTest { @Test fun removeWhenEmpty() { - val map = FastAccessOrderedStringMap(1) + val map = FastStampedStringMap(1) assertThrows { map.removeEntry("1") } @@ -147,7 +147,7 @@ internal class FastAccessOrderedStringMapTest { @Test fun clear() { - val map = FastAccessOrderedStringMap(1) + val map = FastStampedStringMap(1) map.getElsePut("1") { Any() } assertEquals(1, map.size) map.clear() @@ -156,7 +156,7 @@ internal class FastAccessOrderedStringMapTest { @Test fun clearWhenEmpty() { - val map = FastAccessOrderedStringMap(1) + val map = FastStampedStringMap(1) map.clear() assertTrue(map.isEmpty()) } @@ -165,7 +165,7 @@ internal class FastAccessOrderedStringMapTest { fun containsFalse() { val supplier = mock<() -> Any>() whenever(supplier.invoke()) doReturn Any() - val map = FastAccessOrderedStringMap(1) + val map = FastStampedStringMap(1) map.getElsePut("1", supplier) assertFalse(map.containsKey("2")) } @@ -174,7 +174,7 @@ internal class FastAccessOrderedStringMapTest { fun containsTrue() { val supplier = mock<() -> Any>() whenever(supplier.invoke()) doReturn Any() - val map = FastAccessOrderedStringMap(1) + val map = FastStampedStringMap(1) map.getElsePut("1", supplier) assertTrue(map.containsKey("1")) } From 14168cdb45e376b9bab4a505a319dd7bb7558d8b Mon Sep 17 00:00:00 2001 From: "Kenneth J. Shackleton" Date: Fri, 21 Jun 2024 19:33:31 +0100 Subject: [PATCH 06/14] Remove redundant removal. --- .../com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt index 6c3f9bbda7..9ae7b46217 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt @@ -61,9 +61,7 @@ class FastLinkedStringMap( override fun clear() { super.clear() spare = null - head = null var entry = tail - tail = null while (entry != null) { val previous = entry.previous disposal(entry.unlink().value!!) From d01470f78b6121f38e2f0aaca9a7c74e904653da Mon Sep 17 00:00:00 2001 From: "Kenneth J. Shackleton" Date: Fri, 21 Jun 2024 19:41:45 +0100 Subject: [PATCH 07/14] Add method to convert to a linked map. --- .../collections/map/FastStampedStringMap.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt index ba478ad9a4..63d70e7bf6 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt @@ -65,6 +65,22 @@ class FastStampedStringMap( return currentStamp } + internal fun asLinkedMap( + maxSize: Int = size, + disposal: (T) -> Unit + ) = FastLinkedStringMap( + maxSize = maxSize, + capacity = maxSize, + accessOrder = true, + disposal = disposal + ).apply { + entries().sortedBy { + (it as StampedEntry).stamp + }.forEach { + addAssociation(it.index, it.hashCode, it.key, it.value!!) + } + } + private fun entries(): Iterable> = store.flatMap { sequence { var current = it From 25f45e67f94ce34a2d35b77cc32b56792b97c3ce Mon Sep 17 00:00:00 2001 From: "Kenneth J. Shackleton" Date: Sat, 22 Jun 2024 14:34:53 +0100 Subject: [PATCH 08/14] Optimistic LRU cache. --- ...enchmark.kt => CommonLruCacheBenchmark.kt} | 14 +- .../benchmarks/LinkedLruCacheBenchmark.kt | 51 +++++++ .../FastStampedStringMapBenchmark.kt | 4 +- .../com/bloomberg/selekt/SQLConnection.kt | 4 +- .../bloomberg/selekt/cache/CommonLruCache.kt | 78 ++++++++++ .../cache/{LruCache.kt => LinkedLruCache.kt} | 17 ++- .../bloomberg/selekt/cache/StampedCache.kt | 49 +++++++ .../collections/map/FastLinkedStringMap.kt | 22 ++- .../collections/map/FastStampedStringMap.kt | 38 ++--- .../selekt/collections/map/FastStringMap.kt | 10 ++ ...{LruCacheTest.kt => CommonLruCacheTest.kt} | 56 +++---- .../selekt/cache/LinkedLruCacheTest.kt | 137 ++++++++++++++++++ .../selekt/cache/StampedCacheTest.kt | 118 +++++++++++++++ .../map/FastLinkedStringMapTest.kt | 35 +---- .../map/FastStampedStringMapTest.kt | 55 +++---- 15 files changed, 553 insertions(+), 135 deletions(-) rename selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/{LruCacheBenchmark.kt => CommonLruCacheBenchmark.kt} (77%) create mode 100644 selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LinkedLruCacheBenchmark.kt create mode 100644 selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt rename selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/{LruCache.kt => LinkedLruCache.kt} (75%) create mode 100644 selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt rename selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/{LruCacheTest.kt => CommonLruCacheTest.kt} (64%) create mode 100644 selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/LinkedLruCacheTest.kt create mode 100644 selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/StampedCacheTest.kt diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LruCacheBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/CommonLruCacheBenchmark.kt similarity index 77% rename from selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LruCacheBenchmark.kt rename to selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/CommonLruCacheBenchmark.kt index b07b4fded8..711cee5d86 100644 --- a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LruCacheBenchmark.kt +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/CommonLruCacheBenchmark.kt @@ -16,7 +16,7 @@ package com.bloomberg.selekt.cache.benchmarks -import com.bloomberg.selekt.cache.LruCache +import com.bloomberg.selekt.cache.CommonLruCache import org.openjdk.jmh.annotations.Benchmark import org.openjdk.jmh.annotations.BenchmarkMode import org.openjdk.jmh.annotations.Level @@ -26,25 +26,25 @@ import org.openjdk.jmh.annotations.Setup import org.openjdk.jmh.annotations.State @State(Scope.Thread) -open class CacheInput { - internal lateinit var cache: LruCache +open class CommonCacheInput { + internal lateinit var cache: CommonLruCache @Setup(Level.Iteration) fun setUp() { - cache = LruCache(1) {} + cache = CommonLruCache(1) {} } } -open class LruCacheBenchmark { +open class CommonLruCacheBenchmark { @Benchmark @BenchmarkMode(Mode.Throughput) - fun getEntry(input: CacheInput) = input.cache.run { + fun getEntry(input: CommonCacheInput) = input.cache.run { get("1") {} } @Benchmark @BenchmarkMode(Mode.Throughput) - fun getEntryWithEviction(input: CacheInput) = input.cache.run { + fun getEntryWithEviction(input: CommonCacheInput) = input.cache.run { get("1") {} get("2") {} } diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LinkedLruCacheBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LinkedLruCacheBenchmark.kt new file mode 100644 index 0000000000..4d0ab99b0f --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LinkedLruCacheBenchmark.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.cache.benchmarks + +import com.bloomberg.selekt.cache.LinkedLruCache +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +open class LinkedCacheInput { + internal lateinit var cache: LinkedLruCache + + @Setup(Level.Iteration) + fun setUp() { + cache = LinkedLruCache(1) {} + } +} + +open class LinkedLruCacheBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntry(input: LinkedCacheInput) = input.cache.run { + get("1") {} + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntryWithEviction(input: LinkedCacheInput) = input.cache.run { + get("1") {} + get("2") {} + } +} diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStampedStringMapBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStampedStringMapBenchmark.kt index 6fb0f98fa2..cce9d1cc4a 100644 --- a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStampedStringMapBenchmark.kt +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStampedStringMapBenchmark.kt @@ -31,11 +31,11 @@ open class StampedMapInput { @Setup(Level.Iteration) fun setUp() { - map = FastStampedStringMap(1) + map = FastStampedStringMap(1) {} } } -open class FastLruStringMapBenchmark { +open class FastStampedStringMapBenchmark { @Benchmark @BenchmarkMode(Mode.Throughput) fun getEntry(input: StampedMapInput) = input.map.run { diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/SQLConnection.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/SQLConnection.kt index ec5f57e62a..08eb79bc30 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/SQLConnection.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/SQLConnection.kt @@ -16,7 +16,7 @@ package com.bloomberg.selekt -import com.bloomberg.selekt.cache.LruCache +import com.bloomberg.selekt.cache.CommonLruCache import com.bloomberg.selekt.commons.forEachByPosition import com.bloomberg.selekt.commons.forUntil import javax.annotation.concurrent.NotThreadSafe @@ -31,7 +31,7 @@ internal class SQLConnection( key: Key? ) : CloseableSQLExecutor { private val pointer = sqlite.open(path, flags) - private val preparedStatements = LruCache(configuration.maxSqlCacheSize) { + private val preparedStatements = CommonLruCache(configuration.maxSqlCacheSize) { it.close() pooledPreparedStatement = it } diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt new file mode 100644 index 0000000000..01542a5656 --- /dev/null +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.cache + +class CommonLruCache( + @PublishedApi + internal val maxSize: Int, + disposal: (T) -> Unit +) { + @PublishedApi + internal var cache: Any = StampedCache(maxSize, disposal) + + fun evict(key: String) { + when (val cache = cache) { + is StampedCache<*> -> cache.evict(key) + is LinkedLruCache<*> -> cache.evict(key) + else -> error("Unrecognized cache class: {}") + } + } + + fun evictAll() { + when (val cache = cache) { + is StampedCache<*> -> cache.evictAll() + is LinkedLruCache<*> -> cache.evictAll() + else -> error("Unrecognized cache class: {}") + } + } + + @Suppress("UNCHECKED_CAST") + inline fun get(key: String, supplier: () -> T): T = when (cache) { + is StampedCache<*> -> (cache as StampedCache).let { + it.get(key) { + supplier().also { value -> + if (it.shouldTransform()) { + // Adding another entry to the cache will necessitate the removal of the + // least recently used entry first to honour our maximum size constraint. + // For the implementation of the store currently assigned, this is an O(N) + // operation. We transform to an O(1) implementation. + transform() + (this@CommonLruCache.cache as LinkedLruCache).store.put(key, value) + } + } + } + } + is LinkedLruCache<*> -> (cache as LinkedLruCache).get(key, supplier) + else -> error("Unrecognized cache class: {}") + } + + fun containsKey(key: String) = when (val cache = cache) { + is StampedCache<*> -> cache.containsKey(key) + is LinkedLruCache<*> -> cache.containsKey(key) + else -> error("Unrecognized cache class: {}") + } + + @PublishedApi + internal fun StampedCache.shouldTransform() = (store.size >= maxSize) + + @PublishedApi + internal fun transform() { + (cache as StampedCache<*>).asLruCache().also { + cache = it + } + } +} diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LruCache.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LinkedLruCache.kt similarity index 75% rename from selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LruCache.kt rename to selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LinkedLruCache.kt index dcc8e47a51..4c8ccf0110 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LruCache.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LinkedLruCache.kt @@ -20,11 +20,22 @@ import com.bloomberg.selekt.collections.map.FastLinkedStringMap import javax.annotation.concurrent.NotThreadSafe @NotThreadSafe -class LruCache(maxSize: Int, disposal: (T) -> Unit) { +class LinkedLruCache( @PublishedApi @JvmField - @JvmSynthetic - internal val store = FastLinkedStringMap(maxSize, maxSize, false, disposal) + internal val maxSize: Int, + @PublishedApi + @JvmField + internal val store: FastLinkedStringMap +) { + constructor( + maxSize: Int, + disposal: (T) -> Unit + ) : this(maxSize, FastLinkedStringMap( + maxSize = maxSize, + disposal = disposal, + accessOrder = true + )) fun evict(key: String) { store.removeKey(key) diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt new file mode 100644 index 0000000000..e64f46c279 --- /dev/null +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.cache + +import com.bloomberg.selekt.collections.map.FastStampedStringMap +import javax.annotation.concurrent.NotThreadSafe + +@NotThreadSafe +class StampedCache( + capacity: Int, + @PublishedApi + @JvmField + internal val disposal: (T) -> Unit +) { + @PublishedApi + @JvmField + internal var store = FastStampedStringMap(capacity = capacity, disposal = disposal) + + fun evict(key: String) { + store.removeKey(key) + } + + fun evictAll() { + store.clear() + } + + inline fun get(key: String, supplier: () -> T): T = store.getElsePut(key, supplier) + + fun containsKey(key: String) = store.containsKey(key) + + internal fun asLruCache() = LinkedLruCache( + maxSize = store.size, + store = store.asLinkedMap(store.size, disposal) + ) +} diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt index 9ae7b46217..8b8ebf6805 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt @@ -48,12 +48,19 @@ class FastLinkedStringMap( } return it.value!! } - if (size >= maxSize) { - spare = removeLastEntry() - } return addAssociation(index, hashCode, key, supplier()).value!! } + @PublishedApi + internal fun put( + key: String, + value: T + ): T { + val hashCode = hash(key) + val index = hashIndex(hashCode) + return addAssociation(index, hashCode, key, value).value!! + } + fun removeKey(key: String) { disposal((super.removeEntry(key) as LinkedEntry).unlink().value!!) } @@ -110,8 +117,13 @@ class FastLinkedStringMap( hashCode: Int, key: String, value: T - ): Entry = (super.addAssociation(index, hashCode, key, value) as LinkedEntry).also { - putFirst(it) + ): Entry { + if (size >= maxSize) { + spare = removeLastEntry() + } + return (super.addAssociation(index, hashCode, key, value) as LinkedEntry).also { + putFirst(it) + } } override fun createEntry( diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt index 63d70e7bf6..66fb1df7f4 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt @@ -20,7 +20,8 @@ import javax.annotation.concurrent.NotThreadSafe @NotThreadSafe class FastStampedStringMap( - capacity: Int + capacity: Int, + private val disposal: (T) -> Unit ) : FastStringMap(capacity) { private var currentStamp = Int.MIN_VALUE private var spare: StampedEntry? = null @@ -51,9 +52,16 @@ class FastStampedStringMap( return StampedEntry(index, hashCode, key, value, nextStamp(), store[index]) } + fun removeKey(key: String) { + disposal(super.removeEntry(key).value!!) + } + override fun clear() { - super.clear() + entries().forEach { + disposal(it.value!!) + } spare = null + super.clear() } @PublishedApi @@ -74,34 +82,30 @@ class FastStampedStringMap( accessOrder = true, disposal = disposal ).apply { - entries().sortedBy { + this@FastStampedStringMap.entries().sortedBy { (it as StampedEntry).stamp }.forEach { addAssociation(it.index, it.hashCode, it.key, it.value!!) } } - private fun entries(): Iterable> = store.flatMap { - sequence { - var current = it - while (current != null) { - yield(current) - current = current.after - } - } - }.asIterable() - private fun resetAllStamps() { - entries().sortedBy { - (it as StampedEntry).stamp - }.run { + @Suppress("UNCHECKED_CAST") + (entries() as Iterable>).sortedBy(StampedEntry::stamp).run { currentStamp = Int.MIN_VALUE + maxOf(0, size - 1) forEachIndexed { index, it -> - (it as StampedEntry).stamp = Int.MIN_VALUE + index + it.stamp = Int.MIN_VALUE + index } } } + @Suppress("UNCHECKED_CAST") + @PublishedApi + internal fun removeLastEntry(): StampedEntry = (entries() as Iterable>) + .minBy(StampedEntry::stamp).let { + (removeEntry(it.key) as StampedEntry).apply { disposal(value!!) } + } + @PublishedApi internal class StampedEntry( index: Int, diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt index dd5e68492f..cfc7ea8f8c 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt @@ -95,6 +95,16 @@ open class FastStringMap(capacity: Int) { value: T ): Entry = Entry(index, hashCode, key, value, store[index]) + internal fun entries(): Iterable> = store.flatMap { + sequence { + var current = it + while (current != null) { + yield(current) + current = current.after + } + } + } + open fun clear() { store.fill(null) size = 0 diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/LruCacheTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/CommonLruCacheTest.kt similarity index 64% rename from selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/LruCacheTest.kt rename to selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/CommonLruCacheTest.kt index 5c84815ef2..48b2edc80b 100644 --- a/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/LruCacheTest.kt +++ b/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/CommonLruCacheTest.kt @@ -32,22 +32,23 @@ import kotlin.test.assertSame import kotlin.test.assertTrue import kotlin.test.fail -internal class LruCacheTest { +internal class CommonLruCacheTest { + private val first = Any() + private val second = Any() + private val supplier = mock<() -> Any>() + private val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + @Test fun get() { val first = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val cache = LruCache(1, disposal) + val cache = CommonLruCache(1, disposal) cache.get("1") { first } assertSame(first, cache.get("1") { fail() }) } @Test fun getTwo() { - val first = Any() - val second = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val cache = LruCache(2, disposal) + val cache = CommonLruCache(2, disposal) cache.get("1") { first } cache.get("2") { second } assertSame(first, cache.get("1") { fail() }) @@ -56,10 +57,7 @@ internal class LruCacheTest { @Test fun getAfterEvict() { - val first = Any() - val second = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val cache = LruCache(1, disposal) + val cache = CommonLruCache(1, disposal) cache.get("1") { first } cache.get("2") { second } assertFalse(cache.containsKey("1")) @@ -68,10 +66,7 @@ internal class LruCacheTest { @Test fun evict() { - val first = Any() - val second = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val cache = LruCache(2, disposal) + val cache = CommonLruCache(2, disposal) cache.get("1") { first } cache.get("2") { second } cache.evict("1") @@ -83,10 +78,7 @@ internal class LruCacheTest { @Test fun evictAll() { - val first = Any() - val second = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val cache = LruCache(2, disposal) + val cache = CommonLruCache(2, disposal) cache.get("1") { first } cache.get("2") { second } cache.evictAll() @@ -98,8 +90,7 @@ internal class LruCacheTest { @Test fun evictWhenEmpty() { - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val cache = LruCache(1, disposal) + val cache = CommonLruCache(1, disposal) assertThrows { cache.evict("1") } @@ -108,23 +99,22 @@ internal class LruCacheTest { @Test fun evictLeastRecentlyUsed() { - val first = Any() - val second = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val cache = LruCache(1, disposal) + val cache = CommonLruCache(2, disposal) + val third = Any() cache.get("1") { first } cache.get("2") { second } + cache.get("1") { fail() } + cache.get("3") { third } + assertFalse(cache.containsKey("2")) inOrder(disposal) { - verify(disposal, times(1)).invoke(same(first)) + verify(disposal, times(1)).invoke(same(second)) } } @Test fun getWhenAbsent() { - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val supplier = mock<() -> Any>() whenever(supplier.invoke()) doReturn Any() - val cache = LruCache(1, disposal) + val cache = CommonLruCache(1, disposal) val item = cache.get("1", supplier) verify(supplier, times(1)).invoke() assertSame(item, cache.get("1", supplier)) @@ -132,20 +122,16 @@ internal class LruCacheTest { @Test fun containsFalse() { - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val supplier = mock<() -> Any>() whenever(supplier.invoke()) doReturn Any() - val cache = LruCache(1, disposal) + val cache = CommonLruCache(1, disposal) cache.get("1", supplier) assertFalse(cache.containsKey("2")) } @Test fun containsTrue() { - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val supplier = mock<() -> Any>() whenever(supplier.invoke()) doReturn Any() - val cache = LruCache(1, disposal) + val cache = CommonLruCache(1, disposal) cache.get("1", supplier) assertTrue(cache.containsKey("1")) } diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/LinkedLruCacheTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/LinkedLruCacheTest.kt new file mode 100644 index 0000000000..0102172bba --- /dev/null +++ b/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/LinkedLruCacheTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2020 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.cache + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.inOrder +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.same +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue +import kotlin.test.fail + +internal class LinkedLruCacheTest { + private val first = Any() + private val second = Any() + private val supplier = mock<() -> Any>() + private val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + + @Test + fun get() { + val first = Any() + val cache = LinkedLruCache(1, disposal) + cache.get("1") { first } + assertSame(first, cache.get("1") { fail() }) + } + + @Test + fun getTwo() { + val cache = LinkedLruCache(2, disposal) + cache.get("1") { first } + cache.get("2") { second } + assertSame(first, cache.get("1") { fail() }) + assertSame(second, cache.get("2") { fail() }) + } + + @Test + fun getAfterEvict() { + val cache = LinkedLruCache(1, disposal) + cache.get("1") { first } + cache.get("2") { second } + assertFalse(cache.containsKey("1")) + assertSame(second, cache.get("2") { fail() }) + } + + @Test + fun evict() { + val cache = LinkedLruCache(2, disposal) + cache.get("1") { first } + cache.get("2") { second } + cache.evict("1") + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(first)) + } + assertSame(second, cache.get("2") { fail() }) + } + + @Test + fun evictAll() { + val cache = LinkedLruCache(2, disposal) + cache.get("1") { first } + cache.get("2") { second } + cache.evictAll() + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(first)) + verify(disposal, times(1)).invoke(same(second)) + } + } + + @Test + fun evictWhenEmpty() { + val cache = LinkedLruCache(1, disposal) + assertThrows { + cache.evict("1") + } + verify(disposal, never()).invoke(anyOrNull()) + } + + @Test + fun evictLeastRecentlyUsed() { + val cache = LinkedLruCache(2, disposal) + val third = Any() + cache.get("1") { first } + cache.get("2") { second } + cache.get("1") { fail() } + cache.get("3") { third } + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(second)) + } + } + + @Test + fun getWhenAbsent() { + whenever(supplier.invoke()) doReturn Any() + val cache = LinkedLruCache(1, disposal) + val item = cache.get("1", supplier) + verify(supplier, times(1)).invoke() + assertSame(item, cache.get("1", supplier)) + } + + @Test + fun containsFalse() { + whenever(supplier.invoke()) doReturn Any() + val cache = LinkedLruCache(1, disposal) + cache.get("1", supplier) + assertFalse(cache.containsKey("2")) + } + + @Test + fun containsTrue() { + whenever(supplier.invoke()) doReturn Any() + val cache = LinkedLruCache(1, disposal) + cache.get("1", supplier) + assertTrue(cache.containsKey("1")) + } +} diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/StampedCacheTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/StampedCacheTest.kt new file mode 100644 index 0000000000..64ec827d91 --- /dev/null +++ b/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/StampedCacheTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2020 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.cache + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.inOrder +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.same +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue +import kotlin.test.fail + +internal class StampedCacheTest { + private val first = Any() + private val second = Any() + private val supplier = mock<() -> Any>() + private val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + + @Test + fun get() { + val first = Any() + val cache = StampedCache(1, disposal) + cache.get("1") { first } + assertSame(first, cache.get("1") { fail() }) + } + + @Test + fun getTwo() { + val cache = StampedCache(2, disposal) + cache.get("1") { first } + cache.get("2") { second } + assertSame(first, cache.get("1") { fail() }) + assertSame(second, cache.get("2") { fail() }) + } + + @Test + fun evict() { + val cache = StampedCache(2, disposal) + cache.get("1") { first } + cache.get("2") { second } + cache.evict("1") + assertFalse(cache.containsKey("1")) + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(first)) + } + assertSame(second, cache.get("2") { fail() }) + } + + @Test + fun evictAll() { + val cache = StampedCache(2, disposal) + cache.get("1") { first } + cache.get("2") { second } + cache.evictAll() + assertFalse(cache.containsKey("1")) + assertFalse(cache.containsKey("2")) + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(second)) + verify(disposal, times(1)).invoke(same(first)) + } + } + + @Test + fun evictWhenEmpty() { + val cache = StampedCache(1, disposal) + assertThrows { + cache.evict("1") + } + verify(disposal, never()).invoke(anyOrNull()) + } + + @Test + fun getWhenAbsent() { + whenever(supplier.invoke()) doReturn Any() + val cache = StampedCache(1, disposal) + val item = cache.get("1", supplier) + verify(supplier, times(1)).invoke() + assertSame(item, cache.get("1", supplier)) + } + + @Test + fun containsFalse() { + whenever(supplier.invoke()) doReturn Any() + val cache = StampedCache(1, disposal) + cache.get("1", supplier) + assertFalse(cache.containsKey("2")) + } + + @Test + fun containsTrue() { + whenever(supplier.invoke()) doReturn Any() + val cache = StampedCache(1, disposal) + cache.get("1", supplier) + assertTrue(cache.containsKey("1")) + } +} diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMapTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMapTest.kt index bf7a6de421..efd8e89869 100644 --- a/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMapTest.kt +++ b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMapTest.kt @@ -34,19 +34,19 @@ import kotlin.test.assertTrue import kotlin.test.fail internal class FastLinkedStringMapTest { + private val first = Any() + private val second = Any() + private val supplier = mock<() -> Any>() + private val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + @Test fun get() { - val first = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } val map = FastLinkedStringMap(1, 1, false, disposal) assertSame(first, map.getElsePut("1") { first }) } @Test fun getTwo() { - val first = Any() - val second = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } val map = FastLinkedStringMap(2, 64, false, disposal) map.getElsePut("1") { first } map.getElsePut("2") { second } @@ -56,9 +56,6 @@ internal class FastLinkedStringMapTest { @Test fun getAfterEvict() { - val first = Any() - val second = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } val map = FastLinkedStringMap(1, 1, false, disposal) map.getElsePut("1") { first } map.getElsePut("2") { second } @@ -68,9 +65,6 @@ internal class FastLinkedStringMapTest { @Test fun remove() { - val first = Any() - val second = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } val map = FastLinkedStringMap(2, 64, false, disposal) map.getElsePut("1") { first } map.getElsePut("2") { second } @@ -83,9 +77,6 @@ internal class FastLinkedStringMapTest { @Test fun clear() { - val first = Any() - val second = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } val map = FastLinkedStringMap(2, 64, false, disposal) map.getElsePut("1") { first } map.getElsePut("2") { second } @@ -98,7 +89,6 @@ internal class FastLinkedStringMapTest { @Test fun removeWhenEmpty() { - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } val map = FastLinkedStringMap(1, 1, false, disposal) assertThrows { map.removeKey("1") @@ -108,9 +98,6 @@ internal class FastLinkedStringMapTest { @Test fun removeLastEntryAccessed() { - val first = Any() - val second = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } val map = FastLinkedStringMap(2, 2, true, disposal) map.getElsePut("1") { first } map.getElsePut("2") { second } @@ -123,9 +110,6 @@ internal class FastLinkedStringMapTest { @Test fun removeLastEntryInserted() { - val first = Any() - val second = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } val map = FastLinkedStringMap(2, 2, false, disposal) map.getElsePut("1") { first } map.getElsePut("2") { second } @@ -138,9 +122,6 @@ internal class FastLinkedStringMapTest { @Test fun evictLeastRecentlyUsed() { - val first = Any() - val second = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } val map = FastLinkedStringMap(1, 1, false, disposal) map.getElsePut("1") { first } map.getElsePut("2") { second } @@ -151,8 +132,6 @@ internal class FastLinkedStringMapTest { @Test fun getWhenAbsent() { - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val supplier = mock<() -> Any>() whenever(supplier.invoke()) doReturn Any() val map = FastLinkedStringMap(1, 1, false, disposal) val item = map.getElsePut("1", supplier) @@ -162,8 +141,6 @@ internal class FastLinkedStringMapTest { @Test fun containsFalse() { - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val supplier = mock<() -> Any>() whenever(supplier.invoke()) doReturn Any() val map = FastLinkedStringMap(1, 1, false, disposal) map.getElsePut("1", supplier) @@ -172,8 +149,6 @@ internal class FastLinkedStringMapTest { @Test fun containsTrue() { - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val supplier = mock<() -> Any>() whenever(supplier.invoke()) doReturn Any() val map = FastLinkedStringMap(1, 1, false, disposal) map.getElsePut("1", supplier) diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMapTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMapTest.kt index c0cb7fe458..398332d533 100644 --- a/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMapTest.kt +++ b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMapTest.kt @@ -31,34 +31,35 @@ import kotlin.test.assertTrue import kotlin.test.fail internal class FastStampedStringMapTest { + private val first = Any() + private val second = Any() + private val supplier = mock<() -> Any>() + private val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + @Test fun get() { - val first = Any() - val map = FastStampedStringMap(1) + val map = FastStampedStringMap(1, disposal) assertSame(first, map.getElsePut("1") { first }) } @Test fun sizeOne() { - val first = Any() - val map = FastStampedStringMap(1) + val map = FastStampedStringMap(1, disposal) map.getElsePut("1") { first } assertEquals(1, map.size) } @Test fun getTwice() { - val first = Any() - val map = FastStampedStringMap(1) + val map = FastStampedStringMap(1, disposal) map.getElsePut("1") { first } assertSame(first, map.getElsePut("1") { fail() }) } @Test fun getWhenAbsent() { - val supplier = mock<() -> Any>() whenever(supplier.invoke()) doReturn Any() - val map = FastStampedStringMap(1) + val map = FastStampedStringMap(1, disposal) val item = map.getElsePut("1", supplier) verify(supplier, times(1)).invoke() assertSame(item, map.getElsePut("1", supplier)) @@ -67,9 +68,7 @@ internal class FastStampedStringMapTest { @Test fun getTwo() { - val first = Any() - val second = Any() - val map = FastStampedStringMap(64) + val map = FastStampedStringMap(64, disposal) map.getElsePut("1") { first } map.getElsePut("2") { second } assertEquals(2, map.size) @@ -77,9 +76,7 @@ internal class FastStampedStringMapTest { @Test fun getTwoWithCollisions() { - val first = Any() - val second = Any() - val map = FastStampedStringMap(1) + val map = FastStampedStringMap(1, disposal) map.getElsePut("1") { first } map.getElsePut("2") { second } assertSame(first, map.getElsePut("1") { fail() }) @@ -88,9 +85,7 @@ internal class FastStampedStringMapTest { @Test fun sizeTwo() { - val first = Any() - val second = Any() - val map = FastStampedStringMap(1) + val map = FastStampedStringMap(1, disposal) map.getElsePut("1") { first } map.getElsePut("2") { second } assertSame(first, map.getElsePut("1") { fail() }) @@ -99,17 +94,14 @@ internal class FastStampedStringMapTest { @Test fun removeOne() { - val first = Any() - val map = FastStampedStringMap(1) + val map = FastStampedStringMap(1, disposal) map.getElsePut("1") { first } assertSame(first, map.removeEntry("1").value) } @Test fun removeTwo() { - val first = Any() - val second = Any() - val map = FastStampedStringMap(2) + val map = FastStampedStringMap(2, disposal) map.getElsePut("1") { first } map.getElsePut("2") { second } assertSame(first, map.removeEntry("1").value) @@ -118,9 +110,7 @@ internal class FastStampedStringMapTest { @Test fun removeTwoWithCollisions() { - val first = Any() - val second = Any() - val map = FastStampedStringMap(1) + val map = FastStampedStringMap(1, disposal) map.getElsePut("1") { first } map.getElsePut("2") { second } assertSame(first, map.removeEntry("1").value) @@ -129,8 +119,7 @@ internal class FastStampedStringMapTest { @Test fun removeThenSize() { - val first = Any() - val map = FastStampedStringMap(1) + val map = FastStampedStringMap(1, disposal) map.getElsePut("1") { first } map.removeEntry("1") assertEquals(0, map.size) @@ -138,7 +127,7 @@ internal class FastStampedStringMapTest { @Test fun removeWhenEmpty() { - val map = FastStampedStringMap(1) + val map = FastStampedStringMap(1, disposal) assertThrows { map.removeEntry("1") } @@ -147,7 +136,7 @@ internal class FastStampedStringMapTest { @Test fun clear() { - val map = FastStampedStringMap(1) + val map = FastStampedStringMap(1, disposal) map.getElsePut("1") { Any() } assertEquals(1, map.size) map.clear() @@ -156,25 +145,23 @@ internal class FastStampedStringMapTest { @Test fun clearWhenEmpty() { - val map = FastStampedStringMap(1) + val map = FastStampedStringMap(1, disposal) map.clear() assertTrue(map.isEmpty()) } @Test fun containsFalse() { - val supplier = mock<() -> Any>() whenever(supplier.invoke()) doReturn Any() - val map = FastStampedStringMap(1) + val map = FastStampedStringMap(1, disposal) map.getElsePut("1", supplier) assertFalse(map.containsKey("2")) } @Test fun containsTrue() { - val supplier = mock<() -> Any>() whenever(supplier.invoke()) doReturn Any() - val map = FastStampedStringMap(1) + val map = FastStampedStringMap(1, disposal) map.getElsePut("1", supplier) assertTrue(map.containsKey("1")) } From 5a57eee81cd32e4d8dc05df4e3257d781d76fd11 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Shackleton" Date: Sat, 22 Jun 2024 16:36:26 +0100 Subject: [PATCH 09/14] Try nullable. --- .../benchmarks/LinkedLruCacheBenchmark.kt | 9 ++++ .../cache/benchmarks/StampedCacheBenchmark.kt | 51 +++++++++++++++++++ .../bloomberg/selekt/cache/CommonLruCache.kt | 48 +++++++++-------- .../selekt/cache/CommonLruCacheTest.kt | 7 ++- 4 files changed, 92 insertions(+), 23 deletions(-) create mode 100644 selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/StampedCacheBenchmark.kt diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LinkedLruCacheBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LinkedLruCacheBenchmark.kt index 4d0ab99b0f..be0093f9d9 100644 --- a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LinkedLruCacheBenchmark.kt +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LinkedLruCacheBenchmark.kt @@ -28,10 +28,12 @@ import org.openjdk.jmh.annotations.State @State(Scope.Thread) open class LinkedCacheInput { internal lateinit var cache: LinkedLruCache + internal lateinit var largeCache: LinkedLruCache @Setup(Level.Iteration) fun setUp() { cache = LinkedLruCache(1) {} + largeCache = LinkedLruCache(64) {} } } @@ -48,4 +50,11 @@ open class LinkedLruCacheBenchmark { get("1") {} get("2") {} } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntries(input: LinkedCacheInput) = input.largeCache.run { + get("1") { "" } + get("2") { "" } + } } diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/StampedCacheBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/StampedCacheBenchmark.kt new file mode 100644 index 0000000000..91d5176ebe --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/StampedCacheBenchmark.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.cache.benchmarks + +import com.bloomberg.selekt.cache.StampedCache +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +open class StampedCacheInput { + internal lateinit var cache: StampedCache + + @Setup(Level.Iteration) + fun setUp() { + cache = StampedCache(1) {} + } +} + +open class StampedCacheBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntry(input: StampedCacheInput) = input.cache.run { + get("1") {} + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntryWithEviction(input: StampedCacheInput) = input.cache.run { + get("1") {} + get("2") {} + } +} diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt index 01542a5656..4b64769e0b 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt @@ -18,32 +18,36 @@ package com.bloomberg.selekt.cache class CommonLruCache( @PublishedApi + @JvmField internal val maxSize: Int, disposal: (T) -> Unit ) { @PublishedApi - internal var cache: Any = StampedCache(maxSize, disposal) + @JvmField + internal var cache: StampedCache? = StampedCache(maxSize, disposal) + @PublishedApi + @JvmField + internal var linkedCache: LinkedLruCache? = null fun evict(key: String) { - when (val cache = cache) { - is StampedCache<*> -> cache.evict(key) - is LinkedLruCache<*> -> cache.evict(key) - else -> error("Unrecognized cache class: {}") + cache?.let { + it.evict(key) + return } + linkedCache!!.evict(key) } fun evictAll() { - when (val cache = cache) { - is StampedCache<*> -> cache.evictAll() - is LinkedLruCache<*> -> cache.evictAll() - else -> error("Unrecognized cache class: {}") + cache?.let { + it.evictAll() + return } + linkedCache!!.evictAll() } - @Suppress("UNCHECKED_CAST") - inline fun get(key: String, supplier: () -> T): T = when (cache) { - is StampedCache<*> -> (cache as StampedCache).let { - it.get(key) { + inline fun get(key: String, supplier: () -> T): T { + cache?.let { + return it.get(key) { supplier().also { value -> if (it.shouldTransform()) { // Adding another entry to the cache will necessitate the removal of the @@ -51,19 +55,19 @@ class CommonLruCache( // For the implementation of the store currently assigned, this is an O(N) // operation. We transform to an O(1) implementation. transform() - (this@CommonLruCache.cache as LinkedLruCache).store.put(key, value) + linkedCache!!.store.put(key, value) } } } } - is LinkedLruCache<*> -> (cache as LinkedLruCache).get(key, supplier) - else -> error("Unrecognized cache class: {}") + return linkedCache!!.get(key, supplier) } - fun containsKey(key: String) = when (val cache = cache) { - is StampedCache<*> -> cache.containsKey(key) - is LinkedLruCache<*> -> cache.containsKey(key) - else -> error("Unrecognized cache class: {}") + fun containsKey(key: String): Boolean { + cache?.let { + return it.containsKey(key) + } + return linkedCache!!.containsKey(key) } @PublishedApi @@ -71,8 +75,8 @@ class CommonLruCache( @PublishedApi internal fun transform() { - (cache as StampedCache<*>).asLruCache().also { - cache = it + linkedCache = cache!!.asLruCache().also { + cache = null } } } diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/CommonLruCacheTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/CommonLruCacheTest.kt index 48b2edc80b..c4792c1eb9 100644 --- a/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/CommonLruCacheTest.kt +++ b/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/CommonLruCacheTest.kt @@ -16,6 +16,7 @@ package com.bloomberg.selekt.cache +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.mockito.kotlin.anyOrNull @@ -28,6 +29,7 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.assertSame import kotlin.test.assertTrue import kotlin.test.fail @@ -44,6 +46,7 @@ internal class CommonLruCacheTest { val cache = CommonLruCache(1, disposal) cache.get("1") { first } assertSame(first, cache.get("1") { fail() }) + assertNotNull(cache.cache) } @Test @@ -53,6 +56,7 @@ internal class CommonLruCacheTest { cache.get("2") { second } assertSame(first, cache.get("1") { fail() }) assertSame(second, cache.get("2") { fail() }) + assertNotNull(cache.cache) } @Test @@ -62,6 +66,7 @@ internal class CommonLruCacheTest { cache.get("2") { second } assertFalse(cache.containsKey("1")) assertSame(second, cache.get("2") { fail() }) + assertNull(cache.cache) } @Test @@ -83,8 +88,8 @@ internal class CommonLruCacheTest { cache.get("2") { second } cache.evictAll() inOrder(disposal) { - verify(disposal, times(1)).invoke(same(first)) verify(disposal, times(1)).invoke(same(second)) + verify(disposal, times(1)).invoke(same(first)) } } From ca60178ed308cdfbf5e3b035ad4674823d25b3a2 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Shackleton" Date: Sat, 22 Jun 2024 17:09:53 +0100 Subject: [PATCH 10/14] Simpler get. --- .../bloomberg/selekt/cache/CommonLruCache.kt | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt index 4b64769e0b..ac15e06428 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt @@ -46,21 +46,18 @@ class CommonLruCache( } inline fun get(key: String, supplier: () -> T): T { - cache?.let { - return it.get(key) { - supplier().also { value -> - if (it.shouldTransform()) { - // Adding another entry to the cache will necessitate the removal of the - // least recently used entry first to honour our maximum size constraint. - // For the implementation of the store currently assigned, this is an O(N) - // operation. We transform to an O(1) implementation. - transform() - linkedCache!!.store.put(key, value) - } + return cache?.get(key) { + supplier().also { value -> + if (cache!!.shouldTransform()) { + // Adding another entry to the cache will necessitate the removal of the + // least recently used entry first to honour our maximum size constraint. + // For the implementation of the store currently assigned, this is an O(N) + // operation. We transform to an O(1) implementation. + transform() + linkedCache!!.store.put(key, value) } } - } - return linkedCache!!.get(key, supplier) + } ?: linkedCache!!.get(key, supplier) } fun containsKey(key: String): Boolean { From 822321e285fb8d15f57f2390b593e51f928879a9 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Shackleton" Date: Sat, 22 Jun 2024 17:19:01 +0100 Subject: [PATCH 11/14] More large cache benchmarks. --- .../benchmarks/CommonLruCacheBenchmark.kt | 34 +++++++++++++++++++ .../benchmarks/LinkedLruCacheBenchmark.kt | 25 ++++++++++++++ .../cache/benchmarks/StampedCacheBenchmark.kt | 34 +++++++++++++++++++ .../FastLinkedStringMapBenchmark.kt | 25 ++++++++++++++ .../FastStampedStringMapBenchmark.kt | 27 +++++++++++++++ .../jdk/benchmarks/LinkedHashMapBenchmark.kt | 25 ++++++++++++++ 6 files changed, 170 insertions(+) diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/CommonLruCacheBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/CommonLruCacheBenchmark.kt index 711cee5d86..fd1f0e503c 100644 --- a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/CommonLruCacheBenchmark.kt +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/CommonLruCacheBenchmark.kt @@ -28,10 +28,12 @@ import org.openjdk.jmh.annotations.State @State(Scope.Thread) open class CommonCacheInput { internal lateinit var cache: CommonLruCache + internal lateinit var largeCache: CommonLruCache @Setup(Level.Iteration) fun setUp() { cache = CommonLruCache(1) {} + largeCache = CommonLruCache(64) {} } } @@ -48,4 +50,36 @@ open class CommonLruCacheBenchmark { get("1") {} get("2") {} } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntries(input: CommonCacheInput) = input.largeCache.run { + get("1") { "" } + get("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getManyEntries(input: CommonCacheInput) = input.largeCache.run { + get("0") { "" } + get("1") { "" } + get("2") { "" } + get("3") { "" } + get("4") { "" } + get("5") { "" } + get("6") { "" } + get("7") { "" } + get("8") { "" } + get("9") { "" } + get("2") { "" } + get("3") { "" } + get("9") { "" } + get("4") { "" } + get("5") { "" } + get("0") { "" } + get("8") { "" } + get("6") { "" } + get("1") { "" } + get("7") { "" } + } } diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LinkedLruCacheBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LinkedLruCacheBenchmark.kt index be0093f9d9..12c75f3114 100644 --- a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LinkedLruCacheBenchmark.kt +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LinkedLruCacheBenchmark.kt @@ -57,4 +57,29 @@ open class LinkedLruCacheBenchmark { get("1") { "" } get("2") { "" } } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getManyEntries(input: LinkedCacheInput) = input.largeCache.run { + get("0") { "" } + get("1") { "" } + get("2") { "" } + get("3") { "" } + get("4") { "" } + get("5") { "" } + get("6") { "" } + get("7") { "" } + get("8") { "" } + get("9") { "" } + get("2") { "" } + get("3") { "" } + get("9") { "" } + get("4") { "" } + get("5") { "" } + get("0") { "" } + get("8") { "" } + get("6") { "" } + get("1") { "" } + get("7") { "" } + } } diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/StampedCacheBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/StampedCacheBenchmark.kt index 91d5176ebe..6ac71c3ca6 100644 --- a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/StampedCacheBenchmark.kt +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/StampedCacheBenchmark.kt @@ -28,10 +28,12 @@ import org.openjdk.jmh.annotations.State @State(Scope.Thread) open class StampedCacheInput { internal lateinit var cache: StampedCache + internal lateinit var largeCache: StampedCache @Setup(Level.Iteration) fun setUp() { cache = StampedCache(1) {} + largeCache = StampedCache(64) {} } } @@ -48,4 +50,36 @@ open class StampedCacheBenchmark { get("1") {} get("2") {} } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntries(input: StampedCacheInput) = input.largeCache.run { + get("1") { "" } + get("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getManyEntries(input: StampedCacheInput) = input.largeCache.run { + get("0") { "" } + get("1") { "" } + get("2") { "" } + get("3") { "" } + get("4") { "" } + get("5") { "" } + get("6") { "" } + get("7") { "" } + get("8") { "" } + get("9") { "" } + get("2") { "" } + get("3") { "" } + get("9") { "" } + get("4") { "" } + get("5") { "" } + get("0") { "" } + get("8") { "" } + get("6") { "" } + get("1") { "" } + get("7") { "" } + } } diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLinkedStringMapBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLinkedStringMapBenchmark.kt index 89e0e9ea7f..bbf52a22a3 100644 --- a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLinkedStringMapBenchmark.kt +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLinkedStringMapBenchmark.kt @@ -94,4 +94,29 @@ open class FastLinkedStringMapBenchmark { getElsePut("1") { "" } removeKey("1") } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getManyEntriesAccessOrder(input: LinkedMapInput) = input.largeAccessMap.run { + getEntryElsePut("0") { "" } + getEntryElsePut("1") { "" } + getEntryElsePut("2") { "" } + getEntryElsePut("3") { "" } + getEntryElsePut("4") { "" } + getEntryElsePut("5") { "" } + getEntryElsePut("6") { "" } + getEntryElsePut("7") { "" } + getEntryElsePut("8") { "" } + getEntryElsePut("9") { "" } + getEntryElsePut("2") { "" } + getEntryElsePut("3") { "" } + getEntryElsePut("9") { "" } + getEntryElsePut("4") { "" } + getEntryElsePut("5") { "" } + getEntryElsePut("0") { "" } + getEntryElsePut("8") { "" } + getEntryElsePut("6") { "" } + getEntryElsePut("1") { "" } + getEntryElsePut("7") { "" } + } } diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStampedStringMapBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStampedStringMapBenchmark.kt index cce9d1cc4a..32b4e166e8 100644 --- a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStampedStringMapBenchmark.kt +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStampedStringMapBenchmark.kt @@ -28,10 +28,12 @@ import org.openjdk.jmh.annotations.State @State(Scope.Thread) open class StampedMapInput { internal lateinit var map: FastStampedStringMap + internal lateinit var largeMap: FastStampedStringMap @Setup(Level.Iteration) fun setUp() { map = FastStampedStringMap(1) {} + largeMap = FastStampedStringMap(64) {} } } @@ -55,4 +57,29 @@ open class FastStampedStringMapBenchmark { getEntryElsePut("1") { "" } removeEntry("1") } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getManyEntries(input: StampedMapInput) = input.largeMap.run { + getEntryElsePut("0") { "" } + getEntryElsePut("1") { "" } + getEntryElsePut("2") { "" } + getEntryElsePut("3") { "" } + getEntryElsePut("4") { "" } + getEntryElsePut("5") { "" } + getEntryElsePut("6") { "" } + getEntryElsePut("7") { "" } + getEntryElsePut("8") { "" } + getEntryElsePut("9") { "" } + getEntryElsePut("2") { "" } + getEntryElsePut("3") { "" } + getEntryElsePut("9") { "" } + getEntryElsePut("4") { "" } + getEntryElsePut("5") { "" } + getEntryElsePut("0") { "" } + getEntryElsePut("8") { "" } + getEntryElsePut("6") { "" } + getEntryElsePut("1") { "" } + getEntryElsePut("7") { "" } + } } diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/LinkedHashMapBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/LinkedHashMapBenchmark.kt index 9b4d3ec210..fd9b715f2d 100644 --- a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/LinkedHashMapBenchmark.kt +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/LinkedHashMapBenchmark.kt @@ -72,6 +72,31 @@ open class LinkedHashMapBenchmark { getOrPut("2") { "" } } + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getManyEntriesAccessOrder(input: LinkedHashMapInput) = input.largeAccessOrderMap.run { + getOrPut("0") { "" } + getOrPut("1") { "" } + getOrPut("2") { "" } + getOrPut("3") { "" } + getOrPut("4") { "" } + getOrPut("5") { "" } + getOrPut("6") { "" } + getOrPut("7") { "" } + getOrPut("8") { "" } + getOrPut("9") { "" } + getOrPut("2") { "" } + getOrPut("3") { "" } + getOrPut("9") { "" } + getOrPut("4") { "" } + getOrPut("5") { "" } + getOrPut("0") { "" } + getOrPut("8") { "" } + getOrPut("6") { "" } + getOrPut("1") { "" } + getOrPut("7") { "" } + } + @Benchmark @BenchmarkMode(Mode.Throughput) fun getEntryWithRemoval(input: LinkedHashMapInput) = input.smallMap.run { From 41ac56cd6dafb4a442e1827e9718b83bf4ca92c3 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Shackleton" Date: Sat, 22 Jun 2024 18:20:23 +0100 Subject: [PATCH 12/14] StampedCache.store should be final. --- .../src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt index e64f46c279..c3dfb46144 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt @@ -28,7 +28,7 @@ class StampedCache( ) { @PublishedApi @JvmField - internal var store = FastStampedStringMap(capacity = capacity, disposal = disposal) + internal val store = FastStampedStringMap(capacity = capacity, disposal = disposal) fun evict(key: String) { store.removeKey(key) From a8dfdeba4c3099c26279c254f32e2e9352d17b66 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Shackleton" Date: Tue, 25 Jun 2024 08:09:05 +0100 Subject: [PATCH 13/14] Platform not null. --- buildSrc/src/main/kotlin/SelektExtensions.kt | 4 +- .../bloomberg/selekt/cache/CommonLruCache.kt | 12 ++--- .../bloomberg/selekt/cache/LinkedLruCache.kt | 10 ++-- .../bloomberg/selekt/cache/StampedCache.kt | 6 +-- .../collections/map/FastLinkedStringMap.kt | 48 +++++++++---------- .../collections/map/FastStampedStringMap.kt | 30 ++++++------ .../selekt/collections/map/FastStringMap.kt | 30 ++++++------ 7 files changed, 70 insertions(+), 70 deletions(-) diff --git a/buildSrc/src/main/kotlin/SelektExtensions.kt b/buildSrc/src/main/kotlin/SelektExtensions.kt index cc33f4b812..ecdf291aea 100644 --- a/buildSrc/src/main/kotlin/SelektExtensions.kt +++ b/buildSrc/src/main/kotlin/SelektExtensions.kt @@ -62,8 +62,8 @@ fun Project.disableKotlinCompilerAssertions() { kotlinOptions { freeCompilerArgs = listOf( "-Xno-call-assertions", - "-Xno-receiver-assertions", - "-Xno-param-assertions" + "-Xno-param-assertions", + "-Xno-receiver-assertions" ) } } diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt index ac15e06428..916aec3529 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt @@ -16,18 +16,18 @@ package com.bloomberg.selekt.cache -class CommonLruCache( +class CommonLruCache( @PublishedApi @JvmField internal val maxSize: Int, - disposal: (T) -> Unit + disposal: (T & Any) -> Unit ) { @PublishedApi @JvmField - internal var cache: StampedCache? = StampedCache(maxSize, disposal) + internal var cache: StampedCache? = StampedCache(maxSize, disposal) @PublishedApi @JvmField - internal var linkedCache: LinkedLruCache? = null + internal var linkedCache: LinkedLruCache? = null fun evict(key: String) { cache?.let { @@ -45,7 +45,7 @@ class CommonLruCache( linkedCache!!.evictAll() } - inline fun get(key: String, supplier: () -> T): T { + inline fun get(key: String, supplier: () -> T & Any): T & Any { return cache?.get(key) { supplier().also { value -> if (cache!!.shouldTransform()) { @@ -68,7 +68,7 @@ class CommonLruCache( } @PublishedApi - internal fun StampedCache.shouldTransform() = (store.size >= maxSize) + internal fun StampedCache.shouldTransform() = (store.size >= maxSize) @PublishedApi internal fun transform() { diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LinkedLruCache.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LinkedLruCache.kt index 4c8ccf0110..d831612663 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LinkedLruCache.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LinkedLruCache.kt @@ -20,18 +20,18 @@ import com.bloomberg.selekt.collections.map.FastLinkedStringMap import javax.annotation.concurrent.NotThreadSafe @NotThreadSafe -class LinkedLruCache( +class LinkedLruCache( @PublishedApi @JvmField internal val maxSize: Int, @PublishedApi @JvmField - internal val store: FastLinkedStringMap + internal val store: FastLinkedStringMap ) { constructor( maxSize: Int, - disposal: (T) -> Unit - ) : this(maxSize, FastLinkedStringMap( + disposal: (T & Any) -> Unit + ) : this(maxSize, FastLinkedStringMap( maxSize = maxSize, disposal = disposal, accessOrder = true @@ -45,7 +45,7 @@ class LinkedLruCache( store.clear() } - inline fun get(key: String, supplier: () -> T): T = store.getElsePut(key, supplier) + inline fun get(key: String, supplier: () -> T & Any): T & Any = store.getElsePut(key, supplier) fun containsKey(key: String) = store.containsKey(key) } diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt index c3dfb46144..f20b5bf0bf 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt @@ -20,7 +20,7 @@ import com.bloomberg.selekt.collections.map.FastStampedStringMap import javax.annotation.concurrent.NotThreadSafe @NotThreadSafe -class StampedCache( +class StampedCache( capacity: Int, @PublishedApi @JvmField @@ -38,11 +38,11 @@ class StampedCache( store.clear() } - inline fun get(key: String, supplier: () -> T): T = store.getElsePut(key, supplier) + inline fun get(key: String, supplier: () -> T & Any): T = store.getElsePut(key, supplier) fun containsKey(key: String) = store.containsKey(key) - internal fun asLruCache() = LinkedLruCache( + internal fun asLruCache() = LinkedLruCache( maxSize = store.size, store = store.asLinkedMap(store.size, disposal) ) diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt index 8b8ebf6805..e23ce95142 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt @@ -27,24 +27,24 @@ class FastLinkedStringMap( @PublishedApi @JvmField internal val accessOrder: Boolean = false, - private val disposal: (T) -> Unit + private val disposal: (T & Any) -> Unit ) : FastStringMap(capacity) { - private var head: LinkedEntry? = null - private var tail: LinkedEntry? = null + private var head: LinkedEntry? = null + private var tail: LinkedEntry? = null @PublishedApi @JvmField - internal var spare: LinkedEntry? = null + internal var spare: LinkedEntry? = null inline fun getElsePut( key: String, - supplier: () -> T - ): T { + supplier: () -> T & Any + ): T & Any { val hashCode = hash(key) val index = hashIndex(hashCode) entryMatching(index, hashCode, key)?.let { if (accessOrder) { - putFirst(it as LinkedEntry) + putFirst(it as LinkedEntry) } return it.value!! } @@ -54,15 +54,15 @@ class FastLinkedStringMap( @PublishedApi internal fun put( key: String, - value: T - ): T { + value: T & Any + ): T & Any { val hashCode = hash(key) val index = hashIndex(hashCode) return addAssociation(index, hashCode, key, value).value!! } fun removeKey(key: String) { - disposal((super.removeEntry(key) as LinkedEntry).unlink().value!!) + disposal((super.removeEntry(key) as LinkedEntry).unlink().value!!) } override fun clear() { @@ -78,7 +78,7 @@ class FastLinkedStringMap( } } - private fun LinkedEntry.unlink(): Entry = apply { + private fun LinkedEntry.unlink(): Entry = apply { previous?.let { it.next = next } next?.let { it.previous = previous } if (this === head) { @@ -93,7 +93,7 @@ class FastLinkedStringMap( @PublishedApi @JvmSynthetic - internal fun putFirst(node: LinkedEntry): Unit = node.run { + internal fun putFirst(node: LinkedEntry): Unit = node.run { if (this === head) { return } @@ -116,12 +116,12 @@ class FastLinkedStringMap( index: Int, hashCode: Int, key: String, - value: T - ): Entry { + value: T & Any + ): Entry { if (size >= maxSize) { spare = removeLastEntry() } - return (super.addAssociation(index, hashCode, key, value) as LinkedEntry).also { + return (super.addAssociation(index, hashCode, key, value) as LinkedEntry).also { putFirst(it) } } @@ -130,8 +130,8 @@ class FastLinkedStringMap( index: Int, hashCode: Int, key: String, - value: T - ): Entry { + value: T & Any + ): Entry { spare?.let { spare = null return it.update(index, hashCode, key, value, store[index]) @@ -141,7 +141,7 @@ class FastLinkedStringMap( @PublishedApi @JvmSynthetic - internal fun removeLastEntry(): LinkedEntry = tail!!.apply { + internal fun removeLastEntry(): LinkedEntry = tail!!.apply { previous?.let { it.next = null } ?: run { head = null } tail = previous previous = null @@ -156,11 +156,11 @@ class FastLinkedStringMap( index: Int, hashCode: Int, key: String, - value: T, - after: Entry? - ) : Entry(index, hashCode, key, value, after) { + value: T & Any, + after: Entry? + ) : Entry(index, hashCode, key, value, after) { @JvmField - var previous: LinkedEntry? = null + var previous: LinkedEntry? = null @JvmField var next: LinkedEntry? = null @@ -170,8 +170,8 @@ class FastLinkedStringMap( index: Int, hashCode: Int, key: String, - value: T, - after: Entry? + value: T & Any, + after: Entry? ) = apply { this.index = index this.hashCode = hashCode diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt index 66fb1df7f4..afe174ffc2 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt @@ -21,19 +21,19 @@ import javax.annotation.concurrent.NotThreadSafe @NotThreadSafe class FastStampedStringMap( capacity: Int, - private val disposal: (T) -> Unit + private val disposal: (T & Any) -> Unit ) : FastStringMap(capacity) { private var currentStamp = Int.MIN_VALUE - private var spare: StampedEntry? = null + private var spare: StampedEntry? = null inline fun getElsePut( key: String, - supplier: () -> T - ): T { + supplier: () -> T & Any + ): T & Any { val hashCode = hash(key) val index = hashIndex(hashCode) entryMatching(index, hashCode, key)?.let { - (it as StampedEntry).stamp = nextStamp() + (it as StampedEntry).stamp = nextStamp() return it.value!! } return addAssociation(index, hashCode, key, supplier()).value!! @@ -43,8 +43,8 @@ class FastStampedStringMap( index: Int, hashCode: Int, key: String, - value: T - ): Entry { + value: T & Any + ): Entry { spare?.let { spare = null return it.update(index, hashCode, key, value, nextStamp(), store[index]) @@ -75,15 +75,15 @@ class FastStampedStringMap( internal fun asLinkedMap( maxSize: Int = size, - disposal: (T) -> Unit - ) = FastLinkedStringMap( + disposal: (T & Any) -> Unit + ) = FastLinkedStringMap( maxSize = maxSize, capacity = maxSize, accessOrder = true, disposal = disposal ).apply { this@FastStampedStringMap.entries().sortedBy { - (it as StampedEntry).stamp + (it as StampedEntry).stamp }.forEach { addAssociation(it.index, it.hashCode, it.key, it.value!!) } @@ -111,18 +111,18 @@ class FastStampedStringMap( index: Int, hashCode: Int, key: String, - value: T, + value: T & Any, var stamp: Int, - after: Entry? - ) : Entry(index, hashCode, key, value, after) { + after: Entry? + ) : Entry(index, hashCode, key, value, after) { @Suppress("NOTHING_TO_INLINE") inline fun update( index: Int, hashCode: Int, key: String, - value: T, + value: T & Any, stamp: Int, - after: Entry? + after: Entry? ) = apply { this.index = index this.hashCode = hashCode diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt index cfc7ea8f8c..6f96860d82 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt @@ -28,7 +28,7 @@ open class FastStringMap(capacity: Int) { @JvmField @PublishedApi - internal val store = arrayOfNulls>(capacity) + internal val store = arrayOfNulls>(capacity) private val hashLimit = capacity - 1 fun isEmpty() = 0 == size @@ -48,8 +48,8 @@ open class FastStringMap(capacity: Int) { inline fun getEntryElsePut( key: String, - supplier: () -> T - ): Entry { + supplier: () -> T & Any + ): Entry { val hashCode = hash(key) val index = hashIndex(hashCode) var entry = store[index] @@ -62,11 +62,11 @@ open class FastStringMap(capacity: Int) { return addAssociation(index, hashCode, key, supplier()) } - fun removeEntry(key: String): Entry { + fun removeEntry(key: String): Entry { val hashCode = hash(key) val index = hashIndex(hashCode) var entry = store[index] - var previous: Entry? = null + var previous: Entry? = null while (entry != null) { if (entry.hashCode == hashCode && entry.key == key) { return removeAssociation(entry, previous) @@ -82,8 +82,8 @@ open class FastStringMap(capacity: Int) { index: Int, hashCode: Int, key: String, - value: T - ): Entry = createEntry(index, hashCode, key, value).also { + value: T & Any + ): Entry = createEntry(index, hashCode, key, value).also { store[index] = it size += 1 } @@ -92,10 +92,10 @@ open class FastStringMap(capacity: Int) { index: Int, hashCode: Int, key: String, - value: T - ): Entry = Entry(index, hashCode, key, value, store[index]) + value: T & Any + ): Entry = Entry(index, hashCode, key, value, store[index]) - internal fun entries(): Iterable> = store.flatMap { + internal fun entries(): Iterable> = store.flatMap { sequence { var current = it while (current != null) { @@ -112,7 +112,7 @@ open class FastStringMap(capacity: Int) { @Suppress("NOTHING_TO_INLINE") @PublishedApi - internal inline fun entryMatching(index: Int, hashCode: Int, key: String): Entry? { + internal inline fun entryMatching(index: Int, hashCode: Int, key: String): Entry? { var entry = store[index] while (entry != null) { if (entry.hashCode == hashCode && entry.key == key) { @@ -124,9 +124,9 @@ open class FastStringMap(capacity: Int) { } private fun removeAssociation( - entry: Entry, - previousEntry: Entry? - ): Entry { + entry: Entry, + previousEntry: Entry? + ): Entry { if (previousEntry == null) { store[entry.index] = entry.after } else { @@ -152,7 +152,7 @@ open class FastStringMap(capacity: Int) { @JvmField var value: T?, @JvmField - var after: Entry? + var after: Entry? ) { internal fun reset(): T? = value.also { _ -> key = "" From 63e36f47042e3ab9d70c138ae0a8a4bb21045e6c Mon Sep 17 00:00:00 2001 From: "Kenneth J. Shackleton" Date: Sat, 29 Jun 2024 09:44:46 +0100 Subject: [PATCH 14/14] Replace & Any. --- .../bloomberg/selekt/cache/CommonLruCache.kt | 12 ++--- .../bloomberg/selekt/cache/LinkedLruCache.kt | 10 ++-- .../bloomberg/selekt/cache/StampedCache.kt | 6 +-- .../collections/map/FastLinkedStringMap.kt | 50 +++++++++---------- .../collections/map/FastStampedStringMap.kt | 32 ++++++------ .../selekt/collections/map/FastStringMap.kt | 32 ++++++------ 6 files changed, 71 insertions(+), 71 deletions(-) diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt index 916aec3529..ac15e06428 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt @@ -16,18 +16,18 @@ package com.bloomberg.selekt.cache -class CommonLruCache( +class CommonLruCache( @PublishedApi @JvmField internal val maxSize: Int, - disposal: (T & Any) -> Unit + disposal: (T) -> Unit ) { @PublishedApi @JvmField - internal var cache: StampedCache? = StampedCache(maxSize, disposal) + internal var cache: StampedCache? = StampedCache(maxSize, disposal) @PublishedApi @JvmField - internal var linkedCache: LinkedLruCache? = null + internal var linkedCache: LinkedLruCache? = null fun evict(key: String) { cache?.let { @@ -45,7 +45,7 @@ class CommonLruCache( linkedCache!!.evictAll() } - inline fun get(key: String, supplier: () -> T & Any): T & Any { + inline fun get(key: String, supplier: () -> T): T { return cache?.get(key) { supplier().also { value -> if (cache!!.shouldTransform()) { @@ -68,7 +68,7 @@ class CommonLruCache( } @PublishedApi - internal fun StampedCache.shouldTransform() = (store.size >= maxSize) + internal fun StampedCache.shouldTransform() = (store.size >= maxSize) @PublishedApi internal fun transform() { diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LinkedLruCache.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LinkedLruCache.kt index d831612663..02146cd3eb 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LinkedLruCache.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LinkedLruCache.kt @@ -20,18 +20,18 @@ import com.bloomberg.selekt.collections.map.FastLinkedStringMap import javax.annotation.concurrent.NotThreadSafe @NotThreadSafe -class LinkedLruCache( +class LinkedLruCache( @PublishedApi @JvmField internal val maxSize: Int, @PublishedApi @JvmField - internal val store: FastLinkedStringMap + internal val store: FastLinkedStringMap ) { constructor( maxSize: Int, - disposal: (T & Any) -> Unit - ) : this(maxSize, FastLinkedStringMap( + disposal: (T) -> Unit + ) : this(maxSize, FastLinkedStringMap( maxSize = maxSize, disposal = disposal, accessOrder = true @@ -45,7 +45,7 @@ class LinkedLruCache( store.clear() } - inline fun get(key: String, supplier: () -> T & Any): T & Any = store.getElsePut(key, supplier) + inline fun get(key: String, supplier: () -> T): T = store.getElsePut(key, supplier) fun containsKey(key: String) = store.containsKey(key) } diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt index f20b5bf0bf..c3dfb46144 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt @@ -20,7 +20,7 @@ import com.bloomberg.selekt.collections.map.FastStampedStringMap import javax.annotation.concurrent.NotThreadSafe @NotThreadSafe -class StampedCache( +class StampedCache( capacity: Int, @PublishedApi @JvmField @@ -38,11 +38,11 @@ class StampedCache( store.clear() } - inline fun get(key: String, supplier: () -> T & Any): T = store.getElsePut(key, supplier) + inline fun get(key: String, supplier: () -> T): T = store.getElsePut(key, supplier) fun containsKey(key: String) = store.containsKey(key) - internal fun asLruCache() = LinkedLruCache( + internal fun asLruCache() = LinkedLruCache( maxSize = store.size, store = store.asLinkedMap(store.size, disposal) ) diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt index e23ce95142..a3d3b69b79 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt @@ -19,7 +19,7 @@ package com.bloomberg.selekt.collections.map import javax.annotation.concurrent.NotThreadSafe @NotThreadSafe -class FastLinkedStringMap( +class FastLinkedStringMap( @PublishedApi @JvmField internal val maxSize: Int, @@ -27,24 +27,24 @@ class FastLinkedStringMap( @PublishedApi @JvmField internal val accessOrder: Boolean = false, - private val disposal: (T & Any) -> Unit + private val disposal: (T) -> Unit ) : FastStringMap(capacity) { - private var head: LinkedEntry? = null - private var tail: LinkedEntry? = null + private var head: LinkedEntry? = null + private var tail: LinkedEntry? = null @PublishedApi @JvmField - internal var spare: LinkedEntry? = null + internal var spare: LinkedEntry? = null inline fun getElsePut( key: String, - supplier: () -> T & Any - ): T & Any { + supplier: () -> T + ): T { val hashCode = hash(key) val index = hashIndex(hashCode) entryMatching(index, hashCode, key)?.let { if (accessOrder) { - putFirst(it as LinkedEntry) + putFirst(it as LinkedEntry) } return it.value!! } @@ -54,15 +54,15 @@ class FastLinkedStringMap( @PublishedApi internal fun put( key: String, - value: T & Any - ): T & Any { + value: T + ): T { val hashCode = hash(key) val index = hashIndex(hashCode) return addAssociation(index, hashCode, key, value).value!! } fun removeKey(key: String) { - disposal((super.removeEntry(key) as LinkedEntry).unlink().value!!) + disposal((super.removeEntry(key) as LinkedEntry).unlink().value!!) } override fun clear() { @@ -78,7 +78,7 @@ class FastLinkedStringMap( } } - private fun LinkedEntry.unlink(): Entry = apply { + private fun LinkedEntry.unlink(): Entry = apply { previous?.let { it.next = next } next?.let { it.previous = previous } if (this === head) { @@ -93,7 +93,7 @@ class FastLinkedStringMap( @PublishedApi @JvmSynthetic - internal fun putFirst(node: LinkedEntry): Unit = node.run { + internal fun putFirst(node: LinkedEntry): Unit = node.run { if (this === head) { return } @@ -116,12 +116,12 @@ class FastLinkedStringMap( index: Int, hashCode: Int, key: String, - value: T & Any - ): Entry { + value: T + ): Entry { if (size >= maxSize) { spare = removeLastEntry() } - return (super.addAssociation(index, hashCode, key, value) as LinkedEntry).also { + return (super.addAssociation(index, hashCode, key, value) as LinkedEntry).also { putFirst(it) } } @@ -130,8 +130,8 @@ class FastLinkedStringMap( index: Int, hashCode: Int, key: String, - value: T & Any - ): Entry { + value: T + ): Entry { spare?.let { spare = null return it.update(index, hashCode, key, value, store[index]) @@ -141,7 +141,7 @@ class FastLinkedStringMap( @PublishedApi @JvmSynthetic - internal fun removeLastEntry(): LinkedEntry = tail!!.apply { + internal fun removeLastEntry(): LinkedEntry = tail!!.apply { previous?.let { it.next = null } ?: run { head = null } tail = previous previous = null @@ -156,11 +156,11 @@ class FastLinkedStringMap( index: Int, hashCode: Int, key: String, - value: T & Any, - after: Entry? - ) : Entry(index, hashCode, key, value, after) { + value: T, + after: Entry? + ) : Entry(index, hashCode, key, value, after) { @JvmField - var previous: LinkedEntry? = null + var previous: LinkedEntry? = null @JvmField var next: LinkedEntry? = null @@ -170,8 +170,8 @@ class FastLinkedStringMap( index: Int, hashCode: Int, key: String, - value: T & Any, - after: Entry? + value: T, + after: Entry? ) = apply { this.index = index this.hashCode = hashCode diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt index afe174ffc2..5568c3075b 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt @@ -19,21 +19,21 @@ package com.bloomberg.selekt.collections.map import javax.annotation.concurrent.NotThreadSafe @NotThreadSafe -class FastStampedStringMap( +class FastStampedStringMap( capacity: Int, - private val disposal: (T & Any) -> Unit + private val disposal: (T) -> Unit ) : FastStringMap(capacity) { private var currentStamp = Int.MIN_VALUE - private var spare: StampedEntry? = null + private var spare: StampedEntry? = null inline fun getElsePut( key: String, - supplier: () -> T & Any - ): T & Any { + supplier: () -> T + ): T { val hashCode = hash(key) val index = hashIndex(hashCode) entryMatching(index, hashCode, key)?.let { - (it as StampedEntry).stamp = nextStamp() + (it as StampedEntry).stamp = nextStamp() return it.value!! } return addAssociation(index, hashCode, key, supplier()).value!! @@ -43,8 +43,8 @@ class FastStampedStringMap( index: Int, hashCode: Int, key: String, - value: T & Any - ): Entry { + value: T + ): Entry { spare?.let { spare = null return it.update(index, hashCode, key, value, nextStamp(), store[index]) @@ -75,15 +75,15 @@ class FastStampedStringMap( internal fun asLinkedMap( maxSize: Int = size, - disposal: (T & Any) -> Unit - ) = FastLinkedStringMap( + disposal: (T) -> Unit + ) = FastLinkedStringMap( maxSize = maxSize, capacity = maxSize, accessOrder = true, disposal = disposal ).apply { this@FastStampedStringMap.entries().sortedBy { - (it as StampedEntry).stamp + (it as StampedEntry).stamp }.forEach { addAssociation(it.index, it.hashCode, it.key, it.value!!) } @@ -111,18 +111,18 @@ class FastStampedStringMap( index: Int, hashCode: Int, key: String, - value: T & Any, + value: T, var stamp: Int, - after: Entry? - ) : Entry(index, hashCode, key, value, after) { + after: Entry? + ) : Entry(index, hashCode, key, value, after) { @Suppress("NOTHING_TO_INLINE") inline fun update( index: Int, hashCode: Int, key: String, - value: T & Any, + value: T, stamp: Int, - after: Entry? + after: Entry? ) = apply { this.index = index this.hashCode = hashCode diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt index 6f96860d82..667d50909e 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt @@ -22,13 +22,13 @@ import javax.annotation.concurrent.NotThreadSafe * @param capacity a power of two. */ @NotThreadSafe -open class FastStringMap(capacity: Int) { +open class FastStringMap(capacity: Int) { @JvmField var size: Int = 0 @JvmField @PublishedApi - internal val store = arrayOfNulls>(capacity) + internal val store = arrayOfNulls>(capacity) private val hashLimit = capacity - 1 fun isEmpty() = 0 == size @@ -48,8 +48,8 @@ open class FastStringMap(capacity: Int) { inline fun getEntryElsePut( key: String, - supplier: () -> T & Any - ): Entry { + supplier: () -> T + ): Entry { val hashCode = hash(key) val index = hashIndex(hashCode) var entry = store[index] @@ -62,11 +62,11 @@ open class FastStringMap(capacity: Int) { return addAssociation(index, hashCode, key, supplier()) } - fun removeEntry(key: String): Entry { + fun removeEntry(key: String): Entry { val hashCode = hash(key) val index = hashIndex(hashCode) var entry = store[index] - var previous: Entry? = null + var previous: Entry? = null while (entry != null) { if (entry.hashCode == hashCode && entry.key == key) { return removeAssociation(entry, previous) @@ -82,8 +82,8 @@ open class FastStringMap(capacity: Int) { index: Int, hashCode: Int, key: String, - value: T & Any - ): Entry = createEntry(index, hashCode, key, value).also { + value: T + ): Entry = createEntry(index, hashCode, key, value).also { store[index] = it size += 1 } @@ -92,10 +92,10 @@ open class FastStringMap(capacity: Int) { index: Int, hashCode: Int, key: String, - value: T & Any - ): Entry = Entry(index, hashCode, key, value, store[index]) + value: T + ): Entry = Entry(index, hashCode, key, value, store[index]) - internal fun entries(): Iterable> = store.flatMap { + internal fun entries(): Iterable> = store.flatMap { sequence { var current = it while (current != null) { @@ -112,7 +112,7 @@ open class FastStringMap(capacity: Int) { @Suppress("NOTHING_TO_INLINE") @PublishedApi - internal inline fun entryMatching(index: Int, hashCode: Int, key: String): Entry? { + internal inline fun entryMatching(index: Int, hashCode: Int, key: String): Entry? { var entry = store[index] while (entry != null) { if (entry.hashCode == hashCode && entry.key == key) { @@ -124,9 +124,9 @@ open class FastStringMap(capacity: Int) { } private fun removeAssociation( - entry: Entry, - previousEntry: Entry? - ): Entry { + entry: Entry, + previousEntry: Entry? + ): Entry { if (previousEntry == null) { store[entry.index] = entry.after } else { @@ -152,7 +152,7 @@ open class FastStringMap(capacity: Int) { @JvmField var value: T?, @JvmField - var after: Entry? + var after: Entry? ) { internal fun reset(): T? = value.also { _ -> key = ""