From 8c35795071024457650d79b611470fb4a692d62d Mon Sep 17 00:00:00 2001 From: Oleg Dyachenko Date: Fri, 23 Aug 2024 19:56:39 +0700 Subject: [PATCH 1/7] len64 / leadingZeroes64 implementation for go 1.9+ and older --- leading_zeros_18.go | 43 +++++++++++++++++++++++++++++++++++++++++++ leading_zeros_19.go | 14 ++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 leading_zeros_18.go create mode 100644 leading_zeros_19.go diff --git a/leading_zeros_18.go b/leading_zeros_18.go new file mode 100644 index 0000000..cd10a88 --- /dev/null +++ b/leading_zeros_18.go @@ -0,0 +1,43 @@ +//go:build !go1.9 +// +build !go1.9 + +package bitset + +var len8tab = "" + + "\x00\x01\x02\x02\x03\x03\x03\x03\x04\x04\x04\x04\x04\x04\x04\x04" + + "\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05" + + "\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06" + + "\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06" + + "\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07" + + "\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07" + + "\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07" + + "\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07" + + "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + + "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + + "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + + "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + + "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + + "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + + "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + + "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + +// Len64 returns the minimum number of bits required to represent x; the result is 0 for x == 0. +func len64(x uint64) (n uint) { + if x >= 1<<32 { + x >>= 32 + n = 32 + } + if x >= 1<<16 { + x >>= 16 + n += 16 + } + if x >= 1<<8 { + x >>= 8 + n += 8 + } + return n + uint(len8tab[x]) +} + +func leadingZeroes64(v uint64) uint { + return 64 - len64(x) +} diff --git a/leading_zeros_19.go b/leading_zeros_19.go new file mode 100644 index 0000000..74a7942 --- /dev/null +++ b/leading_zeros_19.go @@ -0,0 +1,14 @@ +//go:build go1.9 +// +build go1.9 + +package bitset + +import "math/bits" + +func len64(v uint64) uint { + return uint(bits.Len64(v)) +} + +func leadingZeroes64(v uint64) uint { + return uint(bits.LeadingZeros64(v)) +} From 3fa9653c0f1a732782395cd8e19e9d4f1ec93fb2 Mon Sep 17 00:00:00 2001 From: Oleg Dyachenko Date: Fri, 23 Aug 2024 21:59:09 +0700 Subject: [PATCH 2/7] ShiftLeft implementation --- bitset.go | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/bitset.go b/bitset.go index 4c57a49..b5e1adf 100644 --- a/bitset.go +++ b/bitset.go @@ -1182,3 +1182,65 @@ func (b *BitSet) Select(index uint) uint { } return b.length } + +// ShiftLeft shifts the bitset like << operation would do. +// +// Left shift may require bitset size extension. We try to avoid the +// unnecessary memory operations by detecting the leftmost set bit. +// The function will panic if shift causes excess of capacity. +func (b *BitSet) ShiftLeft(bits uint) { + panicIfNull(b) + + // detect the leftmost set bit + idx := len(b.set) - 1 + for ; idx >= 0 && b.set[idx] == 0; idx-- { + } + + // no set bits, nothing to do + if idx < 0 { + return + } + + // leftmost set bit + pad := len64(b.set[idx]) + left := uint(idx)*wordSize + pad - 1 + + // capacity check + if left+bits >= Cap() { + panic("You are exceeding the capacity") + } + + // destination set + dst := b.set + + // not using extendSet() to avoid unneeded data copying + nsize := wordsNeeded(left + bits) + if len(b.set) < nsize { + dst = make([]uint64, nsize, 2*nsize) + b.length = left + bits + 1 + } + + shift, pages := bits%wordSize, bits>>log2WordSize + if bits%wordSize == 0 { // happy case: just add pages + copy(dst[pages:nsize], b.set) + } else { + if pad+shift >= wordSize { + dst[idx+int(pages)+1] = b.set[idx] >> (wordSize - shift) + } + + for i := idx; i >= 0; i-- { + if i > 0 { + dst[i+int(pages)] = (b.set[i] << shift) | (b.set[i-1] >> (wordSize - shift)) + } else { + dst[i+int(pages)] = b.set[i] << shift + } + } + } + + // zeroing extra pages + for i := 0; i < int(pages); i++ { + dst[i] = 0 + } + + b.set = dst +} From 38849e9509bd87f2765120f4c0673b8e79d34f51 Mon Sep 17 00:00:00 2001 From: Oleg Dyachenko Date: Fri, 23 Aug 2024 22:12:14 +0700 Subject: [PATCH 3/7] ShiftLeft implementation using top() --- bitset.go | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/bitset.go b/bitset.go index b5e1adf..13d8ef6 100644 --- a/bitset.go +++ b/bitset.go @@ -1183,6 +1183,22 @@ func (b *BitSet) Select(index uint) uint { return b.length } +// top detects the top bit set +func (b *BitSet) top() (uint, bool) { + panicIfNull(b) + + idx := len(b.set) - 1 + for ; idx >= 0 && b.set[idx] == 0; idx-- { + } + + // no set bits + if idx < 0 { + return 0, false + } + + return uint(idx)*wordSize + len64(b.set[idx]) - 1, true +} + // ShiftLeft shifts the bitset like << operation would do. // // Left shift may require bitset size extension. We try to avoid the @@ -1191,22 +1207,13 @@ func (b *BitSet) Select(index uint) uint { func (b *BitSet) ShiftLeft(bits uint) { panicIfNull(b) - // detect the leftmost set bit - idx := len(b.set) - 1 - for ; idx >= 0 && b.set[idx] == 0; idx-- { - } - - // no set bits, nothing to do - if idx < 0 { + top, ok := b.top() + if !ok { return } - // leftmost set bit - pad := len64(b.set[idx]) - left := uint(idx)*wordSize + pad - 1 - // capacity check - if left+bits >= Cap() { + if top+bits >= Cap() { panic("You are exceeding the capacity") } @@ -1214,21 +1221,22 @@ func (b *BitSet) ShiftLeft(bits uint) { dst := b.set // not using extendSet() to avoid unneeded data copying - nsize := wordsNeeded(left + bits) + nsize := wordsNeeded(top + bits) if len(b.set) < nsize { dst = make([]uint64, nsize, 2*nsize) - b.length = left + bits + 1 + b.length = top + bits + 1 } + pad, idx := top%wordSize, top>>log2WordSize shift, pages := bits%wordSize, bits>>log2WordSize if bits%wordSize == 0 { // happy case: just add pages copy(dst[pages:nsize], b.set) } else { if pad+shift >= wordSize { - dst[idx+int(pages)+1] = b.set[idx] >> (wordSize - shift) + dst[idx+pages+1] = b.set[idx] >> (wordSize - shift) } - for i := idx; i >= 0; i-- { + for i := int(idx); i >= 0; i-- { if i > 0 { dst[i+int(pages)] = (b.set[i] << shift) | (b.set[i-1] >> (wordSize - shift)) } else { From a73b2971def53a7e27cada847755b6a8c2a8a9d7 Mon Sep 17 00:00:00 2001 From: Oleg Dyachenko Date: Fri, 23 Aug 2024 22:41:53 +0700 Subject: [PATCH 4/7] ShiftRight implementation --- bitset.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/bitset.go b/bitset.go index 13d8ef6..d14b0b5 100644 --- a/bitset.go +++ b/bitset.go @@ -1252,3 +1252,41 @@ func (b *BitSet) ShiftLeft(bits uint) { b.set = dst } + +// ShiftRight shifts the bitset like >> operation would do. +func (b *BitSet) ShiftRight(bits uint) { + panicIfNull(b) + + top, ok := b.top() + if !ok { + return + } + + if bits >= top { + b.set = make([]uint64, wordsNeeded(b.length)) + return + } + + pad, idx := top%wordSize, top>>log2WordSize + shift, pages := bits%wordSize, bits>>log2WordSize + if bits%wordSize == 0 { // happy case: just clear pages + b.set = b.set[pages:] + b.length -= pages * wordSize + } else { + for i := 0; i <= int(idx-pages); i++ { + if i < int(idx-pages) { + b.set[i] = (b.set[i+int(pages)] >> shift) | (b.set[i+int(pages)+1] << (wordSize - shift)) + } else { + b.set[i] = b.set[i+int(pages)] >> shift + } + } + + if pad < shift { + b.set[int(idx-pages)] = 0 + } + } + + for i := int(idx-pages) + 1; i <= int(idx); i++ { + b.set[i] = 0 + } +} From 1ceac61f2616706bc07753b375641fda325f6e9c Mon Sep 17 00:00:00 2001 From: Oleg Dyachenko Date: Fri, 23 Aug 2024 22:52:48 +0700 Subject: [PATCH 5/7] Zero bits cases --- bitset.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bitset.go b/bitset.go index d14b0b5..d6439ac 100644 --- a/bitset.go +++ b/bitset.go @@ -1207,6 +1207,10 @@ func (b *BitSet) top() (uint, bool) { func (b *BitSet) ShiftLeft(bits uint) { panicIfNull(b) + if bits == 0 { + return + } + top, ok := b.top() if !ok { return @@ -1257,6 +1261,10 @@ func (b *BitSet) ShiftLeft(bits uint) { func (b *BitSet) ShiftRight(bits uint) { panicIfNull(b) + if bits == 0 { + return + } + top, ok := b.top() if !ok { return From f156426761cdc163a8687a9e156bdf858ae4094c Mon Sep 17 00:00:00 2001 From: Oleg Dyachenko Date: Fri, 23 Aug 2024 23:16:49 +0700 Subject: [PATCH 6/7] Tests and fixes --- bitset.go | 2 ++ bitset_test.go | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/bitset.go b/bitset.go index d6439ac..373def2 100644 --- a/bitset.go +++ b/bitset.go @@ -1228,6 +1228,8 @@ func (b *BitSet) ShiftLeft(bits uint) { nsize := wordsNeeded(top + bits) if len(b.set) < nsize { dst = make([]uint64, nsize, 2*nsize) + } + if top+bits >= b.length { b.length = top + bits + 1 } diff --git a/bitset_test.go b/bitset_test.go index 34d628a..62b6970 100644 --- a/bitset_test.go +++ b/bitset_test.go @@ -1963,3 +1963,74 @@ func TestSetAll(t *testing.T) { test(fmt.Sprintf("length %d", length), New(length), length) } } + +func TestShiftLeft(t *testing.T) { + data := []uint{5, 28, 45, 72, 89} + + test := func(name string, bits uint) { + t.Run(name, func(t *testing.T) { + b := New(200) + for _, i := range data { + b.Set(i) + } + + b.ShiftLeft(bits) + + if int(b.Count()) != len(data) { + t.Error("bad bits count") + } + + for _, i := range data { + if !b.Test(i + bits) { + t.Errorf("bit %v is not set", i+bits) + } + } + }) + } + + test("zero", 0) + test("no page change", 19) + test("shift to full page", 38) + test("full page shift", 64) + test("no page split", 80) + test("with page split", 114) + test("with extension", 242) +} + +func TestShiftRight(t *testing.T) { + data := []uint{5, 28, 45, 72, 89} + + test := func(name string, bits uint) { + t.Run(name, func(t *testing.T) { + b := New(200) + for _, i := range data { + b.Set(i) + } + + b.ShiftRight(bits) + + count := 0 + for _, i := range data { + if i > bits { + count++ + + if !b.Test(i - bits) { + t.Errorf("bit %v is not set", i-bits) + } + } + } + + if int(b.Count()) != count { + t.Error("bad bits count") + } + }) + } + + test("zero", 0) + test("no page change", 3) + test("no page split", 20) + test("with page split", 40) + test("with extension", 70) + test("full shift", 89) + test("remove all", 242) +} From 5cfc6ad5a4bfe05da59f40b992c9ab389b0fda73 Mon Sep 17 00:00:00 2001 From: Oleg Dyachenko Date: Fri, 23 Aug 2024 23:55:20 +0700 Subject: [PATCH 7/7] One more test --- bitset_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/bitset_test.go b/bitset_test.go index 62b6970..4114bec 100644 --- a/bitset_test.go +++ b/bitset_test.go @@ -2030,6 +2030,7 @@ func TestShiftRight(t *testing.T) { test("no page change", 3) test("no page split", 20) test("with page split", 40) + test("full page shift", 64) test("with extension", 70) test("full shift", 89) test("remove all", 242)