From c77dfc34ec0032b7f8792d7d0096003545069d9a Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Thu, 14 Nov 2024 10:02:03 -0800 Subject: [PATCH 1/5] Add `PreviousClear` and `PreviousSet` functions These are analogous to NextClear and NextSet but scan backwards. The current implementations are naive. --- bitset.go | 40 +++++++++++++++++++++++++++++++++++++ bitset_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/bitset.go b/bitset.go index cdf91c3..8e33dfb 100644 --- a/bitset.go +++ b/bitset.go @@ -586,6 +586,46 @@ func (b *BitSet) NextClear(i uint) (uint, bool) { return 0, false } +// PreviousSet returns the previous set bit from the specified index, +// including possibly the current index +// along with an error code (true = valid, false = no bit found i.e. all bits are clear) +func (b *BitSet) PreviousSet(i uint) (uint, bool) { + x := int(i >> log2WordSize) + if x >= len(b.set) { + return 0, false + } + for { + if b.Test(i) { + return i, true + } + if i == 0 { + break + } + i-- + } + return 0, false +} + +// PreviousClear returns the previous clear bit from the specified index, +// including possibly the current index +// along with an error code (true = valid, false = no clear bit found i.e. all bits are set) +func (b *BitSet) PreviousClear(i uint) (uint, bool) { + x := int(i >> log2WordSize) + if x >= len(b.set) { + return 0, false + } + for { + if !b.Test(i) { + return i, true + } + if i == 0 { + break + } + i-- + } + return 0, false +} + // ClearAll clears the entire BitSet. // It does not free the memory. func (b *BitSet) ClearAll() *BitSet { diff --git a/bitset_test.go b/bitset_test.go index ca21d8b..c76aac0 100644 --- a/bitset_test.go +++ b/bitset_test.go @@ -2087,3 +2087,57 @@ func TestWord(t *testing.T) { }) } } + +func TestPreviousSet(t *testing.T) { + v := New(10) + v.Set(0) + v.Set(2) + v.Set(4) + for _, tt := range []struct { + index uint + want uint + wantFound bool + }{ + {0, 0, true}, + {1, 0, true}, + {2, 2, true}, + {3, 2, true}, + {4, 4, true}, + {5, 4, true}, + {1024, 0, false}, + } { + t.Run(fmt.Sprintf("@%d", tt.index), func(t *testing.T) { + got, found := v.PreviousSet(tt.index) + if got != tt.want || found != tt.wantFound { + t.Errorf("PreviousSet(%d) = %d, %v, want %d, %v", tt.index, got, found, tt.want, tt.wantFound) + } + }) + } +} + +func TestPreviousClear(t *testing.T) { + v := New(10) + v.Set(0) + v.Set(2) + v.Set(4) + for _, tt := range []struct { + index uint + want uint + wantFound bool + }{ + {0, 0, false}, + {1, 1, true}, + {2, 1, true}, + {3, 3, true}, + {4, 3, true}, + {5, 5, true}, + {1024, 0, false}, + } { + t.Run(fmt.Sprintf("@%d", tt.index), func(t *testing.T) { + got, found := v.PreviousClear(tt.index) + if got != tt.want || found != tt.wantFound { + t.Errorf("PreviousClear(%d) = %d, %v, want %d, %v", tt.index, got, found, tt.want, tt.wantFound) + } + }) + } +} From de30b18b14134862398f40dec8682169299126d0 Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Thu, 14 Nov 2024 10:59:32 -0800 Subject: [PATCH 2/5] Add a benchmark for PreviousSet and PreviousClear On an Apple M2 they produces these results: BenchmarkBitsetOps/NextSet-8 2537938 470.0 ns/op BenchmarkBitsetOps/NextClear-8 2564336 468.1 ns/op BenchmarkBitsetOps/PreviousSet-8 19930 59924 ns/op BenchmarkBitsetOps/PreviousClear-8 19912 61002 ns/op A future commit should improve upon this. --- bitset_benchmark_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/bitset_benchmark_test.go b/bitset_benchmark_test.go index 98a1b52..6942817 100644 --- a/bitset_benchmark_test.go +++ b/bitset_benchmark_test.go @@ -130,6 +130,23 @@ func BenchmarkBitsetOps(b *testing.B) { } }) + b.Run("PreviousSet", func(b *testing.B) { + s = New(100000) + b.ResetTimer() + for i := 0; i < b.N; i++ { + s.PreviousSet(99999) + } + }) + + b.Run("PreviousClear", func(b *testing.B) { + s = New(100000) + s.FlipRange(0, 100000) + b.ResetTimer() + for i := 0; i < b.N; i++ { + s.PreviousClear(99999) + } + }) + b.Run("DifferenceCardinality", func(b *testing.B) { empty := New(100000) b.ResetTimer() From cbd10cc2be8e88684bfcef52fa9fde6166c1da30 Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Fri, 15 Nov 2024 09:15:12 -0800 Subject: [PATCH 3/5] Use the `len64` function to accelerate `PreviousSet` and `PreviousClear` On an Apple M2 our benchmark produces these results: BenchmarkBitsetOps/NextSet-8 2552749 468.1 ns/op BenchmarkBitsetOps/NextClear-8 2571921 466.9 ns/op BenchmarkBitsetOps/PreviousSet-8 1760882 682.4 ns/op BenchmarkBitsetOps/PreviousClear-8 1763605 680.0 ns/op Which greatly improves on the prior results of BenchmarkBitsetOps/PreviousSet-8 19930 59924 ns/op BenchmarkBitsetOps/PreviousClear-8 19912 61002 ns/op --- bitset.go | 50 ++++++++++++++++++++++++++++++++++++-------------- bitset_test.go | 12 ++++++++++-- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/bitset.go b/bitset.go index 8e33dfb..1c8a389 100644 --- a/bitset.go +++ b/bitset.go @@ -594,14 +594,23 @@ func (b *BitSet) PreviousSet(i uint) (uint, bool) { if x >= len(b.set) { return 0, false } - for { - if b.Test(i) { - return i, true - } - if i == 0 { - break + w := b.set[x] + // Clear the bits above the index + w = w & ((1 << (wordsIndex(i) + 1)) - 1) + if w != 0 { + return uint(x<= 0 { + w = b.set[x] + if w != 0 { + return uint(x<= len(b.set) { return 0, false } - for { - if !b.Test(i) { - return i, true - } - if i == 0 { - break + w := b.set[x] + // Flip all bits and find the highest one bit + w = ^w + // Clear the bits above the index + w = w & ((1 << (wordsIndex(i) + 1)) - 1) + if w != 0 { + return uint(x<= 0 { + w = b.set[x] + w = ^w + if w != 0 { + return uint(x< Date: Fri, 15 Nov 2024 09:46:44 -0800 Subject: [PATCH 4/5] Improve test coverage for PreviousSet and PreviousClear The missing coverage was probing multiple words and failing to find anything. --- bitset_test.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/bitset_test.go b/bitset_test.go index 0dfd11a..d0bee7f 100644 --- a/bitset_test.go +++ b/bitset_test.go @@ -2117,6 +2117,23 @@ func TestPreviousSet(t *testing.T) { } }) } + v.ClearAll() + for _, tt := range []struct { + index uint + want uint + wantFound bool + }{ + {0, 0, false}, + {120, 0, false}, + {1024, 0, false}, + } { + t.Run(fmt.Sprintf("@%d", tt.index), func(t *testing.T) { + got, found := v.PreviousSet(tt.index) + if got != tt.want || found != tt.wantFound { + t.Errorf("PreviousSet(%d) = %d, %v, want %d, %v", tt.index, got, found, tt.want, tt.wantFound) + } + }) + } } func TestPreviousClear(t *testing.T) { @@ -2148,4 +2165,21 @@ func TestPreviousClear(t *testing.T) { } }) } + v.SetAll() + for _, tt := range []struct { + index uint + want uint + wantFound bool + }{ + {0, 0, false}, + {120, 0, false}, + {1024, 0, false}, + } { + t.Run(fmt.Sprintf("@%d", tt.index), func(t *testing.T) { + got, found := v.PreviousClear(tt.index) + if got != tt.want || found != tt.wantFound { + t.Errorf("PreviousClear(%d) = %d, %v, want %d, %v", tt.index, got, found, tt.want, tt.wantFound) + } + }) + } } From 4d79cab679b26aeb4ec774962116e7d8823fd689 Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Sat, 16 Nov 2024 08:44:09 -0800 Subject: [PATCH 5/5] Eliminate some redundant branches and simplify the scanning loops in PreviousClear and PreviousSet There is no impact on performance, just a clean up. --- bitset.go | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/bitset.go b/bitset.go index 1c8a389..f264d8c 100644 --- a/bitset.go +++ b/bitset.go @@ -600,17 +600,11 @@ func (b *BitSet) PreviousSet(i uint) (uint, bool) { if w != 0 { return uint(x<= 0 { + for x--; x >= 0; x-- { w = b.set[x] if w != 0 { return uint(x<= 0 { + for x--; x >= 0; x-- { w = b.set[x] w = ^w if w != 0 { return uint(x<