updated LRU cache to run with complexity O(1)

why:
  to be used in Clique consensus protocol which suggests 4k cache entries.
  the previous implementation used OrderTable[] which has complexity O(n)
  for deleting entries.
This commit is contained in:
Jordan Hrycaj 2021-05-21 19:44:30 +01:00 committed by Jordan Hrycaj
parent a5e0bb6139
commit 1965ea027b
1 changed files with 99 additions and 23 deletions

View File

@ -11,7 +11,10 @@
## Hash as hash can: LRU cache ## Hash as hash can: LRU cache
## =========================== ## ===========================
## ##
## provide last-recently-used cache mapper ## Provide last-recently-used cache data structure. The implementation
## works with complexity O(1) using a nim hash Table with doubly linked
## data entries.
##
const const
# debugging, enable with: nim c -r -d:noisy:3 ... # debugging, enable with: nim c -r -d:noisy:3 ...
@ -32,9 +35,14 @@ type
LruValue*[T,V,E] = ## derive an LRU value from function argument LruValue*[T,V,E] = ## derive an LRU value from function argument
proc(arg: T): Result[V,E] {.gcsafe, raises: [Defect,CatchableError].} proc(arg: T): Result[V,E] {.gcsafe, raises: [Defect,CatchableError].}
LruItem[K,V] = tuple
prv, nxt: K ## doubly linked items
value: V
LruCache*[T,K,V,E] = object LruCache*[T,K,V,E] = object
maxItems: int ## max number of entries maxItems: int ## max number of entries
tab: OrderedTable[K,V] ## cache data table tab: Table[K,LruItem[K,V]] ## cache data table
first, last: K ## doubly linked item list queue
toKey: LruKey[T,K] toKey: LruKey[T,K]
toValue: LruValue[T,V,E] toValue: LruValue[T,V,E]
@ -59,31 +67,52 @@ proc getLruItem*[T,K,V,E](cache: var LruCache[T,K,V,E]; arg: T): Result[V,E] =
## Return `toValue(arg)`, preferably from result cached earlier ## Return `toValue(arg)`, preferably from result cached earlier
let key = cache.toKey(arg) let key = cache.toKey(arg)
# Get the cache if already generated, marking it as recently used # Relink item if already in the cache => move to last position
if cache.tab.hasKey(key): if cache.tab.hasKey(key):
let value = cache.tab[key] let lruItem = cache.tab[key]
# Pop and append at end. Note that according to manual, this
# costs O(n) => inefficient
cache.tab.del(key)
cache.tab[key] = value
return ok(value)
# Return unless OK if key == cache.last:
# Nothing to do
return ok(lruItem.value)
# Unlink key Item
if key == cache.first:
cache.first = lruItem.nxt
else:
cache.tab[lruItem.prv].nxt = lruItem.nxt
cache.tab[lruItem.nxt].prv = lruItem.prv
# Append key item
cache.tab[cache.last].nxt = key
cache.tab[key].prv = cache.last
cache.last = key
return ok(lruItem.value)
# Calculate value, pass through error unless OK
let rcValue = ? cache.toValue(arg) let rcValue = ? cache.toValue(arg)
# Limit mumer of cached items # Limit number of cached items
if cache.maxItems <= cache.tab.len: if cache.maxItems <= cache.tab.len:
# Delete oldest/first entry # Delete oldest/first entry
var tbd: K var nextKey = cache.tab[cache.first].nxt
# Kludge: OrderedTable[] still misses a proper API. cache.tab.del(cache.first)
for key in cache.tab.keys: cache.first = nextKey
# Tests suggest that deleting here also works in that particular case.
tbd = key
break
cache.tab.del(tbd)
# Add cache entry # Add cache entry
cache.tab[key] = rcValue var tabItem: LruItem[K,V]
# Initialise empty queue
if cache.tab.len == 0:
cache.first = key
cache.last = key
else:
# Append queue item
cache.tab[cache.last].nxt = key
tabItem.prv = cache.last
cache.last = key
tabItem.value = rcValue
cache.tab[key] = tabItem
result = ok(rcValue) result = ok(rcValue)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -93,7 +122,7 @@ proc getLruItem*[T,K,V,E](cache: var LruCache[T,K,V,E]; arg: T): Result[V,E] =
when isMainModule and isMainOK: when isMainModule and isMainOK:
import import
sequtils strformat
const const
cacheLimit = 10 cacheLimit = 10
@ -118,17 +147,64 @@ when isMainModule and isMainOK:
cache.initLruCache(getKey, getValue, cacheLimit) cache.initLruCache(getKey, getValue, cacheLimit)
proc verifyLinks[T,K,V,E](cache: var LruCache[T,K,V,E]) =
var key = cache.first
if cache.tab.len == 1:
doAssert cache.tab.hasKey(key)
doAssert key == cache.last
elif 1 < cache.tab.len:
# forward links
for n in 1 ..< cache.tab.len:
var curKey = key
key = cache.tab[curKey].nxt
if cache.tab[key].prv != curKey:
echo &">>> ({n}): " &
&"cache.tab[{key}].prv == {cache.tab[key].prv} exp {curKey}"
doAssert cache.tab[key].prv == curKey
doAssert key == cache.last
# backward links
for n in 1 ..< cache.tab.len:
var curKey = key
key = cache.tab[curKey].prv
if cache.tab[key].nxt != curKey:
echo &">>> ({n}): " &
&"cache.tab[{key}].nxt == {cache.tab[key].nxt} exp {curKey}"
doAssert cache.tab[key].nxt == curKey
doAssert key == cache.first
proc toKeyList[T,K,V,E](cache: var LruCache[T,K,V,E]): seq[K] =
cache.verifyLinks
if 0 < cache.tab.len:
var key = cache.first
while key != cache.last:
result.add key
key = cache.tab[key].nxt
result.add cache.last
proc toValueList[T,K,V,E](cache: var LruCache[T,K,V,E]): seq[V] =
cache.verifyLinks
if 0 < cache.tab.len:
var key = cache.first
while key != cache.last:
result.add cache.tab[key].value
key = cache.tab[key].nxt
result.add cache.tab[cache.last].value
var lastQ: seq[int] var lastQ: seq[int]
for w in keyList: for w in keyList:
var var
key = w mod 13 key = w mod 13
reSched = cache.tab.hasKey(key) reSched = cache.tab.hasKey(key)
value = cache.getLruItem(key) value = cache.getLruItem(key)
queue = toSeq(cache.tab.keys) queue = cache.toKeyList
values = cache.toValueList
# verfy key/value pairs
for n in 0 ..< queue.len:
doAssert $queue[n] == $values[n]
if reSched: if reSched:
echo "+++ rotate ", value, " => ", queue echo &"+++ rotate {value} => {queue}"
else: else:
echo "*** append ", value, " => ", queue echo &"*** append {value} => {queue}"
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# End # End