From b668f2179e493d71b7ad47f36d1e29438dcd68a8 Mon Sep 17 00:00:00 2001 From: Arseny Balobanov Date: Thu, 12 Mar 2020 12:32:51 +0300 Subject: [PATCH] Adding lrucache task readme+solution+tests. --- lrucache/README.md | 23 ++++++ lrucache/cache.go | 22 ++++++ lrucache/cache_test.go | 162 +++++++++++++++++++++++++++++++++++++++++ lrucache/lru.go | 7 ++ 4 files changed, 214 insertions(+) create mode 100644 lrucache/README.md create mode 100644 lrucache/cache.go create mode 100644 lrucache/cache_test.go create mode 100644 lrucache/lru.go diff --git a/lrucache/README.md b/lrucache/README.md new file mode 100644 index 0000000..49a89d4 --- /dev/null +++ b/lrucache/README.md @@ -0,0 +1,23 @@ +## lrucache + +В этой задаче нужно написать простой Least recently used cache. + +LRU cache - это key-value storage фиксированного размера, реализующий операции: +* `set(k, v)` - обновляет хранимое по ключу `k` значение. + В случае, если операция приводит к превышению размера кэша, + из того удаляется значение по самому "старому" ключу. +* `get(k) -> v, ok` - возвращает значение, хранимое по ключу `k`. + +Обе функции `set` и `get` обновляют access time ключа. + +В файле [cache.go](./cache.go) задан интерфейс `Cache` с подробным описанием всех методов. + +Нужно написать реализацию и конструктор, принимающий размер кэша: +``` +func New(cap int) Cache +``` + +## Ссылки + +1. container/list: https://golang.org/pkg/container/list/ +2. wiki: https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU) diff --git a/lrucache/cache.go b/lrucache/cache.go new file mode 100644 index 0000000..b123a53 --- /dev/null +++ b/lrucache/cache.go @@ -0,0 +1,22 @@ +// +build !change + +package lrucache + +type Cache interface { + // Get returns value associated with the key. + // + // The second value is a bool that is true if the key exists in the cache, + // and false if not. + Get(key int) (int, bool) + // Set updates value associated with the key. + // + // If there is no key in the cache new (key, value) pair is created. + Set(key, value int) + // Range calls function f on all elements of the cache + // in increasing access time order. + // + // Stops earlier if f returns false. + Range(f func(key, value int) bool) + // Clear removes all keys and values from the cache. + Clear() +} diff --git a/lrucache/cache_test.go b/lrucache/cache_test.go new file mode 100644 index 0000000..af9566d --- /dev/null +++ b/lrucache/cache_test.go @@ -0,0 +1,162 @@ +package lrucache + +import ( + "math/rand" + "sort" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCache_empty(t *testing.T) { + c := New(0) + + c.Set(1, 2) + _, ok := c.Get(1) + require.False(t, ok) +} + +func TestCache_update(t *testing.T) { + c := New(1) + + _, ok := c.Get(1) + require.False(t, ok) + + c.Set(1, 2) + v, ok := c.Get(1) + require.True(t, ok) + require.Equal(t, 2, v) +} + +func TestCache_Clear(t *testing.T) { + c := New(5) + + for i := 0; i < 10; i++ { + c.Set(i, i) + } + + c.Clear() + + for i := 9; i >= 0; i-- { + c.Set(i, i) + } + + var keys, values []int + c.Range(func(key, value int) bool { + keys = append(keys, key) + values = append(values, value) + return true + }) + + require.Equal(t, []int{4, 3, 2, 1, 0}, keys) + require.Equal(t, []int{4, 3, 2, 1, 0}, values) +} + +func TestCache_Range(t *testing.T) { + c := New(5) + + for i := 0; i < 10; i++ { + c.Set(i, i) + } + + var keys, values []int + c.Range(func(key, value int) bool { + keys = append(keys, key) + values = append(values, value) + return key < 8 + }) + + require.Equal(t, []int{5, 6, 7, 8}, keys) + require.Equal(t, []int{5, 6, 7, 8}, values) +} + +func TestCache_eviction(t *testing.T) { + r := rand.New(rand.NewSource(42)) + + for _, tc := range []struct { + name string + cap int + numInserts int + maxKey int32 + }{ + { + name: "empty", + cap: 0, + numInserts: 10, + maxKey: 10, + }, + { + name: "underused", + cap: 100, + numInserts: 90, + maxKey: 1000, + }, + { + name: "simple", + cap: 100, + numInserts: 10000, + maxKey: 1000, + }, + } { + t.Run(tc.name, func(t *testing.T) { + c := New(tc.cap) + + keyToValue := make(map[int]int) + for i := 0; i < tc.numInserts; i++ { + key := int(r.Int31n(tc.maxKey)) + c.Set(key, i) + keyToValue[key] = i + } + + var keys, values []int + c.Range(func(key, value int) bool { + require.Equal(t, value, keyToValue[key]) + keys = append(keys, key) + values = append(values, value) + return true + }) + + expectedLen := tc.cap + if len(keyToValue) < tc.cap { + expectedLen = len(keyToValue) + } + require.Len(t, values, expectedLen) + require.True(t, sort.IntsAreSorted(values), "values: %+v", values) + + for _, k := range keys { + v, ok := c.Get(k) + require.True(t, ok) + require.Equal(t, keyToValue[k], v) + } + }) + } +} + +func BenchmarkCache_Set(b *testing.B) { + for _, tc := range []struct { + name string + cap int + }{ + { + name: "small", + cap: 1, + }, + { + name: "medium", + cap: 1000, + }, + { + name: "large", + cap: 1000000, + }, + } { + b.Run(tc.name, func(b *testing.B) { + c := New(tc.cap) + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + c.Set(i, i) + } + }) + } +} diff --git a/lrucache/lru.go b/lrucache/lru.go new file mode 100644 index 0000000..570c32b --- /dev/null +++ b/lrucache/lru.go @@ -0,0 +1,7 @@ +// +build !solution + +package lrucache + +func New(cap int) Cache { + panic("implement me") +}