From 05faef1b64832731c7b34d09e5ea608d5c2663e8 Mon Sep 17 00:00:00 2001 From: Andrzej Liszka Date: Thu, 5 Dec 2024 19:35:56 +0100 Subject: [PATCH 1/6] feature: introduces LenInBytes and capInBytes methods --- bitmap_opt.go | 14 ++++++++++++ bitmap_opt_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/bitmap_opt.go b/bitmap_opt.go index a36431c..91188aa 100644 --- a/bitmap_opt.go +++ b/bitmap_opt.go @@ -666,3 +666,17 @@ func (dst *Bitmap) CompareNumKeys(src *Bitmap) int { return 0 } } + +func (ra *Bitmap) LenInBytes() int { + if ra == nil { + return 0 + } + return len(ra.data) * 2 +} + +func (ra *Bitmap) capInBytes() int { + if ra == nil { + return 0 + } + return cap(ra.data) * 2 +} diff --git a/bitmap_opt_test.go b/bitmap_opt_test.go index aa5c569..7bb2555 100644 --- a/bitmap_opt_test.go +++ b/bitmap_opt_test.go @@ -924,6 +924,59 @@ func TestCompareNumKeys(t *testing.T) { }) } +func TestLenBytes(t *testing.T) { + t.Run("non-nil bitmap", func(t *testing.T) { + bm := NewBitmap() + + for _, x := range []int{1, 1 + maxCardinality, 1 + maxCardinality*2} { + bm.Set(uint64(x)) + + require.Equal(t, len(bm.ToBuffer()), bm.LenInBytes()) + } + }) + + t.Run("empty bitmap", func(t *testing.T) { + bm := NewBitmap() + + // real length is greater then 0, though ToBuffer() returns empty slice + require.Less(t, 0, bm.LenInBytes()) + }) + + t.Run("nil bitmap", func(t *testing.T) { + var bm *Bitmap + + require.Equal(t, 0, bm.LenInBytes()) + }) +} + +func TestCapBytes(t *testing.T) { + t.Run("non-nil bitmap", func(t *testing.T) { + bm := NewBitmap() + + for _, x := range []int{1, 1 + maxCardinality, 1 + maxCardinality*2} { + bm.Set(uint64(x)) + + // ToBuffer() sets cap to len, real cap is >= than buffer's one + require.LessOrEqual(t, cap(bm.ToBuffer()), bm.capInBytes()) + require.LessOrEqual(t, bm.LenInBytes(), bm.capInBytes()) + } + }) + + t.Run("empty bitmap", func(t *testing.T) { + bm := NewBitmap() + + // real cap is greater than 0, though ToBuffer() returns empty slice + require.Less(t, 0, bm.capInBytes()) + require.LessOrEqual(t, bm.LenInBytes(), bm.capInBytes()) + }) + + t.Run("nil bitmap", func(t *testing.T) { + var bm *Bitmap + + require.Equal(t, 0, bm.capInBytes()) + }) +} + func TestMergeToSuperset(t *testing.T) { run := func(t *testing.T, bufs [][]uint16) { containerThreshold := uint64(math.MaxUint16 + 1) From a642d701ee097f9a15847283ecdcad2165c13253 Mon Sep 17 00:00:00 2001 From: Andrzej Liszka Date: Thu, 5 Dec 2024 20:08:01 +0100 Subject: [PATCH 2/6] feature: introduces CloneToBuf method --- bitmap_opt.go | 27 ++++++++++ bitmap_opt_test.go | 130 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) diff --git a/bitmap_opt.go b/bitmap_opt.go index 91188aa..a7e79c8 100644 --- a/bitmap_opt.go +++ b/bitmap_opt.go @@ -1,6 +1,7 @@ package sroar import ( + "fmt" "sync" ) @@ -680,3 +681,29 @@ func (ra *Bitmap) capInBytes() int { } return cap(ra.data) * 2 } + +func (ra *Bitmap) CloneToBuf(buf []byte) *Bitmap { + c := cap(buf) + dstbuf := buf[:c] + if c%2 != 0 { + dstbuf = buf[:c-1] + } + + src := ra + if ra == nil { + src = NewBitmap() + } + + srclen := src.LenInBytes() + if srclen > len(dstbuf) { + panic(fmt.Sprintf("Buffer too small, given %d, required %d", cap(buf), srclen)) + } + + srcbuf := toByteSlice(src.data) + copy(dstbuf, srcbuf) + + // adjust length to src length, keep capacity as entire buffer + bm := FromBuffer(dstbuf) + bm.data = bm.data[:srclen/2] + return bm +} diff --git a/bitmap_opt_test.go b/bitmap_opt_test.go index 7bb2555..5a3d8d6 100644 --- a/bitmap_opt_test.go +++ b/bitmap_opt_test.go @@ -977,6 +977,136 @@ func TestCapBytes(t *testing.T) { }) } +func TestCloneToBuf(t *testing.T) { + assertEqualBitmaps := func(t *testing.T, bm, cloned *Bitmap) { + require.Equal(t, bm.GetCardinality(), cloned.GetCardinality()) + require.Equal(t, bm.LenInBytes(), cloned.LenInBytes()) + require.ElementsMatch(t, bm.ToArray(), cloned.ToArray()) + } + + t.Run("non-nil bitmap", func(t *testing.T) { + bmEmpty := NewBitmap() + + bm1 := NewBitmap() + bm1.Set(1) + + bm2 := NewBitmap() + bm2.Set(1) + bm2.Set(1 + uint64(maxCardinality)) + bm2.Set(2 + uint64(maxCardinality)) + + bm3 := NewBitmap() + bm3.Set(1) + bm3.Set(1 + uint64(maxCardinality)) + bm3.Set(2 + uint64(maxCardinality)) + bm3.Set(1 + uint64(maxCardinality)*2) + bm3.Set(2 + uint64(maxCardinality)*2) + bm3.Set(3 + uint64(maxCardinality)*2) + + for name, bm := range map[string]*Bitmap{ + "empty": bmEmpty, + "bm1": bm1, + "bm2": bm2, + "bm3": bm3, + } { + t.Run(name, func(t *testing.T) { + lenInBytes := bm.LenInBytes() + for name, buf := range map[string][]byte{ + "buf equal len": make([]byte, lenInBytes), + "buf greater len": make([]byte, lenInBytes*3/2), + "buf equal cap": make([]byte, 0, lenInBytes), + "buf greater cap": make([]byte, 0, lenInBytes*3/2), + "buf less len greater cap": make([]byte, lenInBytes/2, lenInBytes*3/2), + } { + t.Run(name, func(t *testing.T) { + cloned := bm.CloneToBuf(buf) + + assertEqualBitmaps(t, bm, cloned) + require.Equal(t, cap(buf), cloned.capInBytes()) + }) + } + }) + } + }) + + t.Run("nil bitmap, cloned as empty bitmap", func(t *testing.T) { + var bmNil *Bitmap + bmEmpty := NewBitmap() + + buf := make([]byte, 0, bmEmpty.LenInBytes()*2) + cloned := bmNil.CloneToBuf(buf) + + assertEqualBitmaps(t, bmEmpty, cloned) + require.Equal(t, cap(buf), cloned.capInBytes()) + }) + + t.Run("source bitmap is not changed on cloned updates", func(t *testing.T) { + bm := NewBitmap() + bm.Set(1) + bmLen := bm.LenInBytes() + bmCap := bm.capInBytes() + + buf := make([]byte, 0, bm.LenInBytes()*4) + cloned := bm.CloneToBuf(buf) + cloned.Set(1 + uint64(maxCardinality)) + cloned.Set(1 + uint64(maxCardinality)*2) + + require.Equal(t, bmLen, bm.LenInBytes()) + require.Equal(t, bmCap, bm.capInBytes()) + require.Equal(t, 1, bm.GetCardinality()) + require.ElementsMatch(t, []uint64{1}, bm.ToArray()) + + require.Less(t, bmLen, cloned.LenInBytes()) + require.LessOrEqual(t, bmCap, cloned.capInBytes()) + require.Equal(t, 3, cloned.GetCardinality()) + require.Equal(t, []uint64{1, 1 + uint64(maxCardinality), 1 + uint64(maxCardinality)*2}, cloned.ToArray()) + }) + + t.Run("reuse bigger buffer to expand size", func(t *testing.T) { + bm := NewBitmap() + bm.Set(1) + + // buf big enough for additional containers + buf := make([]byte, 0, bm.LenInBytes()*4) + cloned := bm.CloneToBuf(buf) + clonedLen := cloned.LenInBytes() + clonedCap := cloned.capInBytes() + + cloned.Set(1 + uint64(maxCardinality)) + cloned.Set(1 + uint64(maxCardinality)*2) + + require.Less(t, clonedLen, cloned.LenInBytes()) + require.Equal(t, clonedCap, cloned.capInBytes()) + }) + + t.Run("panic on smaller buffer size", func(t *testing.T) { + defer func() { + r := recover() + require.NotNil(t, r) + require.Contains(t, r, "Buffer too small") + }() + + bm := NewBitmap() + bm.Set(1) + bmLen := bm.LenInBytes() + + buf := make([]byte, 0, bmLen-1) + bm.CloneToBuf(buf) + }) + + t.Run("allow buffer of odd size", func(t *testing.T) { + bm := NewBitmap() + bm.Set(1) + bmLen := bm.LenInBytes() + + buf := make([]byte, 0, bmLen+3) + cloned := bm.CloneToBuf(buf) + + require.Equal(t, bmLen, cloned.LenInBytes()) + require.Equal(t, bmLen+2, cloned.capInBytes()) + }) +} + func TestMergeToSuperset(t *testing.T) { run := func(t *testing.T, bufs [][]uint16) { containerThreshold := uint64(math.MaxUint16 + 1) From d51e101a393a8bfe685cdba2074054277d4a8038 Mon Sep 17 00:00:00 2001 From: Andrzej Liszka Date: Thu, 5 Dec 2024 12:15:38 +0100 Subject: [PATCH 3/6] chore: size configurable bitmap constructor --- bitmap.go | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/bitmap.go b/bitmap.go index 8a39029..9327654 100644 --- a/bitmap.go +++ b/bitmap.go @@ -99,21 +99,26 @@ func NewBitmap() *Bitmap { } func NewBitmapWith(numKeys int) *Bitmap { + return newBitmapWith(numKeys, minContainerSize, 0) +} + +func newBitmapWith(numKeys, initialContainerSize, additionalCapacity int) *Bitmap { if numKeys < 2 { panic("Must contain at least two keys.") } - ra := &Bitmap{ - // Each key must also keep an offset. So, we need to double the number - // of uint64s allocated. Plus, we need to make space for the first 2 - // uint64s to store the number of keys and node size. - data: make([]uint16, 4*(2*numKeys+2)), - } + keysLen := calcInitialKeysLen(numKeys) + buf := make([]uint16, keysLen+initialContainerSize+additionalCapacity) + return newBitampToBuf(keysLen, initialContainerSize, buf) +} + +func newBitampToBuf(keysLen, initialContainerSize int, buf []uint16) *Bitmap { + ra := &Bitmap{data: buf[:keysLen]} ra.keys = toUint64Slice(ra.data) - ra.keys.setNodeSize(len(ra.data)) + ra.keys.setNodeSize(keysLen) // Always generate a container for key = 0x00. Otherwise, node gets confused // about whether a zero key is a new key or not. - offset := ra.newContainer(minContainerSize) + offset := ra.newContainer(uint16(initialContainerSize)) // First two are for num keys. index=2 -> 0 key. index=3 -> offset. ra.keys.setAt(indexNodeStart+1, offset) ra.keys.setNumKeys(1) @@ -121,6 +126,13 @@ func NewBitmapWith(numKeys int) *Bitmap { return ra } +func calcInitialKeysLen(numKeys int) int { + // Each key must also keep an offset. So, we need to double the number + // of uint64s allocated. Plus, we need to make space for the first 2 + // uint64s to store the number of keys and node size. + return 4 * (2*numKeys + 2) +} + func (ra *Bitmap) initSpaceForKeys(N int) { if N == 0 { return From 2e6fef92ed99d5bff01371348d982d0d9affc922 Mon Sep 17 00:00:00 2001 From: Andrzej Liszka Date: Thu, 5 Dec 2024 12:16:04 +0100 Subject: [PATCH 4/6] feature: introduces Prefill method --- benchmark_opt_test.go | 27 ++++++++++++++ bitmap_opt.go | 85 +++++++++++++++++++++++++++++++++++++++++++ bitmap_opt_test.go | 26 +++++++++++++ 3 files changed, 138 insertions(+) diff --git a/benchmark_opt_test.go b/benchmark_opt_test.go index 49db81a..9135b57 100644 --- a/benchmark_opt_test.go +++ b/benchmark_opt_test.go @@ -5,6 +5,33 @@ import ( "testing" ) +// go test -v -bench BenchmarkPrefillNative -benchmem -run ^$ github.com/weaviate/sroar -cpuprofile cpu.prof +func BenchmarkPrefillNative(b *testing.B) { + for i := 0; i < b.N; i++ { + Prefill(200_000_000) + } +} + +// go test -v -bench BenchmarkPrefillFromSortedList -benchmem -run ^$ github.com/weaviate/sroar -cpuprofile cpu.prof +func BenchmarkPrefillFromSortedList(b *testing.B) { + prefillBufferSize := 65_536 + maxVal := uint64(200_000_000) + inc := uint64(prefillBufferSize) + buf := make([]uint64, prefillBufferSize) + + for i := 0; i < b.N; i++ { + finalBM := NewBitmap() + + for i := uint64(0); i <= maxVal; i += inc { + j := uint64(0) + for ; j < inc && i+j <= maxVal; j++ { + buf[j] = i + j + } + finalBM.Or(FromSortedList(buf[:j])) + } + } +} + // ================================================================================ // // BENCHMARKS comparing performance of different merge implementations diff --git a/bitmap_opt.go b/bitmap_opt.go index a7e79c8..338e581 100644 --- a/bitmap_opt.go +++ b/bitmap_opt.go @@ -2,6 +2,7 @@ package sroar import ( "fmt" + "math" "sync" ) @@ -707,3 +708,87 @@ func (ra *Bitmap) CloneToBuf(buf []byte) *Bitmap { bm.data = bm.data[:srclen/2] return bm } + +// Prefill creates bitmap prefilled with elements [0-maxX] +func Prefill(maxX uint64) *Bitmap { + maxCard64 := uint64(maxCardinality) + + // maxX should be included, therefore +1 + n := maxX / maxCard64 + rem := maxX % maxCard64 + if rem == maxCard64-1 { + n++ + } + rem = (rem + 1) % maxCard64 + + // create additional container for remaining values (or empty if there are not) + // +1 additional key to avoid keys expanding (there should always be 1 spare) + bm := newBitmapWith(int(n)+1+1, maxContainerSize, int(n)*maxContainerSize) + + var refContainer []uint16 + var remOffset = bm.keys.val(0) + + if n > 0 { + refContainer = bm.getContainer(remOffset) + refContainer[indexSize] = maxContainerSize + refContainer[indexType] = typeBitmap + setCardinality(refContainer, maxCardinality) + + // fill entire bitmap container with ones + refContainer64 := uint16To64SliceUnsafe(refContainer[startIdx:]) + for i := range refContainer64 { + refContainer64[i] = math.MaxUint64 + } + + // fill remaining containers by copying reference one + for i := uint64(1); i < n; i++ { + key := (i * maxCard64) & mask + offset := bm.newContainerNoClr(maxContainerSize) + bm.setKey(key, offset) + + copy(bm.data[offset:], refContainer) + } + + // create container for remaining values + key := (n * maxCard64) & mask + remOffset = bm.newContainer(maxContainerSize) + bm.setKey(key, remOffset) + } + + container := bm.getContainer(remOffset) + container[indexSize] = maxContainerSize + container[indexType] = typeBitmap + setCardinality(container, int(rem)) + + if rem > 0 { + n16 := uint16(rem) / 16 + rem16 := uint16(rem) % 16 + + if refContainer != nil { + // refContainer available (maxX >= math.MaxUint16-1), + // fill remaining values container by copying biggest possible slice of refContainer (batches of 16s) + copy(bm.data[remOffset+uint64(startIdx):], refContainer[startIdx:startIdx+n16]) + // set remaining bits + for i := uint16(0); i < rem16; i++ { + container[startIdx+n16] |= bitmapMask[i] + } + } else { + // refContainer not available (maxX < math.MaxUint16-1), + // set bits by copying MaxUint64 first, then MaxUint16, then single bits + n64 := uint16(rem) / 64 + + container64 := uint16To64SliceUnsafe(container[startIdx:]) + for i := uint16(0); i < n64; i++ { + container64[i] = math.MaxUint64 + } + for i := uint16(n64 * 4); i < n16; i++ { + container[startIdx+i] = math.MaxUint16 + } + for i := uint16(0); i < rem16; i++ { + container[startIdx+n16] |= bitmapMask[i] + } + } + } + + return bm +} diff --git a/bitmap_opt_test.go b/bitmap_opt_test.go index 5a3d8d6..30d0502 100644 --- a/bitmap_opt_test.go +++ b/bitmap_opt_test.go @@ -1107,6 +1107,32 @@ func TestCloneToBuf(t *testing.T) { }) } +func TestPrefill(t *testing.T) { + for _, maxX := range []int{ + 0, 1, 123_456, + maxCardinality / 2, + maxCardinality - 1, maxCardinality, maxCardinality + 1, + maxCardinality*3 - 1, maxCardinality * 3, maxCardinality*3 + 1, + } { + t.Run(fmt.Sprintf("value %d", maxX), func(t *testing.T) { + bm := Prefill(uint64(maxX)) + + assertPrefilled(t, bm, maxX) + }) + } +} + +func assertPrefilled(t *testing.T, bm *Bitmap, maxX int) { + require.Equal(t, maxX+1, bm.GetCardinality()) + + arr := bm.ToArray() + require.Len(t, arr, maxX+1) + + for i, x := range arr { + require.Equal(t, uint64(i), x) + } +} + func TestMergeToSuperset(t *testing.T) { run := func(t *testing.T, bufs [][]uint16) { containerThreshold := uint64(math.MaxUint16 + 1) From a00d2da3860738b1276672aa8040ca95d7d50549 Mon Sep 17 00:00:00 2001 From: Andrzej Liszka Date: Fri, 6 Dec 2024 11:15:49 +0100 Subject: [PATCH 5/6] feature: introduces FillUp method --- benchmark_opt_test.go | 38 ++ bitmap_opt.go | 335 ++++++++++--- bitmap_opt_test.go | 1041 +++++++++++++++++++++++++++++++++++++++++ keys.go | 15 + 4 files changed, 1370 insertions(+), 59 deletions(-) diff --git a/benchmark_opt_test.go b/benchmark_opt_test.go index 9135b57..928c8af 100644 --- a/benchmark_opt_test.go +++ b/benchmark_opt_test.go @@ -32,6 +32,44 @@ func BenchmarkPrefillFromSortedList(b *testing.B) { } } +// go test -v -bench BenchmarkFillUpNative -benchmem -run ^$ github.com/weaviate/sroar -cpuprofile cpu.prof +func BenchmarkFillUpNative(b *testing.B) { + for i := 0; i < b.N; i++ { + bm := Prefill(100_000_000) + bm.FillUp(150_000_000) + bm.FillUp(200_000_000) + } +} + +// go test -v -bench BenchmarkPrefillFromSortedList -benchmem -run ^$ github.com/weaviate/sroar -cpuprofile cpu.prof +func BenchmarkFillUpFromSortedList(b *testing.B) { + prefillBufferSize := 65_536 + prefillX := uint64(100_000_000) + fillupX1 := uint64(150_000_000) + fillupX2 := uint64(200_000_000) + inc := uint64(prefillBufferSize) + buf := make([]uint64, prefillBufferSize) + + for i := 0; i < b.N; i++ { + bm := Prefill(prefillX) + + for i := prefillX + 1; i <= fillupX1; i += inc { + j := uint64(0) + for ; j < inc && i+j <= fillupX1; j++ { + buf[j] = i + j + } + bm.Or(FromSortedList(buf[:j])) + } + for i := fillupX1 + 1; i <= fillupX2; i += inc { + j := uint64(0) + for ; j < inc && i+j <= fillupX2; j++ { + buf[j] = i + j + } + bm.Or(FromSortedList(buf[:j])) + } + } +} + // ================================================================================ // // BENCHMARKS comparing performance of different merge implementations diff --git a/bitmap_opt.go b/bitmap_opt.go index 338e581..703a6b6 100644 --- a/bitmap_opt.go +++ b/bitmap_opt.go @@ -711,84 +711,301 @@ func (ra *Bitmap) CloneToBuf(buf []byte) *Bitmap { // Prefill creates bitmap prefilled with elements [0-maxX] func Prefill(maxX uint64) *Bitmap { - maxCard64 := uint64(maxCardinality) - - // maxX should be included, therefore +1 - n := maxX / maxCard64 - rem := maxX % maxCard64 - if rem == maxCard64-1 { - n++ - } - rem = (rem + 1) % maxCard64 + containersCount, remainingCount := calcFullContainersAndRemainingCounts(maxX) - // create additional container for remaining values (or empty if there are not) + // create additional container for remaining values + // (or reserve space for new one if there are not any remaining) // +1 additional key to avoid keys expanding (there should always be 1 spare) - bm := newBitmapWith(int(n)+1+1, maxContainerSize, int(n)*maxContainerSize) + bm := newBitmapWith(int(containersCount)+1+1, maxContainerSize, int(containersCount)*maxContainerSize) + bm.prefill(containersCount, remainingCount) + return bm +} - var refContainer []uint16 - var remOffset = bm.keys.val(0) +// FillUp fill bitmap with elements (maximum-maxX], where maximum means last element. +// If bitmap is empty then [0-maxX] elements are added +// (reusing underlying data slice if big enough to fit all elements). +// If last element is >= than given maxX nothing is done. +func (ra *Bitmap) FillUp(maxX uint64) { + if ra == nil { + return + } - if n > 0 { - refContainer = bm.getContainer(remOffset) - refContainer[indexSize] = maxContainerSize - refContainer[indexType] = typeBitmap - setCardinality(refContainer, maxCardinality) + maxContainersCount, maxRemainingCount := calcFullContainersAndRemainingCounts(maxX) + if ra.IsEmpty() { + // try to fit data into existing memory, + // if there is not enough space anyway, allocate more memory to fit additional container + minimalContainersCount := maxContainersCount + if maxRemainingCount > 0 { + minimalContainersCount++ + } + minimalKeysLen := calcInitialKeysLen(minimalContainersCount + 1) + minimalLen := minimalKeysLen + minimalContainersCount*maxContainerSize - // fill entire bitmap container with ones - refContainer64 := uint16To64SliceUnsafe(refContainer[startIdx:]) - for i := range refContainer64 { - refContainer64[i] = math.MaxUint64 + var bm *Bitmap + if minimalLen <= cap(ra.data) { + bm = newBitampToBuf(minimalKeysLen, maxContainerSize, ra.data) + } else { + bm = newBitmapWith(int(maxContainersCount)+1+1, maxContainerSize, int(maxContainersCount)*maxContainerSize) } + bm.prefill(maxContainersCount, maxRemainingCount) + ra.data = bm.data + ra._ptr = bm._ptr + ra.keys = bm.keys + return + } - // fill remaining containers by copying reference one - for i := uint64(1); i < n; i++ { - key := (i * maxCard64) & mask - offset := bm.newContainerNoClr(maxContainerSize) - bm.setKey(key, offset) + minX := ra.Maximum() + if minX >= maxX { + return + } - copy(bm.data[offset:], refContainer) + maxKey := maxX & mask + minKey := minX & mask + maxY := int(uint16(maxX)) + minY := int(uint16(minX)) + + idx := ra.keys.searchReverse(minKey) + minOffset := ra.keys.val(idx) + + // same container + if maxKey == minKey { + commonContainer := ra.getContainer(minOffset) + card := getCardinality(commonContainer) + newYs := maxY - minY + + switch commonContainer[indexType] { + case typeBitmap: + // set bits in bitmap + bitmap(commonContainer).setRange(minY, maxY, nil) + setCardinality(commonContainer, card+newYs) + case typeArray: + size := commonContainer[indexSize] + if spaceLeft := int(size-startIdx) - card; spaceLeft >= newYs { + // add elements to existing array + for i := 0; i < newYs; i++ { + commonContainer[startIdx+uint16(card+i)] = uint16(minY + 1 + i) + } + setCardinality(commonContainer, card+newYs) + } else { + // create new bitmap container, copy elements from array, set new bits + prevContainer := commonContainer + minOffset = ra.newContainer(maxContainerSize) + ra.setKey(minKey, minOffset) + + commonContainer = ra.fillUpBitmapContainerRange(minOffset, minY, maxY, card+newYs, nil) + for i := 0; i < card; i++ { + y := prevContainer[startIdx+uint16(i)] + commonContainer[startIdx+y/16] |= bitmapMask[y%16] + } + } + default: + panic("unknown container type") } + return + } - // create container for remaining values - key := (n * maxCard64) & mask - remOffset = bm.newContainer(maxContainerSize) - bm.setKey(key, remOffset) + minContainersCount, minRemainingCount := calcFullContainersAndRemainingCounts(minX) + requiredContainersCount := maxContainersCount - minContainersCount + if maxRemainingCount > 0 { + requiredContainersCount++ } - container := bm.getContainer(remOffset) - container[indexSize] = maxContainerSize - container[indexType] = typeBitmap - setCardinality(container, int(rem)) + // first count how many new containers will be required to allocate memory once, then do the fillup + var fillUpCommonContainer func(commonContainer []uint16, onesBitmap bitmap) = nil + // idx of first full container to be added + containerIdx := minContainersCount + if minRemainingCount > 0 { + containerIdx++ + requiredContainersCount-- - if rem > 0 { - n16 := uint16(rem) / 16 - rem16 := uint16(rem) % 16 + commonContainer := ra.getContainer(minOffset) + card := getCardinality(commonContainer) + newYs := maxCardinality - 1 - minY - if refContainer != nil { - // refContainer available (maxX >= math.MaxUint16-1), - // fill remaining values container by copying biggest possible slice of refContainer (batches of 16s) - copy(bm.data[remOffset+uint64(startIdx):], refContainer[startIdx:startIdx+n16]) - // set remaining bits - for i := uint16(0); i < rem16; i++ { - container[startIdx+n16] |= bitmapMask[i] + switch commonContainer[indexType] { + case typeBitmap: + // if bitmap, set proper bits up to maxCardinality + fillUpCommonContainer = func(commonContainer []uint16, onesBitmap bitmap) { + bitmap(commonContainer).setRange(minY, maxCardinality-1, onesBitmap) + setCardinality(commonContainer, card+newYs) + } + case typeArray: + size := commonContainer[indexSize] + if spaceLeft := int(size-startIdx) - card; spaceLeft >= newYs { + // if array add new elements if there is enough space left + fillUpCommonContainer = func(commonContainer []uint16, onesBitmap bitmap) { + for i := 0; i < newYs; i++ { + commonContainer[startIdx+uint16(card+i)] = uint16(minY + 1 + i) + } + setCardinality(commonContainer, card+newYs) + } + } else { + // if not enough space, create new bitmap container, set new bits and set old ones + requiredContainersCount++ + fillUpCommonContainer = func(commonContainer []uint16, onesBitmap bitmap) { + prevContainer := commonContainer + offset := ra.newContainer(maxContainerSize) + ra.setKey(minKey, offset) + + commonContainer = ra.fillUpBitmapContainerRange(offset, minY, maxCardinality-1, card+newYs, onesBitmap) + for i := 0; i < card; i++ { + y := prevContainer[startIdx+uint16(i)] + commonContainer[startIdx+y/16] |= bitmapMask[y%16] + } + } } + default: + panic("unknown container type") + } + } + + // calculate required memory to allocate and expand underlying slice + containersLen := uint64(requiredContainersCount * maxContainerSize) + keysLen := uint64(requiredContainersCount * 2 * 4) + ra.expandNoLengthChange(containersLen + keysLen) + ra.expandKeys(keysLen) + + var onesBitmap bitmap + if containerIdx < maxContainersCount { + // fillup full containers + key := uint64(containerIdx*maxCardinality) & mask + offset := ra.newContainerNoClr(maxContainerSize) + ra.setKey(key, offset) + + onesBitmap = ra.fillUpBitmapContainers(offset, containerIdx+1, maxContainersCount) + } + if maxRemainingCount > 0 { + // fillup last (highest) container + key := uint64(maxContainersCount*maxCardinality) & mask + offset := ra.newContainer(maxContainerSize) + ra.setKey(key, offset) + + ra.fillUpBitmapContainerRange(offset, 0, maxRemainingCount-1, maxRemainingCount, onesBitmap) + } + if minRemainingCount > 0 { + // fillup common container using previously created callback. + // due to slice expanding, container has to be fetched once using new offset + minOffset = ra.keys.val(idx) + commonContainer := ra.getContainer(minOffset) + fillUpCommonContainer(commonContainer, onesBitmap) + } +} + +// prefill prefills containersCount full containers +// and last one with remainingCount first values +func (ra *Bitmap) prefill(containersCount, remainingCount int) { + var onesBitmap bitmap + if containersCount > 0 { + offset := ra.keys.val(0) + onesBitmap = ra.fillUpBitmapContainers(offset, 1, containersCount) + } + if remainingCount > 0 { + var offset uint64 + if containersCount > 0 { + // create container for remaining values + key := uint64(containersCount*maxCardinality) & mask + offset = ra.newContainer(maxContainerSize) + ra.setKey(key, offset) } else { - // refContainer not available (maxX < math.MaxUint16-1), - // set bits by copying MaxUint64 first, then MaxUint16, then single bits - n64 := uint16(rem) / 64 + // get initial container + offset = ra.keys.val(0) + } + ra.fillUpBitmapContainerRange(offset, 0, remainingCount-1, remainingCount, onesBitmap) + } +} - container64 := uint16To64SliceUnsafe(container[startIdx:]) - for i := uint16(0); i < n64; i++ { - container64[i] = math.MaxUint64 - } - for i := uint16(n64 * 4); i < n16; i++ { - container[startIdx+i] = math.MaxUint16 - } - for i := uint16(0); i < rem16; i++ { - container[startIdx+n16] |= bitmapMask[i] +// fillUpBitmapContainerRange gets container by offset, sets its type to bitmap, +// sets bits in range minY-maxY (both included) +func (ra *Bitmap) fillUpBitmapContainerRange(offset uint64, minY, maxY, card int, onesBitmap bitmap) bitmap { + b := bitmap(ra.getContainer(offset)) + b[indexSize] = maxContainerSize + b[indexType] = typeBitmap + setCardinality(b, card) + + b.setRange(minY, maxY, onesBitmap) + return b +} + +// fillUpBitmapContainers gets container by offset, sets its type to bitmap, +// sets all bits, +// then creates [minIdx-maxIdx) containers with all bits set, by copying first container +func (ra *Bitmap) fillUpBitmapContainers(offset uint64, minIdx, maxIdx int) bitmap { + ones := bitmap(ra.getContainer(offset)[:maxContainerSize]) + ones[indexSize] = maxContainerSize + ones[indexType] = typeBitmap + setCardinality(ones, maxCardinality) + + // fill entire bitmap container with ones + ones.fillWithOnes() + + // fill remaining containers by copying first one + for i := minIdx; i < maxIdx; i++ { + key := uint64(i*maxCardinality) & mask + offset := ra.newContainerNoClr(maxContainerSize) + ra.setKey(key, offset) + copy(ra.data[offset:], ones) + } + return ones +} + +func calcFullContainersAndRemainingCounts(maxX uint64) (int, int) { + maxCard64 := uint64(maxCardinality) + + // maxX should be included, therefore +1 + containers := maxX / maxCard64 + remaining := maxX % maxCard64 + if remaining == maxCard64-1 { + containers++ + } + remaining = (remaining + 1) % maxCard64 + return int(containers), int(remaining) +} + +func (b bitmap) setRange(minY, maxY int, onesBitmap bitmap) { + minY16 := (minY + 15) / 16 * 16 + maxY16 := (maxY + 1) / 16 * 16 + + // fmt.Printf(" ==> minY [%d] minY16 [%d] minY64 [%d]\n", minY, minY16, (minY+63)/64*64) + // fmt.Printf(" ==> maxY [%d] maxY16 [%d] maxY64 [%d]\n", maxY, maxY16, (maxY+1)/64*64) + + b16 := b[startIdx:] + if onesBitmap != nil { + if mn, mx := uint16(minY16/16), uint16(maxY16/16); mn < mx { + copy(b16[mn:mx], onesBitmap[startIdx+mn:startIdx+mx]) + } + } else { + minY64 := (minY + 63) / 64 * 64 + maxY64 := (maxY + 1) / 64 * 64 + + if mn, mx := minY64/64, maxY64/64; mn < mx { + b64 := uint16To64SliceUnsafe(b16) + for i := mn; i < mx; i++ { + // fmt.Printf(" ==> b64 i=%d\n", i) + b64[i] = math.MaxUint64 } } + for i, mx := minY16/16, min(minY64/16, maxY16/16); i < mx; i++ { + // fmt.Printf(" ==> b64L i=%d\n", i) + b16[i] = math.MaxUint16 + } + for i, mx := max(minY16/16, maxY64/16), maxY16/16; i < mx; i++ { + // fmt.Printf(" ==> b64R i=%d\n", i) + b16[i] = math.MaxUint16 + } + } + for y, mx := minY, min(minY16, maxY+1); y < mx; y++ { + // fmt.Printf(" ==> b16L i=%d bit=%d\n", y/16, y%16) + b16[y/16] |= bitmapMask[y%16] + } + for y, mx := max(minY, maxY16), maxY+1; y < mx; y++ { + // fmt.Printf(" ==> b16R i=%d bit=%d\n", y/16, y%16) + b16[y/16] |= bitmapMask[y%16] } +} - return bm +func (b bitmap) fillWithOnes() { + b64 := uint16To64SliceUnsafe(b[startIdx:]) + for i := range b64 { + b64[i] = math.MaxUint64 + } } diff --git a/bitmap_opt_test.go b/bitmap_opt_test.go index 30d0502..6a2387b 100644 --- a/bitmap_opt_test.go +++ b/bitmap_opt_test.go @@ -3,6 +3,7 @@ package sroar import ( "fmt" "math" + "math/bits" "math/rand" "testing" @@ -1122,6 +1123,901 @@ func TestPrefill(t *testing.T) { } } +func TestFillUp(t *testing.T) { + t.Run("nil bitmap, noop", func(t *testing.T) { + maxX := maxCardinality + 1 + var bmNil *Bitmap + bmNil.FillUp(uint64(maxX)) + + require.Nil(t, bmNil) + }) + + t.Run("empty small bitmap, resized", func(t *testing.T) { + maxX := maxCardinality + 1 + bmSmall := NewBitmap() + lenBytes := bmSmall.LenInBytes() + capBytes := bmSmall.capInBytes() + + bmSmall.FillUp(uint64(maxX)) + require.Less(t, lenBytes, bmSmall.LenInBytes()) + require.Less(t, capBytes, bmSmall.capInBytes()) + + // + 8 (key) + 2x 4100 container - 64 container + addLen := 2 * (8 + maxContainerSize*2 - minContainerSize) + require.Equal(t, lenBytes+addLen, bmSmall.LenInBytes()) + require.Equal(t, capBytes+addLen, bmSmall.capInBytes()) + + assertPrefilled(t, bmSmall, maxX) + }) + + t.Run("empty big bitmap, reused", func(t *testing.T) { + maxX := maxCardinality + 1 + bmBig := NewBitmap() + bmBig.expandNoLengthChange(3 * maxContainerSize) // big enough to fit 2x fullsize container + lenBytes := bmBig.LenInBytes() + capBytes := bmBig.capInBytes() + + bmBig.FillUp(uint64(maxX)) + require.Less(t, lenBytes, bmBig.LenInBytes()) + require.Equal(t, capBytes, bmBig.capInBytes()) + + // + 8 (key) + 2x 4100 container - 64 container + addLen := 2 * (8 + maxContainerSize*2 - minContainerSize) + require.Equal(t, lenBytes+addLen, bmBig.LenInBytes()) + + assertPrefilled(t, bmBig, maxX) + }) + + t.Run("max value already >= than given maxX, noop", func(t *testing.T) { + maxX := maxCardinality + 1 + + t.Run("prefilled", func(t *testing.T) { + bm := Prefill(uint64(maxX)) + lenBytes := bm.LenInBytes() + capBytes := bm.capInBytes() + + bm.FillUp(uint64(maxX - 10)) + require.Equal(t, lenBytes, bm.LenInBytes()) + require.Equal(t, capBytes, bm.capInBytes()) + + bm.FillUp(uint64(maxX)) + require.Equal(t, lenBytes, bm.LenInBytes()) + require.Equal(t, capBytes, bm.capInBytes()) + }) + + t.Run("single element", func(t *testing.T) { + bm := NewBitmap() + bm.Set(uint64(maxX)) + lenBytes := bm.LenInBytes() + capBytes := bm.capInBytes() + + bm.FillUp(uint64(maxX - 10)) + require.Equal(t, lenBytes, bm.LenInBytes()) + require.Equal(t, capBytes, bm.capInBytes()) + + bm.FillUp(uint64(maxX)) + require.Equal(t, lenBytes, bm.LenInBytes()) + require.Equal(t, capBytes, bm.capInBytes()) + }) + }) + + t.Run("current max value in same container as given maxX", func(t *testing.T) { + t.Run("prefilled bitmap, no resize", func(t *testing.T) { + for _, prefillX := range []int{ + 1023, 1024, 1025, 1039, 1040, 1041, + } { + for _, fillUpX := range []int{ + 4095, 4096, 4097, 4111, 4112, 4113, maxCardinality - 2, maxCardinality - 1, + } { + t.Run(fmt.Sprintf("filled up 1x %d to %d", prefillX, fillUpX), func(t *testing.T) { + prefilled := Prefill(uint64(prefillX)) + lenBytes := prefilled.LenInBytes() + capBytes := prefilled.capInBytes() + + prefilled.FillUp(uint64(fillUpX)) + require.Equal(t, lenBytes, prefilled.LenInBytes()) + require.Equal(t, capBytes, prefilled.capInBytes()) + + assertPrefilled(t, prefilled, fillUpX) + }) + + t.Run(fmt.Sprintf("filled up 3x %d to %d", prefillX, fillUpX), func(t *testing.T) { + prefilled := Prefill(uint64(prefillX)) + lenBytes := prefilled.LenInBytes() + capBytes := prefilled.capInBytes() + + prefilled.FillUp(uint64(fillUpX) - 20) + prefilled.FillUp(uint64(fillUpX) - 10) + prefilled.FillUp(uint64(fillUpX)) + require.Equal(t, lenBytes, prefilled.LenInBytes()) + require.Equal(t, capBytes, prefilled.capInBytes()) + + assertPrefilled(t, prefilled, fillUpX) + }) + } + } + }) + + t.Run("single elem array, no resize", func(t *testing.T) { + for _, currentMaxX := range []int{ + 1023, 1024, 1025, 1039, 1040, 1041, + } { + for _, fillUpX := range []int{ + 1055, 1056, 1057, 1082, + } { + t.Run(fmt.Sprintf("filled 1x %d to %d", currentMaxX, fillUpX), func(t *testing.T) { + singleElem := NewBitmap() + singleElem.Set(uint64(currentMaxX)) + lenBytes := singleElem.LenInBytes() + capBytes := singleElem.capInBytes() + + singleElem.FillUp(uint64(fillUpX)) + require.Equal(t, lenBytes, singleElem.LenInBytes()) + require.Equal(t, capBytes, singleElem.capInBytes()) + + assertFilledUp(t, singleElem, currentMaxX, fillUpX) + }) + + t.Run(fmt.Sprintf("filled 3x %d to %d", currentMaxX, fillUpX), func(t *testing.T) { + singleElem := NewBitmap() + singleElem.Set(uint64(currentMaxX)) + lenBytes := singleElem.LenInBytes() + capBytes := singleElem.capInBytes() + + singleElem.FillUp(uint64(fillUpX) - 10) + singleElem.FillUp(uint64(fillUpX) - 5) + singleElem.FillUp(uint64(fillUpX)) + require.Equal(t, lenBytes, singleElem.LenInBytes()) + require.Equal(t, capBytes, singleElem.capInBytes()) + + assertFilledUp(t, singleElem, currentMaxX, fillUpX) + }) + } + } + }) + + t.Run("single elem array, convert to bitmap", func(t *testing.T) { + for _, currentMaxX := range []int{ + 1023, 1024, 1025, 1039, 1040, 1041, + } { + for _, fillUpX := range []int{ + 4095, 4096, 4097, maxCardinality - 1, + } { + t.Run(fmt.Sprintf("filled 1x %d to %d", currentMaxX, fillUpX), func(t *testing.T) { + singleElem := NewBitmap() + singleElem.Set(uint64(currentMaxX)) + singleElem.expandNoLengthChange(maxContainerSize) + lenBytes := singleElem.LenInBytes() + capBytes := singleElem.capInBytes() + + singleElem.FillUp(uint64(fillUpX)) + require.Less(t, lenBytes, singleElem.LenInBytes()) + require.Equal(t, capBytes, singleElem.capInBytes()) + + // + 4100 container + addLen := 2 * maxContainerSize + require.Equal(t, lenBytes+addLen, singleElem.LenInBytes()) + + assertFilledUp(t, singleElem, currentMaxX, fillUpX) + }) + + t.Run(fmt.Sprintf("filled 3x %d to %d", currentMaxX, fillUpX), func(t *testing.T) { + singleElem := NewBitmap() + singleElem.Set(uint64(currentMaxX)) + singleElem.expandNoLengthChange(maxContainerSize) + lenBytes := singleElem.LenInBytes() + capBytes := singleElem.capInBytes() + + singleElem.FillUp(uint64(fillUpX) - 3040) + singleElem.FillUp(uint64(fillUpX) - 1000) + singleElem.FillUp(uint64(fillUpX)) + require.Less(t, lenBytes, singleElem.LenInBytes()) + require.Equal(t, capBytes, singleElem.capInBytes()) + + // + 4100 container + addLen := 2 * maxContainerSize + require.Equal(t, lenBytes+addLen, singleElem.LenInBytes()) + + assertFilledUp(t, singleElem, currentMaxX, fillUpX) + }) + } + } + }) + }) + + t.Run("current max value in different container than given maxX", func(t *testing.T) { + addDouble := func(prevVal int) int { return 2 * prevVal } + addContainers := func(containersCount int) func(int) int { + return func(prevVal int) int { + // Xx (8 key + 4100 container) + return prevVal + 2*containersCount*(8+maxContainerSize) + } + } + + t.Run("prefilled bitmap", func(t *testing.T) { + for _, tc := range []struct { + prefillX int + fillUpX int + fnExpAddLen func(prevLen int) (newLen int) + fnExpAddCap func(prevCap int) (newCap int) + fnExp3xAddLen func(prevLen int) (newLen int) + fnExp3xAddCap func(prevCap int) (newCap int) + }{ + { + prefillX: maxCardinality - 100, + fillUpX: maxCardinality, + fnExpAddLen: addContainers(1), + fnExpAddCap: addDouble, + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addDouble, + }, + { + prefillX: maxCardinality - 100, + fillUpX: maxCardinality + 1022, + fnExpAddLen: addContainers(1), + fnExpAddCap: addDouble, + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addDouble, + }, + { + prefillX: maxCardinality - 100, + fillUpX: maxCardinality + 1023, + fnExpAddLen: addContainers(1), + fnExpAddCap: addDouble, + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addDouble, + }, + { + prefillX: maxCardinality - 100, + fillUpX: maxCardinality + 1024, + fnExpAddLen: addContainers(1), + fnExpAddCap: addDouble, + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addDouble, + }, + { + prefillX: maxCardinality - 100, + fillUpX: 5*maxCardinality - 1, + fnExpAddLen: addContainers(4), + fnExpAddCap: addContainers(4), + fnExp3xAddLen: addContainers(4), + fnExp3xAddCap: addContainers(4), + }, + { + prefillX: maxCardinality - 100, + fillUpX: 5 * maxCardinality, + fnExpAddLen: addContainers(5), + fnExpAddCap: addContainers(5), + fnExp3xAddLen: addContainers(5), + fnExp3xAddCap: func(prevCap int) int { + // first 4 containers were added, then cap was doubled + return addDouble(addContainers(4)(prevCap)) + }, + }, + { + prefillX: maxCardinality - 100, + fillUpX: 5*maxCardinality + 1, + fnExpAddLen: addContainers(5), + fnExpAddCap: addContainers(5), + fnExp3xAddLen: addContainers(5), + fnExp3xAddCap: func(prevCap int) int { + // first 4 containers were added, then cap was doubled + return addDouble(addContainers(4)(prevCap)) + }, + }, + + { + prefillX: maxCardinality - 50, + fillUpX: maxCardinality, + fnExpAddLen: addContainers(1), + fnExpAddCap: addDouble, + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addDouble, + }, + { + prefillX: maxCardinality - 50, + fillUpX: maxCardinality + 1022, + fnExpAddLen: addContainers(1), + fnExpAddCap: addDouble, + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addDouble, + }, + { + prefillX: maxCardinality - 50, + fillUpX: maxCardinality + 1023, + fnExpAddLen: addContainers(1), + fnExpAddCap: addDouble, + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addDouble, + }, + { + prefillX: maxCardinality - 50, + fillUpX: maxCardinality + 1024, + fnExpAddLen: addContainers(1), + fnExpAddCap: addDouble, + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addDouble, + }, + { + prefillX: maxCardinality - 50, + fillUpX: 5*maxCardinality - 1, + fnExpAddLen: addContainers(4), + fnExpAddCap: addContainers(4), + fnExp3xAddLen: addContainers(4), + fnExp3xAddCap: addContainers(4), + }, + { + prefillX: maxCardinality - 50, + fillUpX: 5 * maxCardinality, + fnExpAddLen: addContainers(5), + fnExpAddCap: addContainers(5), + fnExp3xAddLen: addContainers(5), + fnExp3xAddCap: func(prevCap int) int { + // first 4 containers were added, then cap was doubled + return addDouble(addContainers(4)(prevCap)) + }, + }, + { + prefillX: maxCardinality - 50, + fillUpX: 5*maxCardinality + 1, + fnExpAddLen: addContainers(5), + fnExpAddCap: addContainers(5), + fnExp3xAddLen: addContainers(5), + fnExp3xAddCap: func(prevCap int) int { + // first 4 containers were added, then cap was doubled + return addDouble(addContainers(4)(prevCap)) + }, + }, + + { + prefillX: maxCardinality - 1, + fillUpX: maxCardinality, + fnExpAddLen: addContainers(1), + fnExpAddCap: addDouble, + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addDouble, + }, + { + prefillX: maxCardinality - 1, + fillUpX: maxCardinality + 1022, + fnExpAddLen: addContainers(1), + fnExpAddCap: addDouble, + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addDouble, + }, + { + prefillX: maxCardinality - 1, + fillUpX: maxCardinality + 1023, + fnExpAddLen: addContainers(1), + fnExpAddCap: addDouble, + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addDouble, + }, + { + prefillX: maxCardinality - 1, + fillUpX: maxCardinality + 1024, + fnExpAddLen: addContainers(1), + fnExpAddCap: addDouble, + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addDouble, + }, + { + prefillX: maxCardinality - 1, + fillUpX: 5*maxCardinality - 1, + fnExpAddLen: addContainers(4), + fnExpAddCap: addContainers(4), + fnExp3xAddLen: addContainers(4), + fnExp3xAddCap: addContainers(4), + }, + { + prefillX: maxCardinality - 1, + fillUpX: 5 * maxCardinality, + fnExpAddLen: addContainers(5), + fnExpAddCap: addContainers(5), + fnExp3xAddLen: addContainers(5), + fnExp3xAddCap: func(prevCap int) int { + // first 4 containers were added, then cap was doubled + return addDouble(addContainers(4)(prevCap)) + }, + }, + { + prefillX: maxCardinality - 1, + fillUpX: 5*maxCardinality + 1, + fnExpAddLen: addContainers(5), + fnExpAddCap: addContainers(5), + fnExp3xAddLen: addContainers(5), + fnExp3xAddCap: func(prevCap int) int { + // first 4 containers were added, then cap was doubled + return addDouble(addContainers(4)(prevCap)) + }, + }, + } { + t.Run(fmt.Sprintf("filled up 1x %d to %d", tc.prefillX, tc.fillUpX), func(t *testing.T) { + prefilled := Prefill(uint64(tc.prefillX)) + lenBytes := prefilled.LenInBytes() + capBytes := prefilled.capInBytes() + + prefilled.FillUp(uint64(tc.fillUpX)) + require.Equal(t, tc.fnExpAddLen(lenBytes), prefilled.LenInBytes()) + require.Equal(t, tc.fnExpAddCap(capBytes), prefilled.capInBytes()) + + assertPrefilled(t, prefilled, tc.fillUpX) + }) + + t.Run(fmt.Sprintf("filled up 3x %d to %d", tc.prefillX, tc.fillUpX), func(t *testing.T) { + prefilled := Prefill(uint64(tc.prefillX)) + lenBytes := prefilled.LenInBytes() + capBytes := prefilled.capInBytes() + + prefilled.FillUp(uint64(tc.fillUpX) - 20) + prefilled.FillUp(uint64(tc.fillUpX) - 10) + prefilled.FillUp(uint64(tc.fillUpX)) + require.Equal(t, tc.fnExp3xAddLen(lenBytes), prefilled.LenInBytes()) + require.Equal(t, tc.fnExp3xAddCap(capBytes), prefilled.capInBytes()) + + assertPrefilled(t, prefilled, tc.fillUpX) + }) + } + }) + + t.Run("single elem array, keep common array", func(t *testing.T) { + for _, tc := range []struct { + currentMaxX int + fillUpX int + fnExpAddLen func(prevLen int) (newLen int) + fnExpAddCap func(prevCap int) (newCap int) + fnExp3xAddLen func(prevLen int) (newLen int) + fnExp3xAddCap func(prevCap int) (newCap int) + }{ + { + currentMaxX: maxCardinality - 20, + fillUpX: maxCardinality, + fnExpAddLen: addContainers(1), + fnExpAddCap: addContainers(1), + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addContainers(1), + }, + { + currentMaxX: maxCardinality - 20, + fillUpX: maxCardinality + 1022, + fnExpAddLen: addContainers(1), + fnExpAddCap: addContainers(1), + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addContainers(1), + }, + { + currentMaxX: maxCardinality - 20, + fillUpX: maxCardinality + 1023, + fnExpAddLen: addContainers(1), + fnExpAddCap: addContainers(1), + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addContainers(1), + }, + { + currentMaxX: maxCardinality - 20, + fillUpX: maxCardinality + 1024, + fnExpAddLen: addContainers(1), + fnExpAddCap: addContainers(1), + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addContainers(1), + }, + { + currentMaxX: maxCardinality - 20, + fillUpX: 3*maxCardinality - 1, + fnExpAddLen: addContainers(2), + fnExpAddCap: addContainers(2), + fnExp3xAddLen: addContainers(2), + fnExp3xAddCap: addContainers(2), + }, + { + currentMaxX: maxCardinality - 20, + fillUpX: 3 * maxCardinality, + fnExpAddLen: addContainers(3), + fnExpAddCap: addContainers(3), + fnExp3xAddLen: addContainers(3), + fnExp3xAddCap: func(prevCap int) int { + return addDouble(addContainers(2)(prevCap)) + }, + }, + { + currentMaxX: maxCardinality - 20, + fillUpX: 3*maxCardinality + 1, + fnExpAddLen: addContainers(3), + fnExpAddCap: addContainers(3), + fnExp3xAddLen: addContainers(3), + fnExp3xAddCap: func(prevCap int) int { + return addDouble(addContainers(2)(prevCap)) + }, + }, + + { + currentMaxX: maxCardinality - 10, + fillUpX: maxCardinality, + fnExpAddLen: addContainers(1), + fnExpAddCap: addContainers(1), + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addContainers(1), + }, + { + currentMaxX: maxCardinality - 10, + fillUpX: maxCardinality + 1022, + fnExpAddLen: addContainers(1), + fnExpAddCap: addContainers(1), + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addContainers(1), + }, + { + currentMaxX: maxCardinality - 10, + fillUpX: maxCardinality + 1023, + fnExpAddLen: addContainers(1), + fnExpAddCap: addContainers(1), + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addContainers(1), + }, + { + currentMaxX: maxCardinality - 10, + fillUpX: maxCardinality + 1024, + fnExpAddLen: addContainers(1), + fnExpAddCap: addContainers(1), + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addContainers(1), + }, + { + currentMaxX: maxCardinality - 10, + fillUpX: 3*maxCardinality - 1, + fnExpAddLen: addContainers(2), + fnExpAddCap: addContainers(2), + fnExp3xAddLen: addContainers(2), + fnExp3xAddCap: addContainers(2), + }, + { + currentMaxX: maxCardinality - 10, + fillUpX: 3 * maxCardinality, + fnExpAddLen: addContainers(3), + fnExpAddCap: addContainers(3), + fnExp3xAddLen: addContainers(3), + fnExp3xAddCap: func(prevCap int) int { + return addDouble(addContainers(2)(prevCap)) + }, + }, + { + currentMaxX: maxCardinality - 10, + fillUpX: 3*maxCardinality + 1, + fnExpAddLen: addContainers(3), + fnExpAddCap: addContainers(3), + fnExp3xAddLen: addContainers(3), + fnExp3xAddCap: func(prevCap int) int { + return addDouble(addContainers(2)(prevCap)) + }, + }, + + { + currentMaxX: maxCardinality - 1, + fillUpX: maxCardinality, + fnExpAddLen: addContainers(1), + fnExpAddCap: addContainers(1), + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addContainers(1), + }, + { + currentMaxX: maxCardinality - 1, + fillUpX: maxCardinality + 1022, + fnExpAddLen: addContainers(1), + fnExpAddCap: addContainers(1), + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addContainers(1), + }, + { + currentMaxX: maxCardinality - 1, + fillUpX: maxCardinality + 1023, + fnExpAddLen: addContainers(1), + fnExpAddCap: addContainers(1), + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addContainers(1), + }, + { + currentMaxX: maxCardinality - 1, + fillUpX: maxCardinality + 1024, + fnExpAddLen: addContainers(1), + fnExpAddCap: addContainers(1), + fnExp3xAddLen: addContainers(1), + fnExp3xAddCap: addContainers(1), + }, + { + currentMaxX: maxCardinality - 1, + fillUpX: 3*maxCardinality - 1, + fnExpAddLen: addContainers(2), + fnExpAddCap: addContainers(2), + fnExp3xAddLen: addContainers(2), + fnExp3xAddCap: addContainers(2), + }, + { + currentMaxX: maxCardinality - 1, + fillUpX: 3 * maxCardinality, + fnExpAddLen: addContainers(3), + fnExpAddCap: addContainers(3), + fnExp3xAddLen: addContainers(3), + fnExp3xAddCap: func(prevCap int) int { + return addDouble(addContainers(2)(prevCap)) + }, + }, + { + currentMaxX: maxCardinality - 1, + fillUpX: 3*maxCardinality + 1, + fnExpAddLen: addContainers(3), + fnExpAddCap: addContainers(3), + fnExp3xAddLen: addContainers(3), + fnExp3xAddCap: func(prevCap int) int { + return addDouble(addContainers(2)(prevCap)) + }, + }, + } { + t.Run(fmt.Sprintf("filled up 1x %d to %d", tc.currentMaxX, tc.fillUpX), func(t *testing.T) { + singleElem := NewBitmap() + singleElem.Set(uint64(tc.currentMaxX)) + lenBytes := singleElem.LenInBytes() + capBytes := singleElem.capInBytes() + + singleElem.FillUp(uint64(tc.fillUpX)) + require.Equal(t, tc.fnExpAddLen(lenBytes), singleElem.LenInBytes()) + require.Equal(t, tc.fnExpAddCap(capBytes), singleElem.capInBytes()) + + assertFilledUp(t, singleElem, tc.currentMaxX, tc.fillUpX) + }) + + t.Run(fmt.Sprintf("filled up 3x %d to %d", tc.currentMaxX, tc.fillUpX), func(t *testing.T) { + singleElem := NewBitmap() + singleElem.Set(uint64(tc.currentMaxX)) + lenBytes := singleElem.LenInBytes() + capBytes := singleElem.capInBytes() + + singleElem.FillUp(uint64(tc.fillUpX) - 20) + singleElem.FillUp(uint64(tc.fillUpX) - 10) + singleElem.FillUp(uint64(tc.fillUpX)) + require.Equal(t, tc.fnExp3xAddLen(lenBytes), singleElem.LenInBytes()) + require.Equal(t, tc.fnExp3xAddCap(capBytes), singleElem.capInBytes()) + + assertFilledUp(t, singleElem, tc.currentMaxX, tc.fillUpX) + }) + } + }) + + t.Run("single elem array, convert common to bitmap", func(t *testing.T) { + for _, tc := range []struct { + currentMaxX int + fillUpX int + fnExpAddLen func(prevLen int) (newLen int) + fnExpAddCap func(prevCap int) (newCap int) + fnExp3xAddLen func(prevLen int) (newLen int) + fnExp3xAddCap func(prevCap int) (newCap int) + }{ + { + currentMaxX: maxCardinality - 200, + fillUpX: maxCardinality, + fnExpAddLen: addContainers(2), + fnExpAddCap: addContainers(2), + fnExp3xAddLen: func(prevLen int) int { + return addContainers(2)(prevLen) - 2*8 + }, + fnExp3xAddCap: func(prevCap int) int { + return addDouble(addContainers(1)(prevCap) - 2*8) + }, + }, + { + currentMaxX: maxCardinality - 200, + fillUpX: maxCardinality + 1022, + fnExpAddLen: addContainers(2), + fnExpAddCap: addContainers(2), + fnExp3xAddLen: addContainers(2), + fnExp3xAddCap: addContainers(2), + }, + { + currentMaxX: maxCardinality - 200, + fillUpX: maxCardinality + 1023, + fnExpAddLen: addContainers(2), + fnExpAddCap: addContainers(2), + fnExp3xAddLen: addContainers(2), + fnExp3xAddCap: addContainers(2), + }, + { + currentMaxX: maxCardinality - 200, + fillUpX: maxCardinality + 1024, + fnExpAddLen: addContainers(2), + fnExpAddCap: addContainers(2), + fnExp3xAddLen: addContainers(2), + fnExp3xAddCap: addContainers(2), + }, + { + currentMaxX: maxCardinality - 200, + fillUpX: 3*maxCardinality - 1, + fnExpAddLen: addContainers(3), + fnExpAddCap: addContainers(3), + fnExp3xAddLen: addContainers(3), + fnExp3xAddCap: addContainers(3), + }, + { + currentMaxX: maxCardinality - 200, + fillUpX: 3 * maxCardinality, + fnExpAddLen: addContainers(4), + fnExpAddCap: addContainers(4), + fnExp3xAddLen: addContainers(4), + fnExp3xAddCap: func(prevCap int) int { + return addDouble(addContainers(3)(prevCap)) + }, + }, + { + currentMaxX: maxCardinality - 200, + fillUpX: 3*maxCardinality + 1, + fnExpAddLen: addContainers(4), + fnExpAddCap: addContainers(4), + fnExp3xAddLen: addContainers(4), + fnExp3xAddCap: func(prevCap int) int { + return addDouble(addContainers(3)(prevCap)) + }, + }, + + { + currentMaxX: maxCardinality - 150, + fillUpX: maxCardinality, + fnExpAddLen: addContainers(2), + fnExpAddCap: addContainers(2), + fnExp3xAddLen: func(prevLen int) int { + return addContainers(2)(prevLen) - 2*8 + }, + fnExp3xAddCap: func(prevCap int) int { + return addDouble(addContainers(1)(prevCap) - 2*8) + }, + }, + { + currentMaxX: maxCardinality - 150, + fillUpX: maxCardinality + 1022, + fnExpAddLen: addContainers(2), + fnExpAddCap: addContainers(2), + fnExp3xAddLen: addContainers(2), + fnExp3xAddCap: addContainers(2), + }, + { + currentMaxX: maxCardinality - 150, + fillUpX: maxCardinality + 1023, + fnExpAddLen: addContainers(2), + fnExpAddCap: addContainers(2), + fnExp3xAddLen: addContainers(2), + fnExp3xAddCap: addContainers(2), + }, + { + currentMaxX: maxCardinality - 150, + fillUpX: maxCardinality + 1024, + fnExpAddLen: addContainers(2), + fnExpAddCap: addContainers(2), + fnExp3xAddLen: addContainers(2), + fnExp3xAddCap: addContainers(2), + }, + { + currentMaxX: maxCardinality - 150, + fillUpX: 3*maxCardinality - 1, + fnExpAddLen: addContainers(3), + fnExpAddCap: addContainers(3), + fnExp3xAddLen: addContainers(3), + fnExp3xAddCap: addContainers(3), + }, + { + currentMaxX: maxCardinality - 150, + fillUpX: 3 * maxCardinality, + fnExpAddLen: addContainers(4), + fnExpAddCap: addContainers(4), + fnExp3xAddLen: addContainers(4), + fnExp3xAddCap: func(prevCap int) int { + return addDouble(addContainers(3)(prevCap)) + }, + }, + { + currentMaxX: maxCardinality - 150, + fillUpX: 3*maxCardinality + 1, + fnExpAddLen: addContainers(4), + fnExpAddCap: addContainers(4), + fnExp3xAddLen: addContainers(4), + fnExp3xAddCap: func(prevCap int) int { + return addDouble(addContainers(3)(prevCap)) + }, + }, + + { + currentMaxX: maxCardinality - 100, + fillUpX: maxCardinality, + fnExpAddLen: addContainers(2), + fnExpAddCap: addContainers(2), + fnExp3xAddLen: func(prevLen int) int { + return addContainers(2)(prevLen) - 2*8 + }, + fnExp3xAddCap: func(prevCap int) int { + return addDouble(addContainers(1)(prevCap) - 2*8) + }, + }, + { + currentMaxX: maxCardinality - 100, + fillUpX: maxCardinality + 1022, + fnExpAddLen: addContainers(2), + fnExpAddCap: addContainers(2), + fnExp3xAddLen: addContainers(2), + fnExp3xAddCap: addContainers(2), + }, + { + currentMaxX: maxCardinality - 100, + fillUpX: maxCardinality + 1023, + fnExpAddLen: addContainers(2), + fnExpAddCap: addContainers(2), + fnExp3xAddLen: addContainers(2), + fnExp3xAddCap: addContainers(2), + }, + { + currentMaxX: maxCardinality - 100, + fillUpX: maxCardinality + 1024, + fnExpAddLen: addContainers(2), + fnExpAddCap: addContainers(2), + fnExp3xAddLen: addContainers(2), + fnExp3xAddCap: addContainers(2), + }, + { + currentMaxX: maxCardinality - 100, + fillUpX: 3*maxCardinality - 1, + fnExpAddLen: addContainers(3), + fnExpAddCap: addContainers(3), + fnExp3xAddLen: addContainers(3), + fnExp3xAddCap: addContainers(3), + }, + { + currentMaxX: maxCardinality - 100, + fillUpX: 3 * maxCardinality, + fnExpAddLen: addContainers(4), + fnExpAddCap: addContainers(4), + fnExp3xAddLen: addContainers(4), + fnExp3xAddCap: func(prevCap int) int { + return addDouble(addContainers(3)(prevCap)) + }, + }, + { + currentMaxX: maxCardinality - 100, + fillUpX: 3*maxCardinality + 1, + fnExpAddLen: addContainers(4), + fnExpAddCap: addContainers(4), + fnExp3xAddLen: addContainers(4), + fnExp3xAddCap: func(prevCap int) int { + return addDouble(addContainers(3)(prevCap)) + }, + }, + } { + t.Run(fmt.Sprintf("filled up 1x %d to %d", tc.currentMaxX, tc.fillUpX), func(t *testing.T) { + singleElem := NewBitmap() + singleElem.Set(uint64(tc.currentMaxX)) + lenBytes := singleElem.LenInBytes() + capBytes := singleElem.capInBytes() + + singleElem.FillUp(uint64(tc.fillUpX)) + require.Equal(t, tc.fnExpAddLen(lenBytes), singleElem.LenInBytes()) + require.Equal(t, tc.fnExpAddCap(capBytes), singleElem.capInBytes()) + + assertFilledUp(t, singleElem, tc.currentMaxX, tc.fillUpX) + }) + + t.Run(fmt.Sprintf("filled up 3x %d to %d", tc.currentMaxX, tc.fillUpX), func(t *testing.T) { + singleElem := NewBitmap() + singleElem.Set(uint64(tc.currentMaxX)) + lenBytes := singleElem.LenInBytes() + capBytes := singleElem.capInBytes() + + singleElem.FillUp(uint64(tc.fillUpX) - 20) + singleElem.FillUp(uint64(tc.fillUpX) - 10) + singleElem.FillUp(uint64(tc.fillUpX)) + require.Equal(t, tc.fnExp3xAddLen(lenBytes), singleElem.LenInBytes()) + require.Equal(t, tc.fnExp3xAddCap(capBytes), singleElem.capInBytes()) + + assertFilledUp(t, singleElem, tc.currentMaxX, tc.fillUpX) + }) + } + }) + }) +} + func assertPrefilled(t *testing.T, bm *Bitmap, maxX int) { require.Equal(t, maxX+1, bm.GetCardinality()) @@ -1133,6 +2029,151 @@ func assertPrefilled(t *testing.T, bm *Bitmap, maxX int) { } } +func assertFilledUp(t *testing.T, bm *Bitmap, minX, maxX int) { + require.Equal(t, maxX-minX+1, bm.GetCardinality()) + + arr := bm.ToArray() + require.Equal(t, maxX-minX+1, len(arr)) + + for i, x := range arr { + require.Equal(t, uint64(i+minX), x) + } +} + +func TestPrefillUtils(t *testing.T) { + t.Run("calcNoFullContainerAndRemainingXs", func(t *testing.T) { + maxCard64 := uint64(maxCardinality) + + for _, tc := range []struct { + maxX uint64 + expNoContainers int + expNoRemaining int + }{ + { + maxX: 1, + expNoContainers: 0, + expNoRemaining: 2, + }, + { + maxX: maxCard64 - 2, + expNoContainers: 0, + expNoRemaining: maxCardinality - 1, + }, + { + maxX: maxCard64 - 1, + expNoContainers: 1, + expNoRemaining: 0, + }, + { + maxX: maxCard64, + expNoContainers: 1, + expNoRemaining: 1, + }, + { + maxX: maxCard64 + 1, + expNoContainers: 1, + expNoRemaining: 2, + }, + { + maxX: 4*maxCard64 - 2, + expNoContainers: 3, + expNoRemaining: maxCardinality - 1, + }, + { + maxX: 4*maxCard64 - 1, + expNoContainers: 4, + expNoRemaining: 0, + }, + { + maxX: 4 * maxCard64, + expNoContainers: 4, + expNoRemaining: 1, + }, + { + maxX: 4*maxCard64 + 1, + expNoContainers: 4, + expNoRemaining: 2, + }, + } { + t.Run(fmt.Sprintf("maxX %d", tc.maxX), func(t *testing.T) { + containers, remaining := calcFullContainersAndRemainingCounts(tc.maxX) + require.Equal(t, tc.expNoContainers, containers) + require.Equal(t, tc.expNoRemaining, remaining) + }) + } + }) + + t.Run("setRange", func(t *testing.T) { + newContainerBitmap := func() bitmap { + return bitmap(make([]uint16, maxContainerSize)) + } + + onesBitmap := newContainerBitmap() + onesBitmap.fillWithOnes() + + assertOnes := func(t *testing.T, b bitmap, minY, maxY int) { + count := 0 + for _, v := range uint16To64SliceUnsafe(b[startIdx:]) { + count += bits.OnesCount64(v) + } + require.Equal(t, maxY-minY+1, count) + + for i := uint16(minY); i <= uint16(maxY); i++ { + require.True(t, b.has(i)) + } + } + + type testCase struct { + minY, maxY int + } + testCases := []testCase{ + {minY: 0, maxY: 0}, + {minY: 1, maxY: 11}, + {minY: 2345, maxY: 4567}, + {minY: 4086, maxY: 4096}, + } + for _, pair := range [][2]int{ + {16, 48}, + {128, 320}, + {112, 384}, + {192, 336}, + } { + for i := -2; i <= 2; i++ { + for j := -2; j <= 2; j++ { + testCases = append(testCases, testCase{ + minY: pair[0] + i, + maxY: pair[1] + j, + }) + } + } + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("minY %d - maxY %d, without ones bitmap", tc.minY, tc.maxY), func(t *testing.T) { + b := newContainerBitmap() + b.setRange(tc.minY, tc.maxY, nil) + + assertOnes(t, b, tc.minY, tc.maxY) + }) + t.Run(fmt.Sprintf("minY %d - maxY %d, with ones bitmap", tc.minY, tc.maxY), func(t *testing.T) { + b := newContainerBitmap() + b.setRange(tc.minY, tc.maxY, onesBitmap) + + assertOnes(t, b, tc.minY, tc.maxY) + }) + } + }) + + t.Run("fillWithOnes", func(t *testing.T) { + b := bitmap(make([]uint16, maxContainerSize)) + b.fillWithOnes() + + for _, v := range uint16To64SliceUnsafe(b[startIdx:]) { + require.Equal(t, 64, bits.OnesCount64(v)) + } + }) +} + func TestMergeToSuperset(t *testing.T) { run := func(t *testing.T, bufs [][]uint16) { containerThreshold := uint64(math.MaxUint16 + 1) diff --git a/keys.go b/keys.go index 7fe1ca6..647786e 100644 --- a/keys.go +++ b/keys.go @@ -89,6 +89,21 @@ func (n node) search(k uint64) int { // return int(simd.Search(n[keyOffset(0):keyOffset(N)], k)) } +// Search returns the index of a smallest key >= k in a node. +// Runs from highest to smallest key. +func (n node) searchReverse(k uint64) int { + N := n.numKeys() + idx := N + + for i := N - 1; i >= 0; i-- { + if n.key(i) < k { + break + } + idx = i + } + return idx +} + func zeroOut(data []uint64) { for i := 0; i < len(data); i++ { data[i] = 0 From 398775d264ce059db9df6885cdee553ed75c9f28 Mon Sep 17 00:00:00 2001 From: Andrzej Liszka Date: Tue, 17 Dec 2024 17:31:37 +0100 Subject: [PATCH 6/6] performance: improved bitmap's minimum and maximum methods --- container.go | 39 ++++++++++++++++++++++++--------------- container_opt_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 15 deletions(-) create mode 100644 container_opt_test.go diff --git a/container.go b/container.go index 89e41dd..7fa4f38 100644 --- a/container.go +++ b/container.go @@ -636,32 +636,41 @@ func (b bitmap) isFull() bool { } func (b bitmap) minimum() uint16 { - N := getCardinality(b) - if N == 0 { + if N := getCardinality(b); N == 0 { return 0 } - for i, x := range b[startIdx:] { - lz := bits.LeadingZeros16(x) - if lz == 16 { - continue + + b64 := uint16To64SliceUnsafe(b[startIdx:]) + for i := 0; i < len(b64); i++ { + if b64[i] != 0 { + for j := 0; j < 4; j++ { + idx := i*4 + j + if lz := bits.LeadingZeros16(b[idx+int(startIdx)]); lz != 16 { + return uint16(16*idx + lz) + } + } + break } - return uint16(16*i + lz) } panic("We shouldn't reach here") } func (b bitmap) maximum() uint16 { - N := getCardinality(b) - if N == 0 { + if N := getCardinality(b); N == 0 { return 0 } - for i := len(b) - 1; i >= int(startIdx); i-- { - x := b[i] - tz := bits.TrailingZeros16(x) - if tz == 16 { - continue + + b64 := uint16To64SliceUnsafe(b[startIdx:]) + for i := len(b64) - 1; i >= 0; i-- { + if b64[i] != 0 { + for j := 3; j >= 0; j-- { + idx := i*4 + j + if tz := bits.TrailingZeros16(b[idx+int(startIdx)]); tz != 16 { + return uint16(16*idx + 15 - tz) + } + } + break } - return uint16(16*(i-int(startIdx)) + 15 - tz) } panic("We shouldn't reach here") } diff --git a/container_opt_test.go b/container_opt_test.go new file mode 100644 index 0000000..6e90cc2 --- /dev/null +++ b/container_opt_test.go @@ -0,0 +1,40 @@ +package sroar + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestContainerExtremes(t *testing.T) { + bitmapYs := []int{ + 0, 1, 15, 16, 17, 1022, 1023, 1024, 1025, + maxCardinality/2 - 1, maxCardinality / 2, maxCardinality/2 + 1, + maxCardinality - 3, maxCardinality - 2, maxCardinality - 1, + } + + t.Run("bitmap maximum", func(t *testing.T) { + b := bitmap(make([]uint16, maxContainerSize)) + + for i := 0; i < len(bitmapYs); i++ { + y := uint16(bitmapYs[i]) + t.Run(fmt.Sprint(y), func(t *testing.T) { + b.add(y) + require.Equal(t, y, b.maximum()) + }) + } + }) + + t.Run("bitmap minimum", func(t *testing.T) { + b := bitmap(make([]uint16, maxContainerSize)) + + for i := len(bitmapYs) - 1; i >= 0; i-- { + y := uint16(bitmapYs[i]) + t.Run(fmt.Sprint(y), func(t *testing.T) { + b.add(y) + require.Equal(t, y, b.minimum()) + }) + } + }) +}