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..5f4f68ab63 --- /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.length == key.length && 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")) + } +}