Skip to content

Commit

Permalink
Simplify setting up a new LRU
Browse files Browse the repository at this point in the history
This change removes the Config type and changes the API.
  • Loading branch information
rockdaboot committed Mar 25, 2023
1 parent d921eae commit d8ace71
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 119 deletions.
18 changes: 8 additions & 10 deletions bench/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,13 @@ import (
freelru "github.com/elastic/go-freelru"
)

const lruSize = 8192

func BenchmarkFreeLRUGet(b *testing.B) {
lru, err := freelru.New[int, int](lruSize, nil, hashIntAESENC)
lru, err := freelru.New[int, int](CAP, hashIntAESENC)
if err != nil {
b.Fatalf("err: %v", err)
}

for i := 0; i < 8192; i++ {
for i := 0; i < CAP; i++ {
// nolint:gosec
val := int(rand.Int63())
lru.Add(i, val)
Expand All @@ -51,12 +49,12 @@ func BenchmarkFreeLRUGet(b *testing.B) {
}

func BenchmarkSimpleLRUGet(b *testing.B) {
lru, err := simplelru.NewLRU[int, int](lruSize, nil)
lru, err := simplelru.NewLRU[int, int](CAP, nil)
if err != nil {
b.Fatalf("err: %v", err)
}

for i := 0; i < 8192; i++ {
for i := 0; i < CAP; i++ {
// nolint:gosec
val := int(rand.Int63())
lru.Add(i, val)
Expand All @@ -71,12 +69,12 @@ func BenchmarkSimpleLRUGet(b *testing.B) {
}

func BenchmarkFreeCacheGet(b *testing.B) {
lru := freecache.NewCache(lruSize)
lru := freecache.NewCache(CAP)

b.ReportAllocs()
b.ResetTimer()

for i := 0; i < 8192; i++ {
for i := 0; i < CAP; i++ {
// nolint:gosec
val := int(rand.Int63())
bv := [8]byte{}
Expand All @@ -102,9 +100,9 @@ func BenchmarkFreeCacheGet(b *testing.B) {
}

func BenchmarkMapGet(b *testing.B) {
cache := make(map[int]int, lruSize)
cache := make(map[int]int, CAP)

for i := 0; i < 8192; i++ {
for i := 0; i < CAP; i++ {
// nolint:gosec
val := int(rand.Int63())
cache[i] = val
Expand Down
58 changes: 23 additions & 35 deletions bench/lru_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import (
freelru "github.com/elastic/go-freelru"
)

const CAP = 8192

func BenchmarkHashInt_FNV1A(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = hashIntFNV1A(i)
Expand Down Expand Up @@ -70,19 +72,8 @@ func BenchmarkHashString_XXHASH(b *testing.B) {
}
}

func makeConfig[K comparable, V any](hash freelru.HashKeyCallback[K]) *freelru.Config[K, V] {
cfg := freelru.DefaultConfig[K, V]()
cfg.Capacity = 8192
cfg.Size = 8192 // Reader may try out a factor of 2: it makes the LRU significantly faster.
cfg.HashKey = hash
return &cfg
}

func runFreeLRUAddInt[V any](b *testing.B, cfg *freelru.Config[int, V]) {
if cfg == nil {
cfg = makeConfig[int, V](hashIntAESENC)
}
lru, err := freelru.NewWithConfig(*cfg)
func runFreeLRUAddInt[V any](b *testing.B) {
lru, err := freelru.New[int, V](CAP, hashIntAESENC)
if err != nil {
b.Fatalf("err: %v", err)
}
Expand All @@ -101,11 +92,8 @@ func runFreeLRUAddInt[V any](b *testing.B, cfg *freelru.Config[int, V]) {
}
}

func runFreeLRUAddIntAscending[V any](b *testing.B, cfg *freelru.Config[int, V]) {
if cfg == nil {
cfg = makeConfig[int, V](hashIntAESENC)
}
lru, err := freelru.NewWithConfig(*cfg)
func runFreeLRUAddIntAscending[V any](b *testing.B) {
lru, err := freelru.New[int, V](CAP, hashIntAESENC)
if err != nil {
b.Fatalf("err: %v", err)
}
Expand All @@ -120,23 +108,23 @@ func runFreeLRUAddIntAscending[V any](b *testing.B, cfg *freelru.Config[int, V])
}

func BenchmarkFreeLRUAdd_int_int(b *testing.B) {
runFreeLRUAddInt[int](b, nil)
runFreeLRUAddInt[int](b)
}

func BenchmarkFreeLRUAdd_int_int128(b *testing.B) {
runFreeLRUAddInt[int128](b, nil)
runFreeLRUAddInt[int128](b)
}

func BenchmarkFreeLRUAdd_int_int_Ascending(b *testing.B) {
runFreeLRUAddIntAscending[int](b, nil)
runFreeLRUAddIntAscending[int](b)
}

func BenchmarkFreeLRUAdd_int_int128_Ascending(b *testing.B) {
runFreeLRUAddIntAscending[int128](b, nil)
runFreeLRUAddIntAscending[int128](b)
}

func BenchmarkFreeLRUAdd_uint32_uint64(b *testing.B) {
lru, err := freelru.NewWithConfig(*makeConfig[uint32, uint64](hashUInt32))
lru, err := freelru.New[uint32, uint64](CAP, hashUInt32)
if err != nil {
b.Fatalf("err: %v", err)
}
Expand All @@ -151,7 +139,7 @@ func BenchmarkFreeLRUAdd_uint32_uint64(b *testing.B) {
}

func BenchmarkFreeLRUAdd_string_uint64(b *testing.B) {
lru, err := freelru.NewWithConfig(*makeConfig[string, uint64](hashStringAESENC))
lru, err := freelru.New[string, uint64](CAP, hashStringAESENC)
if err != nil {
b.Fatalf("err: %v", err)
}
Expand All @@ -168,7 +156,7 @@ func BenchmarkFreeLRUAdd_string_uint64(b *testing.B) {
}

func BenchmarkFreeLRUAdd_int_string(b *testing.B) {
lru, err := freelru.NewWithConfig(*makeConfig[int, string](hashIntFNV1A))
lru, err := freelru.New[int, string](CAP, hashIntFNV1A)
if err != nil {
b.Fatalf("err: %v", err)
}
Expand All @@ -182,7 +170,7 @@ func BenchmarkFreeLRUAdd_int_string(b *testing.B) {
}

func runSimpleLRUAddInt[V any](b *testing.B) {
lru, err := simplelru.NewLRU[int, V](8192, nil)
lru, err := simplelru.NewLRU[int, V](CAP, nil)
if err != nil {
b.Fatalf("err: %v", err)
}
Expand All @@ -205,7 +193,7 @@ func BenchmarkSimpleLRUAdd_int_int128(b *testing.B) {
}

func BenchmarkSimpleLRUAdd_uint32_uint64(b *testing.B) {
lru, err := simplelru.NewLRU[uint32, uint64](8192, nil)
lru, err := simplelru.NewLRU[uint32, uint64](CAP, nil)
if err != nil {
b.Fatalf("err: %v", err)
}
Expand All @@ -220,7 +208,7 @@ func BenchmarkSimpleLRUAdd_uint32_uint64(b *testing.B) {
}

func BenchmarkSimpleLRUAdd_string_uint64(b *testing.B) {
lru, err := simplelru.NewLRU[string, uint64](8192, nil)
lru, err := simplelru.NewLRU[string, uint64](CAP, nil)
if err != nil {
b.Fatalf("err: %v", err)
}
Expand All @@ -237,7 +225,7 @@ func BenchmarkSimpleLRUAdd_string_uint64(b *testing.B) {
}

func BenchmarkSimpleLRUAdd_int_string(b *testing.B) {
lru, err := simplelru.NewLRU[int, string](8192, nil)
lru, err := simplelru.NewLRU[int, string](CAP, nil)
if err != nil {
b.Fatalf("err: %v", err)
}
Expand All @@ -251,7 +239,7 @@ func BenchmarkSimpleLRUAdd_int_string(b *testing.B) {
}

func BenchmarkFreeCacheAdd_int_int(b *testing.B) {
lru := freecache.NewCache(8192)
lru := freecache.NewCache(CAP)

b.ReportAllocs()
b.ResetTimer()
Expand All @@ -267,7 +255,7 @@ func BenchmarkFreeCacheAdd_int_int(b *testing.B) {
}

func BenchmarkFreeCacheAdd_int_int128(b *testing.B) {
lru := freecache.NewCache(8192)
lru := freecache.NewCache(CAP)

b.ReportAllocs()
b.ResetTimer()
Expand All @@ -284,7 +272,7 @@ func BenchmarkFreeCacheAdd_int_int128(b *testing.B) {
}

func BenchmarkFreeCacheAdd_uint32_uint64(b *testing.B) {
lru := freecache.NewCache(8192)
lru := freecache.NewCache(CAP)

b.ReportAllocs()
b.ResetTimer()
Expand All @@ -300,7 +288,7 @@ func BenchmarkFreeCacheAdd_uint32_uint64(b *testing.B) {
}

func BenchmarkFreeCacheAdd_string_uint64(b *testing.B) {
lru := freecache.NewCache(8192)
lru := freecache.NewCache(CAP)

keys := makeStrings(b.N)

Expand All @@ -317,7 +305,7 @@ func BenchmarkFreeCacheAdd_string_uint64(b *testing.B) {
}

func BenchmarkFreeCacheAdd_int_string(b *testing.B) {
lru := freecache.NewCache(8192)
lru := freecache.NewCache(CAP)

b.ReportAllocs()
b.ResetTimer()
Expand Down Expand Up @@ -369,7 +357,7 @@ func BenchmarkMapAdd_string_uint64(b *testing.B) {
// go tool pprof mem.out
// (then check the top10)
func TestSimpleLRUAdd(t *testing.T) {
cache, _ := simplelru.NewLRU[uint64, int](8192, nil)
cache, _ := simplelru.NewLRU[uint64, int](CAP, nil)

var val int
for i := uint64(0); i < 1000; i++ {
Expand Down
103 changes: 40 additions & 63 deletions lru.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,36 +31,6 @@ type OnEvictCallback[K comparable, V any] func(K, V)
// HashKeyCallback is the function that creates a hash from the passed key.
type HashKeyCallback[K comparable] func(K) uint32

// Config is the type for the LRU configuration passed to New().
type Config[K comparable, V any] struct {
// Capacity is the maximal number of elements of the LRU cache before eviction takes place.
Capacity uint32

// Size is the size of the LRU in elements.
// It must be >= Capacity.
// The more it exceeds Capacity, the less likely are costly collisions when inserting elements.
Size uint32

// OnEvict is called for every eviction.
OnEvict OnEvictCallback[K, V]

// HashKey is called for whenever the hash of a key is needed.
HashKey HashKeyCallback[K]

// Lifetime is the default lifetime for elements.
// A value of 0 (default) means forever (no expiration).
Lifetime time.Duration
}

// DefaultConfig returns a default configuration for New().
func DefaultConfig[K comparable, V any]() Config[K, V] {
return Config[K, V]{
Capacity: 64,
Size: 64,
OnEvict: nil,
}
}

type element[K comparable, V any] struct {
key K
value V
Expand Down Expand Up @@ -113,48 +83,55 @@ type LRU[K comparable, V any] struct {
removals uint64
}

// SetLifetime sets the default lifetime of LRU elements.
// Lifetime 0 means "forever".
func (lru *LRU[K, V]) SetLifetime(lifetime time.Duration) {
lru.lifetime = lifetime
}

// SetOnEvict sets the OnEvict callback function.
// The onEvict function is called for each evicted lru entry.
func (lru *LRU[K, V]) SetOnEvict(onEvict OnEvictCallback[K, V]) {
lru.onEvict = onEvict
}

// New constructs an LRU with the given capacity of elements.
// The onEvict function is called for each evicted cache entry.
func New[K comparable, V any](cap uint32, onEvict OnEvictCallback[K, V],
hash HashKeyCallback[K]) (*LRU[K, V], error) {
cfg := DefaultConfig[K, V]()
cfg.Capacity = cap
cfg.Size = cap
cfg.OnEvict = onEvict
cfg.HashKey = hash
return NewWithConfig(cfg)
// The hash function calculates a hash value from the keys.
func New[K comparable, V any](cap uint32, hash HashKeyCallback[K]) (*LRU[K, V], error) {
return NewWithSize[K, V](cap, cap, hash)
}

// NewWithConfig constructs an LRU from the given configuration.
func NewWithConfig[K comparable, V any](cfg Config[K, V]) (*LRU[K, V], error) {
if cfg.Capacity == 0 {
return nil, errors.New("Capacity must be positive")
// NewWithSize constructs an LRU with the given capacity and size.
// The hash function calculates a hash value from the keys.
// A size greater than the capacity increases memory consumption and decreases the CPU consumption
// by reducing the chance of collisions.
// Size must not be lower than the capacity.
func NewWithSize[K comparable, V any](cap, size uint32, hash HashKeyCallback[K]) (
*LRU[K, V], error) {
if cap == 0 {
return nil, errors.New("capacity must be positive")
}
if cfg.Size == emptyBucket {
return nil, fmt.Errorf("size must not be %#X", cfg.Capacity)
if size == emptyBucket {
return nil, fmt.Errorf("size must not be %#X", size)
}
if cfg.Size < cfg.Capacity {
return nil, fmt.Errorf("size (%d) is smaller than capacity (%d)", cfg.Size, cfg.Capacity)
if size < cap {
return nil, fmt.Errorf("size (%d) is smaller than capacity (%d)", size, cap)
}
if cfg.HashKey == nil {
return nil, errors.New("HashKey must be set")
if hash == nil {
return nil, errors.New("hash function must be set")
}

// The hashtable size is over-provisioned by X% to reduce collisions
// as collisions are relatively expensive.
mask := uint32(0)
if bits.OnesCount32(cfg.Size) == 1 {
mask = cfg.Size - 1
}
lru := &LRU[K, V]{
cap: cfg.Capacity,
size: cfg.Size,
buckets: make([]uint32, cfg.Size),
elements: make([]element[K, V], cfg.Size),
onEvict: cfg.OnEvict,
hash: cfg.HashKey,
mask: mask,
lifetime: cfg.Lifetime,
cap: cap,
size: cap,
hash: hash,
buckets: make([]uint32, size),
elements: make([]element[K, V], size),
}

// If the size is 2^N, we can avoid costly divisions.
if bits.OnesCount32(lru.size) == 1 {
lru.mask = lru.size - 1
}

// Mark all slots as free.
Expand Down

0 comments on commit d8ace71

Please sign in to comment.