diff --git a/go.mod b/go.mod index 32eb409c97..c0e36b9094 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/dnscrypt/dnscrypt-proxy -go 1.21 +go 1.23.4 require ( github.com/BurntSushi/toml v1.5.0 @@ -16,7 +16,7 @@ require ( github.com/jedisct1/go-dnsstamps v0.0.0-20240423203910-07a0735c7774 github.com/jedisct1/go-hpke-compact v0.0.0-20241212093903-5caa4621366f github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 - github.com/jedisct1/go-sieve-cache v0.1.6 + github.com/jedisct1/go-sieve-cache v0.1.7 github.com/jedisct1/xsecretbox v0.0.0-20241212092125-3afc4917ac41 github.com/k-sone/critbitgo v1.4.0 github.com/kardianos/service v1.2.2 diff --git a/go.sum b/go.sum index 7514c628c6..d6ab8cad8e 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,8 @@ github.com/jedisct1/go-hpke-compact v0.0.0-20241212093903-5caa4621366f h1:h5/HKr github.com/jedisct1/go-hpke-compact v0.0.0-20241212093903-5caa4621366f/go.mod h1:IjVYCPbDciyDZpJpUIYodX+FvctxGmnHVZ/ZwGBCjNA= github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 h1:FWpSWRD8FbVkKQu8M1DM9jF5oXFLyE+XpisIYfdzbic= github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7/go.mod h1:BMxO138bOokdgt4UaxZiEfypcSHX0t6SIFimVP1oRfk= -github.com/jedisct1/go-sieve-cache v0.1.6 h1:cbAUZpdAX2iuX8+eb/nb4G/VlGyFXcKGLAbY9Zaz4XA= -github.com/jedisct1/go-sieve-cache v0.1.6/go.mod h1:EN88bnjKpiyS9TfZZNbUkCwgpWueZSaUi4vgVtw2988= +github.com/jedisct1/go-sieve-cache v0.1.7 h1:mdc4gTMZI6pUvMTze5K5CHxLxkk5iJcug85g2hERBVk= +github.com/jedisct1/go-sieve-cache v0.1.7/go.mod h1:EN88bnjKpiyS9TfZZNbUkCwgpWueZSaUi4vgVtw2988= github.com/jedisct1/xsecretbox v0.0.0-20241212092125-3afc4917ac41 h1:TPF+VETyhqUOY51j3KF0uk5cgHQ2Bzi6XCorcGNGfTs= github.com/jedisct1/xsecretbox v0.0.0-20241212092125-3afc4917ac41/go.mod h1:eh2PYNEklsNDqUxnbnN9Duvpw1b+ZectZAtDUDRj2tA= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= diff --git a/vendor/github.com/jedisct1/go-sieve-cache/pkg/sievecache/bitset.go b/vendor/github.com/jedisct1/go-sieve-cache/pkg/sievecache/bitset.go new file mode 100644 index 0000000000..5b7e348bce --- /dev/null +++ b/vendor/github.com/jedisct1/go-sieve-cache/pkg/sievecache/bitset.go @@ -0,0 +1,125 @@ +package sievecache + +import ( + "math/bits" +) + +// BitSet provides a memory-efficient way to store boolean values +// using 1 bit per value instead of 1 byte per value. +type BitSet struct { + bits []uint64 + size int +} + +// NewBitSet creates a new bit set with the given initial capacity. +func NewBitSet(capacity int) *BitSet { + // Calculate how many uint64s we need to store capacity bits + numWords := (capacity + 63) / 64 + return &BitSet{ + bits: make([]uint64, numWords), + size: capacity, + } +} + +// Set sets the bit at the given index to the specified value. +func (b *BitSet) Set(index int, value bool) { + if index >= b.size { + b.resize(index + 1) + } + + wordIndex := index >> 6 // Equivalent to index / 64 + bitIndex := index & 0x3F // Equivalent to index % 64 + + if value { + b.bits[wordIndex] |= 1 << bitIndex + } else { + b.bits[wordIndex] &= ^(1 << bitIndex) + } +} + +// Get returns the value of the bit at the given index. +func (b *BitSet) Get(index int) bool { + if index >= b.size { + return false + } + + wordIndex := index >> 6 // Equivalent to index / 64 + bitIndex := index & 0x3F // Equivalent to index % 64 + + return (b.bits[wordIndex] & (1 << bitIndex)) != 0 +} + +// Resize increases the capacity of the bit set to at least the specified size. +func (b *BitSet) resize(newSize int) { + if newSize <= b.size { + return + } + + // Calculate new number of words needed using bit shifting + numWords := (newSize + 63) >> 6 // Equivalent to (newSize + 63) / 64 + + // If we need more words, extend the slice + if numWords > len(b.bits) { + // Apply capacity growth strategy similar to Go slices + newCap := len(b.bits) + if newCap < 4 { + newCap = 4 + } + for newCap < numWords { + newCap += newCap >> 1 // Grow by 50% + } + + newBits := make([]uint64, numWords, newCap) + copy(newBits, b.bits) + b.bits = newBits + } + + b.size = newSize +} + +// Append adds a new bit to the end of the set. +func (b *BitSet) Append(value bool) { + b.Set(b.size, value) +} + +// Truncate reduces the size of the bit set to the specified size. +func (b *BitSet) Truncate(newSize int) { + if newSize >= b.size { + return + } + + // Calculate new number of words needed using bit shifting + numWords := (newSize + 63) >> 6 // Equivalent to (newSize + 63) / 64 + + // Clear any bits in the last word that are beyond the new size + if numWords > 0 { + lastWordBits := newSize & 0x3F // Equivalent to newSize % 64 + if lastWordBits > 0 { + // Create a mask for the bits we want to keep + mask := (uint64(1) << lastWordBits) - 1 + // Apply the mask to the last word + b.bits[numWords-1] &= mask + } + } + + // If we need fewer words, truncate the slice + if numWords < len(b.bits) { + b.bits = b.bits[:numWords] + } + + b.size = newSize +} + +// Size returns the number of bits in the set. +func (b *BitSet) Size() int { + return b.size +} + +// CountSetBits returns the number of bits that are set to true. +func (b *BitSet) CountSetBits() int { + var count int + for _, word := range b.bits { + count += bits.OnesCount64(word) + } + return count +} diff --git a/vendor/github.com/jedisct1/go-sieve-cache/pkg/sievecache/node.go b/vendor/github.com/jedisct1/go-sieve-cache/pkg/sievecache/node.go index 3107c59b11..971e482748 100644 --- a/vendor/github.com/jedisct1/go-sieve-cache/pkg/sievecache/node.go +++ b/vendor/github.com/jedisct1/go-sieve-cache/pkg/sievecache/node.go @@ -2,16 +2,14 @@ package sievecache // Node represents an internal cache entry type Node[K comparable, V any] struct { - Key K - Value V - Visited bool + Key K + Value V } // NewNode creates a new cache node func NewNode[K comparable, V any](key K, value V) Node[K, V] { return Node[K, V]{ - Key: key, - Value: value, - Visited: false, + Key: key, + Value: value, } } diff --git a/vendor/github.com/jedisct1/go-sieve-cache/pkg/sievecache/sievecache.go b/vendor/github.com/jedisct1/go-sieve-cache/pkg/sievecache/sievecache.go index a13dbb558d..198fa6b950 100644 --- a/vendor/github.com/jedisct1/go-sieve-cache/pkg/sievecache/sievecache.go +++ b/vendor/github.com/jedisct1/go-sieve-cache/pkg/sievecache/sievecache.go @@ -8,16 +8,17 @@ import ( // SieveCache provides an efficient in-memory cache with the SIEVE eviction algorithm. // This is the single-threaded implementation. type SieveCache[K comparable, V any] struct { - // Map of keys to indices in the nodes slice + // Map of keys to indices in the nodes slice (pointer, 8 bytes) indices map[K]int - // Slice of all cache nodes + // Slice of all cache nodes (pointer + len + cap, 24 bytes) nodes []Node[K, V] - // Index to the "hand" pointer used by the SIEVE algorithm for eviction - hand int - // Flag indicating if the hand pointer is initialized - handInitialized bool - // Maximum number of entries the cache can hold + // Bit array for visited flags using 1 bit per entry (pointer, 8 bytes) + visited *BitSet + // Grouping integer fields together for better memory alignment (each 8 bytes) capacity int + hand int + // Place smaller fields last to minimize padding (bool is 1 byte) + handInitialized bool } // New creates a new cache with the given capacity. @@ -30,6 +31,7 @@ func New[K comparable, V any](capacity int) (*SieveCache[K, V], error) { return &SieveCache[K, V]{ indices: make(map[K]int, capacity), nodes: make([]Node[K, V], 0, capacity), + visited: NewBitSet(capacity), hand: 0, handInitialized: false, capacity: capacity, @@ -69,7 +71,7 @@ func (c *SieveCache[K, V]) Get(key K) (V, bool) { } // Mark as visited for the SIEVE algorithm - c.nodes[idx].Visited = true + c.visited.Set(idx, true) return c.nodes[idx].Value, true } @@ -84,7 +86,7 @@ func (c *SieveCache[K, V]) GetPointer(key K) *V { } // Mark as visited for the SIEVE algorithm - c.nodes[idx].Visited = true + c.visited.Set(idx, true) return &c.nodes[idx].Value } @@ -95,7 +97,7 @@ func (c *SieveCache[K, V]) Insert(key K, value V) bool { // Check if key already exists if idx, exists := c.indices[key]; exists { // Update existing entry - c.nodes[idx].Visited = true + c.visited.Set(idx, true) c.nodes[idx].Value = value return false } @@ -109,6 +111,7 @@ func (c *SieveCache[K, V]) Insert(key K, value V) bool { node := NewNode(key, value) c.nodes = append(c.nodes, node) idx := len(c.nodes) - 1 + c.visited.Append(false) // Initialize as not visited c.indices[key] = idx return true } @@ -129,6 +132,7 @@ func (c *SieveCache[K, V]) Remove(key K) (V, bool) { if idx == len(c.nodes)-1 { node := c.nodes[len(c.nodes)-1] c.nodes = c.nodes[:len(c.nodes)-1] + c.visited.Truncate(len(c.nodes)) return node.Value, true } @@ -149,9 +153,16 @@ func (c *SieveCache[K, V]) Remove(key K) (V, bool) { // Remove the node by replacing it with the last one and updating the map removedNode := c.nodes[idx] - lastNode := c.nodes[len(c.nodes)-1] + lastIdx := len(c.nodes) - 1 + lastNode := c.nodes[lastIdx] + + // Move the last node to the removed position c.nodes[idx] = lastNode - c.nodes = c.nodes[:len(c.nodes)-1] + c.visited.Set(idx, c.visited.Get(lastIdx)) + + // Truncate slices + c.nodes = c.nodes[:lastIdx] + c.visited.Truncate(lastIdx) // Update the indices map for the moved node if idx < len(c.nodes) { @@ -187,13 +198,13 @@ func (c *SieveCache[K, V]) Evict() (V, bool) { // Scan for a non-visited entry for { // If current node is not visited, mark it for eviction - if !c.nodes[currentIdx].Visited { + if !c.visited.Get(currentIdx) { foundIdx = currentIdx break } // Mark as non-visited for next scan - c.nodes[currentIdx].Visited = false + c.visited.Set(currentIdx, false) // Move to previous node or wrap to end if currentIdx > 0 { @@ -238,13 +249,17 @@ func (c *SieveCache[K, V]) Evict() (V, bool) { if evictIdx == len(c.nodes)-1 { // If last node, just remove it c.nodes = c.nodes[:len(c.nodes)-1] + c.visited.Truncate(len(c.nodes)) return nodeToEvict.Value, true } // Otherwise swap with the last node - lastNode := c.nodes[len(c.nodes)-1] + lastIdx := len(c.nodes) - 1 + lastNode := c.nodes[lastIdx] c.nodes[evictIdx] = lastNode - c.nodes = c.nodes[:len(c.nodes)-1] + c.visited.Set(evictIdx, c.visited.Get(lastIdx)) + c.nodes = c.nodes[:lastIdx] + c.visited.Truncate(lastIdx) // Update the indices map for the moved node c.indices[lastNode.Key] = evictIdx @@ -257,8 +272,12 @@ func (c *SieveCache[K, V]) Evict() (V, bool) { // Clear removes all entries from the cache. func (c *SieveCache[K, V]) Clear() { + // Pre-allocate map with capacity hint to avoid rehashing during growth c.indices = make(map[K]int, c.capacity) + // Pre-allocate slice with capacity hint to minimize reallocations c.nodes = make([]Node[K, V], 0, c.capacity) + // Initialize bit set + c.visited = NewBitSet(c.capacity) c.hand = 0 c.handInitialized = false } @@ -310,20 +329,32 @@ func (c *SieveCache[K, V]) ForEach(f func(k K, v V)) { } } +// ForEachValue iterates over all values in the cache and applies the function f to each. +// This allows modifying the values in-place. +func (c *SieveCache[K, V]) ForEachValue(f func(v *V)) { + for i := range c.nodes { + f(&c.nodes[i].Value) + } +} + // Retain only keeps elements specified by the predicate. // Removes all entries for which f returns false. func (c *SieveCache[K, V]) Retain(f func(k K, v V) bool) { - // Estimate number of elements to remove - pre-allocate with a reasonable capacity - estimatedRemoveCount := len(c.nodes) / 4 // Assume about 25% will be removed - if estimatedRemoveCount < 8 { - estimatedRemoveCount = 8 // Minimum size for small caches + // Use a more efficient allocation strategy for the removal list + nodeCount := len(c.nodes) + if nodeCount == 0 { + return } - if estimatedRemoveCount > 1024 { - estimatedRemoveCount = 1024 // Cap at reasonable maximum + + // Start with a small capacity and grow as needed + // This avoids over-allocation for large caches with few removals + initialCap := min(32, nodeCount/4) + if initialCap < 8 { + initialCap = 8 } // Collect indices to remove - toRemove := make([]int, 0, estimatedRemoveCount) + toRemove := make([]int, 0, initialCap) for i, node := range c.nodes { if !f(node.Key, node.Value) { @@ -341,6 +372,7 @@ func (c *SieveCache[K, V]) Retain(f func(k K, v V) bool) { // If it's the last element, just remove it if idx == len(c.nodes)-1 { c.nodes = c.nodes[:len(c.nodes)-1] + c.visited.Truncate(len(c.nodes)) } else { // Replace with the last element lastIdx := len(c.nodes) - 1 @@ -348,7 +380,9 @@ func (c *SieveCache[K, V]) Retain(f func(k K, v V) bool) { // Move the last node to the removed position c.nodes[idx] = lastNode + c.visited.Set(idx, c.visited.Get(lastIdx)) c.nodes = c.nodes[:lastIdx] + c.visited.Truncate(lastIdx) // Update indices map if not removed if idx < len(c.nodes) { @@ -388,12 +422,7 @@ func (c *SieveCache[K, V]) RecommendedCapacity(minFactor, maxFactor, lowThreshol } // Count entries with visited flag set - visitedCount := 0 - for _, node := range c.nodes { - if node.Visited { - visitedCount++ - } - } + visitedCount := c.visited.CountSetBits() // Calculate the utilization ratio (visited entries / total entries) utilizationRatio := float64(visitedCount) / float64(len(c.nodes)) diff --git a/vendor/modules.txt b/vendor/modules.txt index 7226a18719..4815b1cad4 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -63,7 +63,7 @@ github.com/jedisct1/go-hpke-compact # github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 ## explicit; go 1.23.4 github.com/jedisct1/go-minisign -# github.com/jedisct1/go-sieve-cache v0.1.6 +# github.com/jedisct1/go-sieve-cache v0.1.7 ## explicit; go 1.21 github.com/jedisct1/go-sieve-cache/pkg/sievecache # github.com/jedisct1/xsecretbox v0.0.0-20241212092125-3afc4917ac41