Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic rework, double implementation #5

Merged
merged 5 commits into from
May 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.13
go-version: ^1.18

- name: Check out code into the Go module directory
uses: actions/checkout@v2
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ This will retrieve the library.

### Usage

Note: this package also has a non-generic `tinylru.LRU` implementation.

```go
// Create an LRU cache
var cache tinylru.LRU
var cache tinylru.LRUG[string, string]

// Set the cache size. This is the maximum number of items that the cache can
// hold before evicting old items. The default size is 256.
Expand All @@ -33,7 +35,7 @@ prev, ok := cache.Set("hello", "world")
value, ok := cache.Get("hello")

// Delete a key. Returns the deleted value and ok if a previous value exists.
prev, ok := tr.Delete("hello")
prev, ok := cache.Delete("hello")
```

A `Set` function may evict old items when adding a new item while LRU is at
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/tidwall/tinylru

go 1.14
go 1.18
204 changes: 204 additions & 0 deletions lrug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// Copyright 2020 Joshua J Baker. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.

package tinylru

import "sync"

type lrugItem[Key comparable, Value any] struct {
key Key // user-defined key
value Value // user-defined value
prev *lrugItem[Key, Value] // prev item in list. More recently used
next *lrugItem[Key, Value] // next item in list. Less recently used
}

// LRUG implements an LRU cache
type LRUG[Key comparable, Value any] struct {
mu sync.RWMutex // protect all things
size int // max number of items.
items map[Key]*lrugItem[Key, Value] // active items
head *lrugItem[Key, Value] // head of list
tail *lrugItem[Key, Value] // tail of list
}

//go:noinline
func (lru *LRUG[Key, Value]) init() {
lru.items = make(map[Key]*lrugItem[Key, Value])
lru.head = new(lrugItem[Key, Value])
lru.tail = new(lrugItem[Key, Value])
lru.head.next = lru.tail
lru.tail.prev = lru.head
if lru.size == 0 {
lru.size = DefaultSize
}
}

func (lru *LRUG[Key, Value]) evict() *lrugItem[Key, Value] {
item := lru.tail.prev
lru.pop(item)
delete(lru.items, item.key)
return item
}

func (lru *LRUG[Key, Value]) pop(item *lrugItem[Key, Value]) {
item.prev.next = item.next
item.next.prev = item.prev
}

func (lru *LRUG[Key, Value]) push(item *lrugItem[Key, Value]) {
lru.head.next.prev = item
item.next = lru.head.next
item.prev = lru.head
lru.head.next = item
}

// Resize sets the maximum size of an LRU cache. If this value is less than
// the number of items currently in the cache, then items will be evicted.
// Returns evicted items.
// This operation will panic if the size is less than one.
func (lru *LRUG[Key, Value]) Resize(size int) (evictedKeys []Key,
evictedValues []Value) {
if size <= 0 {
panic("invalid size")
}

lru.mu.Lock()
defer lru.mu.Unlock()
for size < len(lru.items) {
item := lru.evict()
evictedKeys = append(evictedKeys, item.key)
evictedValues = append(evictedValues, item.value)
}
lru.size = size
return evictedKeys, evictedValues
}

// Len returns the length of the lru cache
func (lru *LRUG[Key, Value]) Len() int {
lru.mu.RLock()
defer lru.mu.RUnlock()
return len(lru.items)
}

// SetEvicted sets or replaces a value for a key. If this operation causes an
// eviction then the evicted item is returned.
func (lru *LRUG[Key, Value]) SetEvicted(key Key, value Value) (
prev Value, replaced bool, evictedKey Key,
evictedValue Value, evicted bool) {
lru.mu.Lock()
defer lru.mu.Unlock()
if lru.items == nil {
lru.init()
}
item := lru.items[key]
if item == nil {
if len(lru.items) == lru.size {
item = lru.evict()
evictedKey, evictedValue, evicted = item.key, item.value, true
} else {
item = new(lrugItem[Key, Value])
}
item.key = key
item.value = value
lru.push(item)
lru.items[key] = item
} else {
prev, replaced = item.value, true
item.value = value
if lru.head.next != item {
lru.pop(item)
lru.push(item)
}
}
return prev, replaced, evictedKey, evictedValue, evicted
}

// Set or replace a value for a key.
func (lru *LRUG[Key, Value]) Set(key Key, value Value) (prev Value,
replaced bool) {
prev, replaced, _, _, _ = lru.SetEvicted(key, value)
return prev, replaced
}

// Get a value for key
func (lru *LRUG[Key, Value]) Get(key Key) (value Value, ok bool) {
lru.mu.Lock()
defer lru.mu.Unlock()
item := lru.items[key]
if item == nil {
return
}
if lru.head.next != item {
lru.pop(item)
lru.push(item)
}
return item.value, true
}

// Contains returns true if the key exists.
func (lru *LRUG[Key, Value]) Contains(key Key) bool {
lru.mu.RLock()
defer lru.mu.RUnlock()
_, ok := lru.items[key]
return ok
}

// Peek returns the value for key value without updating
// the recently used status.
func (lru *LRUG[Key, Value]) Peek(key Key) (value Value, ok bool) {
lru.mu.RLock()
defer lru.mu.RUnlock()

if item := lru.items[key]; item != nil {
return item.value, true
}
return
}

// Delete a value for a key
func (lru *LRUG[Key, Value]) Delete(key Key) (prev Value, deleted bool) {
lru.mu.Lock()
defer lru.mu.Unlock()
item := lru.items[key]
if item == nil {
return
}
delete(lru.items, key)
lru.pop(item)
return item.value, true
}

// Range iterates over all key/values in the order of most recently to
// least recently used items.
// It's not safe to call other LRU operations while ranging.
func (lru *LRUG[Key, Value]) Range(iter func(key Key, value Value) bool) {
lru.mu.RLock()
defer lru.mu.RUnlock()
if head := lru.head; head != nil {
item := head.next
for item != lru.tail {
if !iter(item.key, item.value) {
return
}
item = item.next
}
}
}

// Reverse iterates over all key/values in the order of least recently to
// most recently used items.
// It's not safe to call other LRU operations while ranging.
func (lru *LRUG[Key, Value]) Reverse(iter func(key Key, value Value) bool) {
lru.mu.RLock()
defer lru.mu.RUnlock()
if tail := lru.tail; tail != nil {
item := tail.prev
for item != lru.head {
if !iter(item.key, item.value) {
return
}
item = item.prev
}
}
}
Loading