Skip to content

Commit

Permalink
Optimistic LRU cache.
Browse files Browse the repository at this point in the history
  • Loading branch information
kennethshackleton committed Jun 22, 2024
1 parent d01470f commit 25f45e6
Show file tree
Hide file tree
Showing 15 changed files with 553 additions and 135 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Any>
open class CommonCacheInput {
internal lateinit var cache: CommonLruCache<Any>

@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") {}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Any>

@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") {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,7 +31,7 @@ internal class SQLConnection(
key: Key?
) : CloseableSQLExecutor {
private val pointer = sqlite.open(path, flags)
private val preparedStatements = LruCache<SQLPreparedStatement>(configuration.maxSqlCacheSize) {
private val preparedStatements = CommonLruCache<SQLPreparedStatement>(configuration.maxSqlCacheSize) {
it.close()
pooledPreparedStatement = it
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T : Any>(
@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<T>).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<T>).store.put(key, value)
}
}
}
}
is LinkedLruCache<*> -> (cache as LinkedLruCache<T>).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<T>.shouldTransform() = (store.size >= maxSize)

@PublishedApi
internal fun transform() {
(cache as StampedCache<*>).asLruCache().also {
cache = it
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,22 @@ import com.bloomberg.selekt.collections.map.FastLinkedStringMap
import javax.annotation.concurrent.NotThreadSafe

@NotThreadSafe
class LruCache<T : Any>(maxSize: Int, disposal: (T) -> Unit) {
class LinkedLruCache<T : Any>(
@PublishedApi
@JvmField
@JvmSynthetic
internal val store = FastLinkedStringMap(maxSize, maxSize, false, disposal)
internal val maxSize: Int,
@PublishedApi
@JvmField
internal val store: FastLinkedStringMap<T>
) {
constructor(
maxSize: Int,
disposal: (T) -> Unit
) : this(maxSize, FastLinkedStringMap(
maxSize = maxSize,
disposal = disposal,
accessOrder = true
))

fun evict(key: String) {
store.removeKey(key)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T : Any>(
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)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,19 @@ class FastLinkedStringMap<T>(
}
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<T>).unlink().value!!)
}
Expand Down Expand Up @@ -110,8 +117,13 @@ class FastLinkedStringMap<T>(
hashCode: Int,
key: String,
value: T
): Entry<T> = (super.addAssociation(index, hashCode, key, value) as LinkedEntry<T>).also {
putFirst(it)
): Entry<T> {
if (size >= maxSize) {
spare = removeLastEntry()
}
return (super.addAssociation(index, hashCode, key, value) as LinkedEntry<T>).also {
putFirst(it)
}
}

override fun createEntry(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import javax.annotation.concurrent.NotThreadSafe

@NotThreadSafe
class FastStampedStringMap<T>(
capacity: Int
capacity: Int,
private val disposal: (T) -> Unit
) : FastStringMap<T>(capacity) {
private var currentStamp = Int.MIN_VALUE
private var spare: StampedEntry<T>? = null
Expand Down Expand Up @@ -51,9 +52,16 @@ class FastStampedStringMap<T>(
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
Expand All @@ -74,34 +82,30 @@ class FastStampedStringMap<T>(
accessOrder = true,
disposal = disposal
).apply {
entries().sortedBy {
this@FastStampedStringMap.entries().sortedBy {
(it as StampedEntry<T>).stamp
}.forEach {
addAssociation(it.index, it.hashCode, it.key, it.value!!)
}
}

private fun entries(): Iterable<Entry<T>> = store.flatMap {
sequence {
var current = it
while (current != null) {
yield(current)
current = current.after
}
}
}.asIterable()

private fun resetAllStamps() {
entries().sortedBy {
(it as StampedEntry<T>).stamp
}.run {
@Suppress("UNCHECKED_CAST")
(entries() as Iterable<StampedEntry<T>>).sortedBy(StampedEntry<T>::stamp).run {
currentStamp = Int.MIN_VALUE + maxOf(0, size - 1)
forEachIndexed { index, it ->
(it as StampedEntry<T>).stamp = Int.MIN_VALUE + index
it.stamp = Int.MIN_VALUE + index
}
}
}

@Suppress("UNCHECKED_CAST")
@PublishedApi
internal fun removeLastEntry(): StampedEntry<T> = (entries() as Iterable<StampedEntry<T>>)
.minBy(StampedEntry<T>::stamp).let {
(removeEntry(it.key) as StampedEntry<T>).apply { disposal(value!!) }
}

@PublishedApi
internal class StampedEntry<T>(
index: Int,
Expand Down
Loading

0 comments on commit 25f45e6

Please sign in to comment.