diff --git a/core/filtermaps/bitset.go b/core/filtermaps/bitset.go new file mode 100644 index 000000000000..c8fc9cdee838 --- /dev/null +++ b/core/filtermaps/bitset.go @@ -0,0 +1,135 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package filtermaps + +import "math/bits" + +// indexBitset represents a set of indices using a bitmap. +type indexBitset struct { + minIndex uint32 + maxIndex uint32 + bits []uint64 +} + +// newIndexBitset creates a bitset from a list of indices. +// Returns an empty bitset if indices is empty. +func newIndexBitset(indices []uint32) *indexBitset { + if len(indices) == 0 { + return &indexBitset{} + } + // Find index range + minIdx, maxIdx := indices[0], indices[0] + for _, idx := range indices[1:] { + if idx < minIdx { + minIdx = idx + } + if idx > maxIdx { + maxIdx = idx + } + } + // Calculate number of uint64 needed + rangeSize := maxIdx - minIdx + 1 + bitsCount := (rangeSize + 63) / 64 + bitset := &indexBitset{ + minIndex: minIdx, + maxIndex: maxIdx, + bits: make([]uint64, bitsCount), + } + // Set all specified indices + for _, idx := range indices { + bitset.Set(idx) + } + return bitset +} + +// Has checks if an index exists in the set. +func (b *indexBitset) Has(idx uint32) bool { + if b.bits == nil || idx < b.minIndex || idx > b.maxIndex { + return false + } + pos := idx - b.minIndex + wordIdx := pos / 64 + bitIdx := pos % 64 + return (b.bits[wordIdx] & (1 << bitIdx)) != 0 +} + +// Set adds an index to the set. +func (b *indexBitset) Set(idx uint32) { + if b.bits == nil || idx < b.minIndex || idx > b.maxIndex { + return + } + pos := idx - b.minIndex + wordIdx := pos / 64 + bitIdx := pos % 64 + b.bits[wordIdx] |= 1 << bitIdx +} + +// Clear removes an index from the set. +func (b *indexBitset) Clear(idx uint32) { + if b.bits == nil || idx < b.minIndex || idx > b.maxIndex { + return + } + pos := idx - b.minIndex + wordIdx := pos / 64 + bitIdx := pos % 64 + b.bits[wordIdx] &^= 1 << bitIdx +} + +// Count returns the number of indices in the set. +func (b *indexBitset) Count() int { + if b.bits == nil { + return 0 + } + count := 0 + for _, word := range b.bits { + count += bits.OnesCount64(word) + } + return count +} + +// IsEmpty checks if the set is empty. +func (b *indexBitset) IsEmpty() bool { + if b.bits == nil { + return true + } + for _, word := range b.bits { + if word != 0 { + return false + } + } + return true +} + +// Iterate traverses all indices in the set. +// The callback function fn is called with each index in the set. +// Iteration order is from smallest to largest. +func (b *indexBitset) Iterate(fn func(uint32)) { + if b.bits == nil { + return + } + for i, word := range b.bits { + if word == 0 { + continue + } + baseIdx := b.minIndex + uint32(i*64) + for bitIdx := 0; bitIdx < 64; bitIdx++ { + if (word & (1 << bitIdx)) != 0 { + fn(baseIdx + uint32(bitIdx)) + } + } + } +} diff --git a/core/filtermaps/bitset_test.go b/core/filtermaps/bitset_test.go new file mode 100644 index 000000000000..ad2261832256 --- /dev/null +++ b/core/filtermaps/bitset_test.go @@ -0,0 +1,170 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package filtermaps + +import ( + "testing" +) + +func TestBitsetBasic(t *testing.T) { + indices := []uint32{100, 101, 105, 110, 115} + bs := newIndexBitset(indices) + // Test Has - existing indices + for _, idx := range indices { + if !bs.Has(idx) { + t.Errorf("Expected Has(%d) = true", idx) + } + } + // Test Has - non-existing indices + notInSet := []uint32{99, 102, 103, 104, 106, 107, 108, 109, 111, 116} + for _, idx := range notInSet { + if bs.Has(idx) { + t.Errorf("Expected Has(%d) = false", idx) + } + } + // Test Count + if count := bs.Count(); count != len(indices) { + t.Errorf("Expected Count = %d, got %d", len(indices), count) + } +} + +func TestBitsetClear(t *testing.T) { + indices := []uint32{100, 101, 105, 110, 115} + bs := newIndexBitset(indices) + // Clear an index + bs.Clear(105) + if bs.Has(105) { + t.Error("Expected Has(105) = false after Clear") + } + // Count should decrease + if count := bs.Count(); count != 4 { + t.Errorf("Expected Count = 4, got %d", count) + } + // Other indices should remain unaffected + if !bs.Has(100) || !bs.Has(101) || !bs.Has(110) || !bs.Has(115) { + t.Error("Other indices should remain") + } +} + +func TestBitsetSet(t *testing.T) { + indices := []uint32{100, 105, 110} + bs := newIndexBitset(indices) + // Set a new index + bs.Set(102) + if !bs.Has(102) { + t.Error("Expected Has(102) = true after Set") + } + // Count should increase + if count := bs.Count(); count != 4 { + t.Errorf("Expected Count = 4, got %d", count) + } +} + +func TestBitsetIterate(t *testing.T) { + indices := []uint32{100, 101, 105, 110, 115} + bs := newIndexBitset(indices) + // Collect iterated indices + var collected []uint32 + bs.Iterate(func(idx uint32) { + collected = append(collected, idx) + }) + // Verify count + if len(collected) != len(indices) { + t.Errorf("Expected %d indices, got %d", len(indices), len(collected)) + } + // Verify all indices are present + for _, idx := range indices { + found := false + for _, c := range collected { + if c == idx { + found = true + break + } + } + if !found { + t.Errorf("Index %d not found in iteration", idx) + } + } +} + +// Benchmark: Bitset vs Map +func BenchmarkBitsetVsMap(b *testing.B) { + // Generate test data: 1000 consecutive indices + indices := make([]uint32, 1000) + for i := range indices { + indices[i] = uint32(5000 + i) + } + + b.Run("Bitset_Has", func(b *testing.B) { + bs := newIndexBitset(indices) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = bs.Has(5500) + } + }) + + b.Run("Map_Has", func(b *testing.B) { + m := make(map[uint32]struct{}) + for _, idx := range indices { + m[idx] = struct{}{} + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = m[5500] + } + }) + + b.Run("Bitset_Set", func(b *testing.B) { + bs := newIndexBitset(indices) + b.ResetTimer() + for i := 0; i < b.N; i++ { + bs.Set(5500) + } + }) + + b.Run("Map_Set", func(b *testing.B) { + m := make(map[uint32]struct{}) + for _, idx := range indices { + m[idx] = struct{}{} + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + m[5500] = struct{}{} + } + }) + + b.Run("Bitset_Clear", func(b *testing.B) { + bs := newIndexBitset(indices) + b.ResetTimer() + for i := 0; i < b.N; i++ { + bs.Clear(5500) + bs.Set(5500) // Reset for next iteration + } + }) + + b.Run("Map_Delete", func(b *testing.B) { + m := make(map[uint32]struct{}) + for _, idx := range indices { + m[idx] = struct{}{} + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + delete(m, 5500) + m[5500] = struct{}{} // Re-add for next iteration + } + }) +} diff --git a/core/filtermaps/map_renderer.go b/core/filtermaps/map_renderer.go index e1284a382977..9066b7d88145 100644 --- a/core/filtermaps/map_renderer.go +++ b/core/filtermaps/map_renderer.go @@ -48,14 +48,17 @@ var ( // mapRenderer represents a process that renders filter maps in a specified // range according to the actual targetView. type mapRenderer struct { - f *FilterMaps - renderBefore uint32 - currentMap *renderedMap - finishedMaps map[uint32]*renderedMap - finished common.Range[uint32] - iterator *logIterator + f *FilterMaps + renderBefore uint32 + currentMap *renderedMap + finishedMaps map[uint32]*renderedMap + finished common.Range[uint32] + iterator *logIterator + rowMappingCache *lru.Cache[common.Hash, lvPos] } +type lvPos struct{ rowIndex, layerIndex uint32 } + // renderedMap represents a single filter map that is being rendered in memory. type renderedMap struct { filterMap filterMap @@ -110,10 +113,11 @@ func (f *FilterMaps) renderMapsFromSnapshot(cp *renderedMap) (*mapRenderer, erro lastBlock: cp.lastBlock, blockLvPtrs: slices.Clone(cp.blockLvPtrs), }, - finishedMaps: make(map[uint32]*renderedMap), - finished: common.NewRange(cp.mapIndex, 0), - renderBefore: math.MaxUint32, - iterator: iter, + finishedMaps: make(map[uint32]*renderedMap), + finished: common.NewRange(cp.mapIndex, 0), + renderBefore: math.MaxUint32, + iterator: iter, + rowMappingCache: lru.NewCache[common.Hash, lvPos](cachedRowMappings), }, nil } @@ -131,10 +135,11 @@ func (f *FilterMaps) renderMapsFromMapBoundary(firstMap, renderBefore uint32, st mapIndex: firstMap, lastBlock: iter.blockNumber, }, - finishedMaps: make(map[uint32]*renderedMap), - finished: common.NewRange(firstMap, 0), - renderBefore: renderBefore, - iterator: iter, + finishedMaps: make(map[uint32]*renderedMap), + finished: common.NewRange(firstMap, 0), + renderBefore: renderBefore, + iterator: iter, + rowMappingCache: lru.NewCache[common.Hash, lvPos](cachedRowMappings), }, nil } @@ -319,9 +324,8 @@ func (r *mapRenderer) renderCurrentMap(stopCb func() bool) (bool, error) { if r.iterator.lvIndex == 0 { r.currentMap.blockLvPtrs = []uint64{0} } - type lvPos struct{ rowIndex, layerIndex uint32 } - rowMappingCache := lru.NewCache[common.Hash, lvPos](cachedRowMappings) - defer rowMappingCache.Purge() + // Clear the cache for this map rendering + r.rowMappingCache.Purge() for r.iterator.lvIndex < uint64(r.currentMap.mapIndex+1)< 0 { return false } } - delete(m.baseRequested, mapIndex) + m.baseRequested.Clear(mapIndex) return true } @@ -827,15 +822,15 @@ func (m *matchSequenceInstance) dropBase(mapIndex uint32) bool { // matcher based on the known results from the base matcher and removes it // from the internal requested set and returns true if possible. func (m *matchSequenceInstance) dropNext(mapIndex uint32) bool { - if _, ok := m.nextRequested[mapIndex]; !ok { + if !m.nextRequested.Has(mapIndex) { return false } - if _, ok := m.needMatched[mapIndex]; ok { + if m.needMatched.Has(mapIndex) { if base := m.baseResults[mapIndex]; base == nil || len(base) > 0 { return false } } - delete(m.nextRequested, mapIndex) + m.nextRequested.Clear(mapIndex) return true }