From c802caedddaa1f96b30c0f229c1e305e3f7bb5de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Podg=C3=B3rski?= Date: Tue, 7 Apr 2020 16:26:20 +0200 Subject: [PATCH] The first implementation of intersect in place --- roaring/containers_slice.go | 44 ++- roaring/containers_test.go | 86 ++++++ roaring/roaring.go | 468 +++++++++++++++++++++++++++++ roaring/roaring_internal_test.go | 112 +++++++ roaring/roaring_test.go | 485 ++++++++++++++++++++++++++----- 5 files changed, 1114 insertions(+), 81 deletions(-) diff --git a/roaring/containers_slice.go b/roaring/containers_slice.go index 06a735b6d..98fe91fd3 100644 --- a/roaring/containers_slice.go +++ b/roaring/containers_slice.go @@ -77,8 +77,7 @@ func (sc *sliceContainers) Remove(key uint64) { return } if key == sc.lastKey { - sc.lastKey = ^uint64(0) - sc.lastContainer = nil + sc.invalidateCache() } sc.keys = append(sc.keys[:i], sc.keys[i+1:]...) sc.containers = append(sc.containers[:i], sc.containers[i+1:]...) @@ -159,8 +158,7 @@ func (sc *sliceContainers) Count() uint64 { func (sc *sliceContainers) Reset() { sc.keys = sc.keys[:0] sc.containers = sc.containers[:0] - sc.lastContainer = nil - sc.lastKey = ^uint64(0) + sc.invalidateCache() } func (sc *sliceContainers) ResetN(n int) { @@ -171,8 +169,7 @@ func (sc *sliceContainers) ResetN(n int) { sc.keys = sc.keys[:0] sc.containers = sc.containers[:0] } - sc.lastContainer = nil - sc.lastKey = ^uint64(0) + sc.invalidateCache() } func (sc *sliceContainers) seek(key uint64) (int, bool) { @@ -187,13 +184,28 @@ func (sc *sliceContainers) seek(key uint64) (int, bool) { func (sc *sliceContainers) Iterator(key uint64) (citer ContainerIterator, found bool) { i, found := sc.seek(key) - return &sliceIterator{e: sc, i: i}, found + return &sliceIterator{e: sc, i: i, index: i}, found } +// Repair tries to repair all containers, +// results in nil and empty containers getting dropped from the slice. +// For instance, that has to happen for writing the roaring format, +// which can't represent an empty container (c.N() == 0). func (sc *sliceContainers) Repair() { - for _, c := range sc.containers { + n := 0 + for i, c := range sc.containers { + if c == nil { + continue + } c.Repair() + sc.containers[n] = c + sc.keys[n] = sc.keys[i] + n++ } + sc.containers = sc.containers[:n] + sc.keys = sc.keys[:n] + + sc.invalidateCache() } // Update calls fn (existing-container, existed), and expects @@ -228,27 +240,35 @@ func (sc *sliceContainers) UpdateEvery(fn func(uint64, *Container, bool) (*Conta sc.containers[i] = nc } } - // invalidate cache. + + sc.invalidateCache() +} + +func (sc *sliceContainers) invalidateCache() { sc.lastKey = ^uint64(0) sc.lastContainer = nil } type sliceIterator struct { e *sliceContainers - i int - key uint64 - value *Container + i int // next e's index to get key, value + index int // current e's index of key, value + key uint64 // current key + value *Container // current value } func (si *sliceIterator) Next() bool { if si.e == nil { return false } + // discard nil containers from iteration. we don't always // actually remove them because copying is expensive. for si.i < len(si.e.keys) { si.key = si.e.keys[si.i] si.value = si.e.containers[si.i] + // keep the current index of key, value + si.index = si.i si.i++ if si.value != nil { return true diff --git a/roaring/containers_test.go b/roaring/containers_test.go index 1869d31af..6cd950514 100644 --- a/roaring/containers_test.go +++ b/roaring/containers_test.go @@ -99,3 +99,89 @@ func testContainersIterator(cs Containers, t *testing.T) { t.Fatalf("itr should be done, but got true") } } + +func TestSliceContainers(t *testing.T) { + const size = 10 + n := size + sc := newSliceContainers() + + // Add n keys + for i := 0; i < n; i++ { + key, set := uint64(i), []uint16{uint16(i)} + sc.Put(key, NewContainerArray(set)) + } + + t.Run("Get n keys", func(t *testing.T) { + for i := 0; i < n; i++ { + key, set := uint64(i), []uint16{uint16(i)} + c := sc.Get(key) + if c == nil { + t.Fatalf("Get(%d) returned nil container", key) + } + if c.data[0] != set[0] { + t.Fatalf("Get(%d): expected: %v, got: %v", key, set[0], c.data[0]) + } + } + }) + + t.Run("Last key/container", func(t *testing.T) { + key, c := sc.Last() + if key != uint64(n-1) || c.data[0] != uint16(n-1) { + t.Fatalf("Last: expected: %v, got: %d, %v", n-1, key, c.data) + } + }) + + // Remove odd keys + for i := 1; i < size; i += 2 { + key := uint64(i) + sc.Remove(key) + n-- + } + + t.Run("Try to Get removed containers", func(t *testing.T) { + for i := 1; i < size; i += 2 { + key := uint64(i) + c := sc.Get(key) + if c != nil { + t.Fatalf("Get(for non existing key %d): found container: %v", key, c.data) + } + } + + // Test - Last key/container + key, c := sc.Last() + if key != uint64(size-2) || c.data[0] != uint16(size-2) { + t.Fatalf("Last: expected: %v, got: %d, %v", size-2, key, c.data) + } + + if sc.Size() != n { + t.Fatalf("Size: expected: %d, got: %d", n, sc.Size()) + } + }) + + t.Run("Nil containers and repair them", func(t *testing.T) { + // Remove half of even containers + for i := range sc.containers { + if i%2 == 0 { + sc.containers[i] = nil + n-- + } + } + sc.Repair() + + if sc.Size() != n { + t.Fatalf("Size: expected: %d, got: %d", n, sc.Size()) + } + + for i, key := range sc.keys { + if sc.containers[i] == nil { + t.Fatalf("Found nil container for key: %d at index: %d", key, i) + } else { + if sc.containers[i].data[0] != uint16(key) { + t.Fatalf("Invalid container data for key: %d at index: %d - expected: %d, got: %d", + key, i, uint16(key), sc.containers[i].data[0], + ) + } + } + } + }) +} diff --git a/roaring/roaring.go b/roaring/roaring.go index 369e8c2a7..267e3b8c7 100644 --- a/roaring/roaring.go +++ b/roaring/roaring.go @@ -694,6 +694,474 @@ func (b *Bitmap) Intersect(other *Bitmap) *Bitmap { return output } +type mutableContainersIterator struct { + c Containers + + cit ContainerIterator + sit *sliceIterator +} + +func newMutableContainersIterator(cs Containers, key uint64) *mutableContainersIterator { + it := &mutableContainersIterator{c: cs} + + if sc, ok := cs.(*sliceContainers); ok { + if i, found := sc.seek(key); found { + it.sit = &sliceIterator{e: sc, i: i, index: i} + return it + } + } + + it.cit, _ = cs.Iterator(key) + return it +} + +func (it *mutableContainersIterator) update(key uint64, newCont *Container) { + if it.sit != nil { + if key == it.sit.key { + it.sit.e.containers[it.sit.index] = newCont + } + } + + it.c.Update(key, func(_ *Container, _ bool) (*Container, bool) { + return newCont, true + }) +} + +func (it *mutableContainersIterator) Next() bool { + if it.sit != nil { + return it.sit.Next() + } + + return it.cit.Next() +} + +func (it *mutableContainersIterator) Value() (uint64, *Container) { + if it.sit != nil { + return it.sit.Value() + } + + return it.cit.Value() +} + +// IntersectInPlace returns the bitwise intersection of b and others, +// modifying b in place. +func (b *Bitmap) IntersectInPlace(others ...*Bitmap) { + var bSize int + if bSize = b.Size(); bSize == 0 { + // If b doesn't have any containers then return early. + return + } + + otherIters := make(handledIters, 0, len(others)) + for _, other := range others { + it, _ := other.Containers.Iterator(0) + if !it.Next() { + // An empty bitmap - reset all + b.Containers.Reset() + return + } + + otherIters = append(otherIters, handledIter{ + iter: it, + hasNext: true, + }) + } + + bIter := newMutableContainersIterator(b.Containers, 0) + for bIter.Next() { + bKey, bCont := bIter.Value() + if bCont.N() == 0 { + // No point in intersecting things from an empty container. + bIter.update(bKey, nil) + continue + } + + // Loop until every iters current value has been handled. + for _, otherIter := range otherIters { + if !otherIter.hasNext { + continue + } + + otherKey, otherCont := otherIter.iter.Value() + for otherKey < bKey { + otherIter.hasNext = otherIter.iter.Next() + if !otherIter.hasNext { + break + } + otherKey, otherCont = otherIter.iter.Value() + } + + if bKey == otherKey { + // Note: a nil container is valid, and has N == 0. + if otherCont.N() != 0 { + if bCont.frozen() { + bCont = bCont.Clone() + b.Containers.Put(bKey, bCont) + } + bCont = bCont.intersectInPlace(otherCont) + bIter.update(bKey, bCont) + if bCont == nil || bCont.N() == 0 { + break + } + + otherIter.hasNext = otherIter.iter.Next() + continue + } + } + + bIter.update(bKey, nil) + break + } + } + + b.Containers.Repair() +} + +func (c *Container) intersectInPlace(other *Container) *Container { + // short-circuit the trivial cases + if c == nil || other == nil || c.N() == 0 || other.N() == 0 { + c = nil + return c + } + cFull, otherFull := (c.N() == maxContainerVal+1), (other.N() == maxContainerVal+1) + if cFull && otherFull { + return c + } + if cFull { + return c.copyInPlace(other) + } + if otherFull { + return c + } + + switch c.typ() { + case containerArray: + switch other.typ() { + case containerArray: + return intersectArrayArrayInPlace(c, other) + case containerBitmap: + return intersectArrayBitmapInPlace(c, other) + case containerRun: + return intersectArrayRunInPlace(c, other) + } + + case containerBitmap: + switch other.typ() { + case containerArray: + return intersectBitmapArrayInPlace(c, other) + case containerBitmap: + return intersectBitmapBitmapInPlace(c, other) + case containerRun: + return intersectBitmapRunInPlace(c, other) + } + + case containerRun: + switch other.typ() { + case containerArray: + return intersectRunArrayInPlace(c, other) + case containerBitmap: + return intersectRunBitmapInPlace(c, other) + case containerRun: + return intersectRunRunInPlace(c, other) + } + } + + if roaringParanoia { + panic(fmt.Sprintf("invalid intersect op: unknown types %d/%d", c.typ(), other.typ())) + } + return nil +} + +func (c *Container) copyInPlace(other *Container) *Container { + switch other.typ() { + case containerArray: + c.setTyp(containerArray) + c.setArrayMaybeCopy(other.array(), true) + c.setN(other.N()) + + case containerBitmap: + bmp := make([]uint64, bitmapN) + copy(bmp, other.bitmap()) + c.setTyp(containerBitmap) + c.setBitmap(bmp) + c.setN(other.N()) + + case containerRun: + c.setTyp(containerRun) + c.setRunsMaybeCopy(other.runs(), true) + c.setN(other.N()) + } + + return c +} + +func intersectArrayArrayInPlace(a, b *Container) *Container { + statsHit("intersectInPlace/ArrayArray") + + a = a.Thaw() + aa, ba := a.array(), b.array() + an, bn := len(aa), len(ba) + n := 0 + for i, j := 0, 0; i < an && j < bn; { + va, vb := aa[i], ba[j] + + if va < vb { + i++ + } else if va > vb { + j++ + } else { + aa[n] = va + n, i, j = n+1, i+1, j+1 + } + } + aa = aa[:n] + a.setArray(aa) + + return a +} + +func intersectArrayRunInPlace(a, b *Container) *Container { + statsHit("intersectInPlace/ArrayRun") + + a = a.Thaw() + aa, br := a.array(), b.runs() + an, bn := len(aa), len(br) + + n := 0 + for i, j := 0, 0; i < an && j < bn; { + va, vb := aa[i], br[j] + if va < vb.start { + i++ + } else if va > vb.last { + j++ + } else { + aa[n] = va + n++ + i++ + } + } + + aa = aa[:n] + a.setArray(aa) + + return a +} + +func intersectArrayBitmapInPlace(a, b *Container) *Container { + statsHit("intersectInPlace/ArrayBitmap") + + a = a.Thaw() + aa := a.array() + bb := b.bitmap() + + n := 0 + for _, va := range aa { + b := bb[va>>6] + bidx := va % 64 + mask := uint64(1) << bidx + if b&mask > 0 { + aa[n] = va + n++ + } + } + + aa = aa[:n] + a.setArray(aa) + + return a +} + +func intersectBitmapBitmapInPlace(a, b *Container) *Container { + statsHit("intersectInPlace/BitmapBitmap") + + a = a.Thaw() + ab := a.bitmap()[:bitmapN] + bb := b.bitmap()[:bitmapN] + + n := int32(0) + for i := 0; i < bitmapN; i += 4 { + // unrolling is still effective in go + ptr := (*[4]uint64)(unsafe.Pointer(&bb[i])) + ab[i] &= ptr[0] + ab[i+1] &= ptr[1] + ab[i+2] &= ptr[2] + ab[i+3] &= ptr[3] + + n += int32(popcount(ab[i])) + + int32(popcount(ab[i+1])) + + int32(popcount(ab[i+2])) + + int32(popcount(ab[i+3])) + } + a.setN(n) + + return a +} + +func intersectBitmapArrayInPlace(a, b *Container) *Container { + statsHit("intersectInPlace/BitmapArray") + + a = a.Thaw() + ab := a.bitmap() + ba := b.array() + + bn := len(ba) + array := make([]uint16, bn) + n := int32(0) + for _, vb := range ba { + i := vb >> 6 + mask := uint64(1) << uint(vb%64) + if ab[i]&mask > 0 { + array[n] = vb + n++ + } + } + array = array[:n] + a.setTyp(containerArray) + a.setArray(array) + a.setN(n) + + return a +} + +func intersectBitmapRunInPlace(a, b *Container) *Container { + statsHit("intersectInPlace/BitmapRun") + + a = a.Thaw() + ab := a.bitmap() + br := b.runs() + an := len(ab) + bitmap := make([]uint64, an) + + n := int32(0) + for _, vb := range br { + i := vb.start >> 6 // index into a + + vastart := i << 6 + valast := vastart + 63 + for valast >= vb.start && vastart <= vb.last && int(i) < an { + if vastart >= vb.start && valast <= vb.last { // a within b + bitmap[i] = ab[i] + n += int32(popcount(ab[i])) + } else if vb.start >= vastart && vb.last <= valast { // b within a + var mask uint64 = ((1 << (vb.last - vb.start + 1)) - 1) << (vb.start - vastart) + bits := ab[i] & mask + bitmap[i] |= bits + n += int32(popcount(bits)) + } else if vastart < vb.start { // a overlaps front of b + offset := 64 - (1 + valast - vb.start) + bits := (ab[i] >> offset) << offset + bitmap[i] |= bits + n += int32(popcount(bits)) + } else if vb.start < vastart { // b overlaps front of a + offset := 64 - (1 + vb.last - vastart) + bits := (ab[i] << offset) >> offset + bitmap[i] |= bits + n += int32(popcount(bits)) + } + + i++ + vastart = i << 6 + valast = vastart + 63 + } + } + a.setBitmap(bitmap) + a.setN(n) + + return a +} + +func intersectRunRunInPlace(a, b *Container) *Container { + statsHit("intersectInPlace/RunRun") + + a = a.Thaw() + ar, br := a.runs(), b.runs() + an, bn := len(ar), len(br) + + var runs []interval16 + if an > bn { + runs = make([]interval16, 0, an) + } else { + runs = make([]interval16, 0, bn) + } + + n := int32(0) + for i, j := 0, 0; i < an && j < bn; { + va, vb := ar[i], br[j] + + if va.last < vb.start { + // |--va--| |--vb--| + i++ + } else if vb.last < va.start { + // |--vb--| |--va--| + j++ + } else if va.last > vb.last && va.start >= vb.start { + // |--vb-|-|-va--| + runs = append(runs, interval16{start: va.start, last: vb.last}) + n += int32(vb.last-va.start) + 1 + j++ + } else if va.last > vb.last && va.start < vb.start { + // |--va|--vb--|--| + runs = append(runs, vb) + n += int32(vb.last-vb.start) + 1 + j++ + } else if va.last <= vb.last && va.start >= vb.start { + // |--vb|--va--|--| + runs = append(runs, va) + n += int32(va.last-va.start) + 1 + i++ + } else if va.last <= vb.last && va.start < vb.start { + // |--va-|-|-vb--| + runs = append(runs, interval16{start: vb.start, last: va.last}) + n += int32(va.last-vb.start) + 1 + i++ + } + } + a.setRuns(runs) + a.setN(n) + + return a +} + +func intersectRunArrayInPlace(a, b *Container) *Container { + statsHit("intersectInPlace/RunArray") + + a = a.Thaw() + ar, ba := a.runs(), b.array() + an, bn := len(ar), len(ba) + + array := make([]uint16, bn) + n := 0 + for i, j := 0, 0; i < an && j < bn; { + va, vb := ar[i], ba[j] + if vb < va.start { + j++ + } else if vb > va.last { + i++ + } else { + array[n] = vb + n++ + j++ + } + } + + array = array[:n] + a.setTyp(containerArray) + a.setArray(array) + + return a +} + +func intersectRunBitmapInPlace(a, b *Container) *Container { + statsHit("intersectInPlace/RunBitmap") + + // TODO(@kuba--): + // Figure out how to efficiently intersect dense runs with bitmaps. + // So far we convert run to bitmap and intersect bitmaps - it's much faster + // than naive O(n^2) algorithm. + a = a.runToBitmap() + return intersectBitmapBitmapInPlace(a, b) +} + // Union returns the bitwise union of b and others as a new bitmap. func (b *Bitmap) Union(others ...*Bitmap) *Bitmap { if len(others) == 1 { diff --git a/roaring/roaring_internal_test.go b/roaring/roaring_internal_test.go index f7c2f2f78..9e7a468fc 100644 --- a/roaring/roaring_internal_test.go +++ b/roaring/roaring_internal_test.go @@ -2754,6 +2754,15 @@ func differenceInPlaceWrapper(a, b *Container) *Container { return out.Containers.Get(0) } +func intersectInPlaceWrapper(a, b *Container) *Container { + out := NewBitmap() + out.Containers.Put(0, a.Clone()) + B := NewBitmap() + B.Containers.Put(0, b) + out.IntersectInPlace(B) + return out.Containers.Get(0) +} + func TestContainerCombinations(t *testing.T) { cts := setupContainerTests() @@ -2872,6 +2881,109 @@ func TestContainerCombinations(t *testing.T) { {intersect, "evenBitsSet", "oddBitsSet", "empty"}, {intersect, "evenBitsSet", "evenBitsSet", "evenBitsSet"}, + // intersect in place + {intersectInPlaceWrapper, "empty", "empty", "empty"}, + {intersectInPlaceWrapper, "empty", "full", "empty"}, + {intersectInPlaceWrapper, "empty", "firstBitSet", "empty"}, + {intersectInPlaceWrapper, "empty", "lastBitSet", "empty"}, + {intersectInPlaceWrapper, "empty", "firstBitUnset", "empty"}, + {intersectInPlaceWrapper, "empty", "lastBitUnset", "empty"}, + {intersectInPlaceWrapper, "empty", "innerBitsSet", "empty"}, + {intersectInPlaceWrapper, "empty", "outerBitsSet", "empty"}, + {intersectInPlaceWrapper, "empty", "oddBitsSet", "empty"}, + {intersectInPlaceWrapper, "empty", "evenBitsSet", "empty"}, + // + {intersectInPlaceWrapper, "full", "empty", "empty"}, + {intersectInPlaceWrapper, "full", "full", "full"}, + {intersectInPlaceWrapper, "full", "firstBitSet", "firstBitSet"}, + {intersectInPlaceWrapper, "full", "lastBitSet", "lastBitSet"}, + {intersectInPlaceWrapper, "full", "firstBitUnset", "firstBitUnset"}, + {intersectInPlaceWrapper, "full", "lastBitUnset", "lastBitUnset"}, + {intersectInPlaceWrapper, "full", "innerBitsSet", "innerBitsSet"}, + {intersectInPlaceWrapper, "full", "outerBitsSet", "outerBitsSet"}, + {intersectInPlaceWrapper, "full", "oddBitsSet", "oddBitsSet"}, + {intersectInPlaceWrapper, "full", "evenBitsSet", "evenBitsSet"}, + // + {intersectInPlaceWrapper, "firstBitSet", "empty", "empty"}, + {intersectInPlaceWrapper, "firstBitSet", "full", "firstBitSet"}, + {intersectInPlaceWrapper, "firstBitSet", "firstBitSet", "firstBitSet"}, + {intersectInPlaceWrapper, "firstBitSet", "lastBitSet", "empty"}, + {intersectInPlaceWrapper, "firstBitSet", "firstBitUnset", "empty"}, + {intersectInPlaceWrapper, "firstBitSet", "lastBitUnset", "firstBitSet"}, + {intersectInPlaceWrapper, "firstBitSet", "innerBitsSet", "empty"}, + {intersectInPlaceWrapper, "firstBitSet", "outerBitsSet", "firstBitSet"}, + {intersectInPlaceWrapper, "firstBitSet", "oddBitsSet", "empty"}, + {intersectInPlaceWrapper, "firstBitSet", "evenBitsSet", "firstBitSet"}, + // + {intersectInPlaceWrapper, "lastBitSet", "empty", "empty"}, + {intersectInPlaceWrapper, "lastBitSet", "full", "lastBitSet"}, + {intersectInPlaceWrapper, "lastBitSet", "firstBitSet", "empty"}, + {intersectInPlaceWrapper, "lastBitSet", "lastBitSet", "lastBitSet"}, + {intersectInPlaceWrapper, "lastBitSet", "firstBitUnset", "lastBitSet"}, + {intersectInPlaceWrapper, "lastBitSet", "lastBitUnset", "empty"}, + {intersectInPlaceWrapper, "lastBitSet", "innerBitsSet", "empty"}, + {intersectInPlaceWrapper, "lastBitSet", "outerBitsSet", "lastBitSet"}, + {intersectInPlaceWrapper, "lastBitSet", "oddBitsSet", "lastBitSet"}, + {intersectInPlaceWrapper, "lastBitSet", "evenBitsSet", "empty"}, + // + {intersectInPlaceWrapper, "firstBitUnset", "empty", "empty"}, + {intersectInPlaceWrapper, "firstBitUnset", "full", "firstBitUnset"}, + {intersectInPlaceWrapper, "firstBitUnset", "firstBitSet", "empty"}, + {intersectInPlaceWrapper, "firstBitUnset", "lastBitSet", "lastBitSet"}, + {intersectInPlaceWrapper, "firstBitUnset", "firstBitUnset", "firstBitUnset"}, + {intersectInPlaceWrapper, "firstBitUnset", "lastBitUnset", "innerBitsSet"}, + {intersectInPlaceWrapper, "firstBitUnset", "innerBitsSet", "innerBitsSet"}, + {intersectInPlaceWrapper, "firstBitUnset", "outerBitsSet", "lastBitSet"}, + {intersectInPlaceWrapper, "firstBitUnset", "oddBitsSet", "oddBitsSet"}, + // + {intersectInPlaceWrapper, "lastBitUnset", "empty", "empty"}, + {intersectInPlaceWrapper, "lastBitUnset", "full", "lastBitUnset"}, + {intersectInPlaceWrapper, "lastBitUnset", "firstBitSet", "firstBitSet"}, + {intersectInPlaceWrapper, "lastBitUnset", "lastBitSet", "empty"}, + {intersectInPlaceWrapper, "lastBitUnset", "firstBitUnset", "innerBitsSet"}, + {intersectInPlaceWrapper, "lastBitUnset", "lastBitUnset", "lastBitUnset"}, + {intersectInPlaceWrapper, "lastBitUnset", "innerBitsSet", "innerBitsSet"}, + {intersectInPlaceWrapper, "lastBitUnset", "outerBitsSet", "firstBitSet"}, + {intersectInPlaceWrapper, "lastBitUnset", "evenBitsSet", "evenBitsSet"}, + // + {intersectInPlaceWrapper, "innerBitsSet", "empty", "empty"}, + {intersectInPlaceWrapper, "innerBitsSet", "full", "innerBitsSet"}, + {intersectInPlaceWrapper, "innerBitsSet", "firstBitSet", "empty"}, + {intersectInPlaceWrapper, "innerBitsSet", "lastBitSet", "empty"}, + {intersectInPlaceWrapper, "innerBitsSet", "firstBitUnset", "innerBitsSet"}, + {intersectInPlaceWrapper, "innerBitsSet", "lastBitUnset", "innerBitsSet"}, + {intersectInPlaceWrapper, "innerBitsSet", "innerBitsSet", "innerBitsSet"}, + {intersectInPlaceWrapper, "innerBitsSet", "outerBitsSet", "empty"}, + // + {intersectInPlaceWrapper, "outerBitsSet", "empty", "empty"}, + {intersectInPlaceWrapper, "outerBitsSet", "full", "outerBitsSet"}, + {intersectInPlaceWrapper, "outerBitsSet", "firstBitSet", "firstBitSet"}, + {intersectInPlaceWrapper, "outerBitsSet", "lastBitSet", "lastBitSet"}, + {intersectInPlaceWrapper, "outerBitsSet", "firstBitUnset", "lastBitSet"}, + {intersectInPlaceWrapper, "outerBitsSet", "lastBitUnset", "firstBitSet"}, + {intersectInPlaceWrapper, "outerBitsSet", "innerBitsSet", "empty"}, + {intersectInPlaceWrapper, "outerBitsSet", "outerBitsSet", "outerBitsSet"}, + {intersectInPlaceWrapper, "outerBitsSet", "oddBitsSet", "lastBitSet"}, + {intersectInPlaceWrapper, "outerBitsSet", "evenBitsSet", "firstBitSet"}, + // + {intersectInPlaceWrapper, "oddBitsSet", "empty", "empty"}, + {intersectInPlaceWrapper, "oddBitsSet", "full", "oddBitsSet"}, + {intersectInPlaceWrapper, "oddBitsSet", "firstBitSet", "empty"}, + {intersectInPlaceWrapper, "oddBitsSet", "lastBitSet", "lastBitSet"}, + {intersectInPlaceWrapper, "oddBitsSet", "firstBitUnset", "oddBitsSet"}, + {intersectInPlaceWrapper, "oddBitsSet", "outerBitsSet", "lastBitSet"}, + {intersectInPlaceWrapper, "oddBitsSet", "oddBitsSet", "oddBitsSet"}, + {intersectInPlaceWrapper, "oddBitsSet", "evenBitsSet", "empty"}, + // + {intersectInPlaceWrapper, "evenBitsSet", "empty", "empty"}, + {intersectInPlaceWrapper, "evenBitsSet", "full", "evenBitsSet"}, + {intersectInPlaceWrapper, "evenBitsSet", "firstBitSet", "firstBitSet"}, + {intersectInPlaceWrapper, "evenBitsSet", "lastBitSet", "empty"}, + {intersectInPlaceWrapper, "evenBitsSet", "lastBitUnset", "evenBitsSet"}, + {intersectInPlaceWrapper, "evenBitsSet", "outerBitsSet", "firstBitSet"}, + {intersectInPlaceWrapper, "evenBitsSet", "oddBitsSet", "empty"}, + {intersectInPlaceWrapper, "evenBitsSet", "evenBitsSet", "evenBitsSet"}, + // union {union, "empty", "empty", "empty"}, {union, "empty", "full", "full"}, diff --git a/roaring/roaring_test.go b/roaring/roaring_test.go index cc6514b2c..a5c4a9a25 100644 --- a/roaring/roaring_test.go +++ b/roaring/roaring_test.go @@ -481,6 +481,288 @@ func TestBitmap_Intersection(t *testing.T) { } +func TestBitmap_IntersectionInPlace(t *testing.T) { + bm0 := roaring.NewFileBitmap(0, 2683177) + bm1 := roaring.NewFileBitmap() + for i := uint64(628); i < 2683301; i++ { + if _, err := bm1.Add(i); err != nil { + t.Fatalf("adding bits: %v", err) + } + } + + bm0.IntersectInPlace(bm1) + if n := bm0.Count(); n != 1 { + t.Fatalf("unexpected n: %d", n) + } +} + +func TestBitmap_IntersectionInPlace_Empty(t *testing.T) { + bm0 := roaring.NewFileBitmap(0, 2683177) + bm1 := roaring.NewFileBitmap() + + bm0.IntersectInPlace(bm1) + if n := bm0.Count(); n != 0 { + t.Fatalf("unexpected n: %d", n) + } + + bm1.IntersectInPlace(roaring.NewFileBitmap(0, 2683177)) + if n := bm1.Count(); n != 0 { + t.Fatalf("unexpected n: %d", n) + } +} + +func TestBitmap_IntersectArrayArrayInPlace(t *testing.T) { + bm0 := roaring.NewFileBitmap(0, 1, 7, 9, 11, 2683, 5005) + bm1 := roaring.NewFileBitmap(0, 2683, 2684, 5000) + expected := []uint64{0, 2683} + + bm0.IntersectInPlace(bm1) + if n := bm0.Count(); n != 2 { + t.Fatalf("unexpected n: %d", n) + } + for _, e := range expected { + if !bm0.Contains(e) { + t.Fatalf("missing value %d", e) + } + } + + // confirm that it also works going the other way + bm1.IntersectInPlace(bm0) + if n := bm1.Count(); n != 2 { + t.Fatalf("unexpected n: %d", n) + } + for _, e := range expected { + if !bm1.Contains(e) { + t.Fatalf("missing value %d", e) + } + } +} + +func TestBitmap_IntersectArrayBitmapInPlace(t *testing.T) { + bm0 := roaring.NewFileBitmap(0, 1, 7, 9, 11, 2683, 5005) + bm1 := roaring.NewFileBitmap() + for i := uint64(1); i < 65536; i += 3 { + if _, err := bm1.Add(i); err != nil { + t.Fatalf("adding bits(%d): %v", i, err) + } + } + + expected := []uint64{1, 7, 2683, 5005} + + bm0.IntersectInPlace(bm1) + + if n := bm0.Count(); n != uint64(len(expected)) { + t.Fatalf("unexpected n: %d", n) + } + for _, e := range expected { + if !bm0.Contains(e) { + t.Fatalf("missing value %d", e) + } + } +} + +func TestBitmap_IntersectArrayRunInPlace(t *testing.T) { + bm0 := roaring.NewFileBitmap(0, 1, 7, 9, 11, 2683, 5005) + bm1 := roaring.NewFileBitmap(5, 6, 7, 8, 9, 10, 11, 13) + bm1.Optimize() // convert to runs + + expected := []uint64{7, 9, 11} + + bm0.IntersectInPlace(bm1) + + if n := bm0.Count(); n != uint64(len(expected)) { + t.Fatalf("unexpected n: %d", n) + } + for _, e := range expected { + if !bm0.Contains(e) { + t.Fatalf("missing value %d", e) + } + } +} + +func TestBitmap_IntersectBitmapBitmapInPlace(t *testing.T) { + bm0 := roaring.NewFileBitmap() + for i := uint64(0); i < 65536; i += 2 { + if _, err := bm0.Add(i); err != nil { + t.Fatalf("adding bits: %v", err) + } + } + + bm1 := roaring.NewFileBitmap() + for i := uint64(0); i < 65536; i += 3 { + if _, err := bm1.Add(i); err != nil { + t.Fatalf("adding bits: %v", err) + } + } + + bm0.IntersectInPlace(bm1) + if n := bm0.Count(); n != 10923 { + t.Fatalf("unexpected n: %d", n) + } +} + +func TestBitmap_IntersectBitmapArrayInPlace(t *testing.T) { + bm0 := roaring.NewFileBitmap(0, 1, 7, 9, 11, 2683, 5005) + bm1 := roaring.NewFileBitmap() + for i := uint64(1); i < 65536; i += 3 { + if _, err := bm1.Add(i); err != nil { + t.Fatalf("adding bits(%d): %v", i, err) + } + } + + expected := []uint64{1, 7, 2683, 5005} + + bm1.IntersectInPlace(bm0) + + if n := bm1.Count(); n != uint64(len(expected)) { + t.Fatalf("unexpected n: %d", n) + } + for _, e := range expected { + if !bm1.Contains(e) { + t.Fatalf("missing value %d", e) + } + } + + bm11 := roaring.NewFileBitmap(5, 6, 7, 8, 9, 10, 11, 13, 2683) + bm12 := roaring.NewFileBitmap(6, 7, 10, 13, 2683) + + bm0.IntersectInPlace(bm11, bm12) + + expected = []uint64{7, 2683} + if n := bm0.Count(); n != uint64(len(expected)) { + t.Fatalf("unexpected n: %d", n) + } + for _, e := range expected { + if !bm0.Contains(e) { + t.Fatalf("missing value %d", e) + } + } +} + +func TestBitmap_IntersectBitmapRunInPlace(t *testing.T) { + // Intersect two runs that result in a bitmap. + bm1 := roaring.NewFileBitmap() + runLen := uint64(25) + spaceLen := uint64(8) + offset := (runLen / 2) + spaceLen + for i := uint64(0); i < (65536 - runLen - offset); i += (runLen + spaceLen) { + for j := uint64(0); j < runLen; j++ { + if _, err := bm1.Add(offset + i + j); err != nil { + t.Fatalf("adding bits: %v", err) + } + } + } + bm1.Optimize() // convert to runs + + bm0 := roaring.NewFileBitmap() + runLen = uint64(32) + spaceLen = uint64(1) + for i := uint64(0); i < (65536 - runLen); i += (runLen + spaceLen) { + for j := uint64(0); j < runLen; j++ { + if _, err := bm0.Add(i + j); err != nil { + t.Fatalf("adding bits: %v", err) + } + } + } + + bm0.IntersectInPlace(bm1) + if n := bm0.Count(); n != 47628 { + t.Fatalf("unexpected n: %d", n) + } +} + +func TestBitmap_IntersectRunRunInPlace(t *testing.T) { + // Intersect two runs that result in an array. + bm0 := roaring.NewFileBitmap(0, 1, 2, 3, 4, 5, 10, 11, 12, 13, 14, 15) + bm0.Optimize() // convert to runs + bm1 := roaring.NewFileBitmap(5, 6, 7, 8, 9, 10, 11, 13) + bm1.Optimize() // convert to runs + bm0.IntersectInPlace(bm1) + if n := bm0.Count(); n != 4 { + t.Fatalf("unexpected n: %d", n) + } + + // Intersect two runs that result in a bitmap. + bm2 := roaring.NewFileBitmap() + runLen := uint64(25) + spaceLen := uint64(8) + offset := (runLen / 2) + spaceLen + for i := uint64(0); i < (65536 - runLen - offset); i += (runLen + spaceLen) { + for j := uint64(0); j < runLen; j++ { + if _, err := bm2.Add(offset + i + j); err != nil { + t.Fatalf("adding bits: %v", err) + } + } + } + bm2.Optimize() // convert to runs + bm3 := roaring.NewFileBitmap() + runLen = uint64(32) + spaceLen = uint64(1) + for i := uint64(0); i < (65536 - runLen); i += (runLen + spaceLen) { + for j := uint64(0); j < runLen; j++ { + if _, err := bm3.Add(i + j); err != nil { + t.Fatalf("adding bits: %v", err) + } + } + } + bm3.Optimize() // convert to runs + bm2.IntersectInPlace(bm3) + if n := bm2.Count(); n != 47628 { + t.Fatalf("unexpected n: %d", n) + } +} + +func TestBitmap_IntersectRunArrayInPlace(t *testing.T) { + // Intersect two runs that result in an array. + bm0 := roaring.NewFileBitmap(0, 1, 2, 3, 4, 5, 10, 11, 12, 13, 14, 15) + bm0.Optimize() // convert to runs + bm1 := roaring.NewFileBitmap(5, 6, 7, 8, 9, 10, 11, 13) + + bm0.IntersectInPlace(bm1) + + expected := []uint64{5, 10, 11, 13} + if n := bm0.Count(); n != uint64(len(expected)) { + t.Fatalf("unexpected n: %d", n) + } + for _, e := range expected { + if !bm0.Contains(e) { + t.Fatalf("missing value %d", e) + } + } +} + +func TestBitmap_IntersectRunBitmapInPlace(t *testing.T) { + // Intersect two runs that result in a bitmap. + bm0 := roaring.NewFileBitmap() + runLen := uint64(25) + spaceLen := uint64(8) + offset := (runLen / 2) + spaceLen + for i := uint64(0); i < (65536 - runLen - offset); i += (runLen + spaceLen) { + for j := uint64(0); j < runLen; j++ { + if _, err := bm0.Add(offset + i + j); err != nil { + t.Fatalf("adding bits: %v", err) + } + } + } + bm0.Optimize() // convert to runs + + bm1 := roaring.NewFileBitmap() + runLen = uint64(32) + spaceLen = uint64(1) + for i := uint64(0); i < (65536 - runLen); i += (runLen + spaceLen) { + for j := uint64(0); j < runLen; j++ { + if _, err := bm1.Add(i + j); err != nil { + t.Fatalf("adding bits: %v", err) + } + } + } + + bm0.IntersectInPlace(bm1) + if n := bm0.Count(); n != 47628 { + t.Fatalf("unexpected n: %d", n) + } +} + func TestBitmap_Union1(t *testing.T) { bm0 := roaring.NewFileBitmap(0, 2683177) bm1 := roaring.NewFileBitmap() @@ -1193,9 +1475,13 @@ func testBitmapQuick(t *testing.T, n int, min, max uint64) { } } -func TestBitmap_Marshal_Quick_Array1(t *testing.T) { testBitmapMarshalQuick(t, 1000, 1000, 2000, false) } -func TestBitmap_Marshal_Quick_Array2(t *testing.T) { testBitmapMarshalQuick(t, 10000, 0, 1000, false) } -func TestBitmap_Marshal_Quick_Bitmap1(t *testing.T) { testBitmapMarshalQuick(t, 10000, 0, 10000, false) } +func TestBitmap_Marshal_Quick_Array1(t *testing.T) { + testBitmapMarshalQuick(t, 1000, 1000, 2000, false) +} +func TestBitmap_Marshal_Quick_Array2(t *testing.T) { testBitmapMarshalQuick(t, 10000, 0, 1000, false) } +func TestBitmap_Marshal_Quick_Bitmap1(t *testing.T) { + testBitmapMarshalQuick(t, 10000, 0, 10000, false) +} func TestBitmap_Marshal_Quick_Bitmap2(t *testing.T) { testBitmapMarshalQuick(t, 10000, 10000, 20000, false) } @@ -1403,55 +1689,55 @@ func isAllType(b *roaring.Bitmap, typ string) bool { return true } -// getBenchData yields some sample data -func getBenchData(tb testing.TB) *benchmarkSampleData { - data := &sampleData - if data.a1 == nil { - // throughout this, we ignore any errors from bitmap adds, - // because errors in those should result in the Optimize - // pass producing the wrong values, so we can just check there. - const max = (1 << 24) / 64 +func newBenchData(tb testing.TB) *benchmarkSampleData { + tb.Helper() - // Build bitmap with array container. - data.a1 = roaring.NewFileBitmap() - data.a2 = roaring.NewFileBitmap() - // two lists of different lengths - for i, n := 0, roaring.ArrayMaxSize/3; i < n; i++ { - _, _ = data.a1.Add(uint64(rand.Intn(max))) - _, _ = data.a2.Add(uint64(rand.Intn(max))) - } - for i, n := 0, roaring.ArrayMaxSize/3; i < n; i++ { - _, _ = data.a1.Add(uint64(rand.Intn(max))) - } + var data benchmarkSampleData - // Build bitmap with bitmap container. - data.b = roaring.NewFileBitmap() - for i, n := 0, MaxContainerVal/3; i < n; i++ { - _, _ = data.b.Add(uint64(i * 3)) - } - - // build bitmap with run container - data.r1 = roaring.NewFileBitmap() - for i, n := 0, MaxContainerVal; i < n; i++ { - _, _ = data.r1.Add(uint64(i)) - } - // build bitmap with multiple runs - data.r2 = roaring.NewFileBitmap() - for i, n := 0, MaxContainerVal; i < n; i++ { - _, _ = data.r2.Add(uint64(i)) - // break the runs up, this should produce 16 runs, which - // is small enough to make RLE tempting - if i&0xfff == 0xfff { - i += 5 - } - } - data.a1.Optimize() - data.a2.Optimize() - data.b.Optimize() - data.r1.Optimize() - data.r2.Optimize() + // throughout this, we ignore any errors from bitmap adds, + // because errors in those should result in the Optimize + // pass producing the wrong values, so we can just check there. + const max = (1 << 24) / 64 + // Build bitmap with array container. + data.a1 = roaring.NewFileBitmap() + data.a2 = roaring.NewFileBitmap() + // two lists of different lengths + for i, n := 0, roaring.ArrayMaxSize/3; i < n; i++ { + _, _ = data.a1.Add(uint64(rand.Intn(max))) + _, _ = data.a2.Add(uint64(rand.Intn(max))) } + for i, n := 0, roaring.ArrayMaxSize/3; i < n; i++ { + _, _ = data.a1.Add(uint64(rand.Intn(max))) + } + + // Build bitmap with bitmap container. + data.b = roaring.NewFileBitmap() + for i, n := 0, MaxContainerVal/3; i < n; i++ { + _, _ = data.b.Add(uint64(i * 3)) + } + + // build bitmap with run container + data.r1 = roaring.NewFileBitmap() + for i, n := 0, MaxContainerVal; i < n; i++ { + _, _ = data.r1.Add(uint64(i)) + } + // build bitmap with multiple runs + data.r2 = roaring.NewFileBitmap() + for i, n := 0, MaxContainerVal; i < n; i++ { + _, _ = data.r2.Add(uint64(i)) + // break the runs up, this should produce 16 runs, which + // is small enough to make RLE tempting + if i&0xfff == 0xfff { + i += 5 + } + } + data.a1.Optimize() + data.a2.Optimize() + data.b.Optimize() + data.r1.Optimize() + data.r2.Optimize() + if !isAllType(data.a1, "array") { tb.Fatalf("expected data.a1 to be an array, it wasn't.") } @@ -1467,6 +1753,34 @@ func getBenchData(tb testing.TB) *benchmarkSampleData { if !isAllType(data.r2, "run") { tb.Fatalf("expected data.r2 to be RLE, it wasn't.") } + + return &data +} + +// getBenchData yields some sample data +func getBenchData(tb testing.TB) *benchmarkSampleData { + tb.Helper() + + data := &sampleData + if data.a1 == nil { + data = newBenchData(tb) + } else { + if !isAllType(data.a1, "array") { + tb.Fatalf("expected data.a1 to be an array, it wasn't.") + } + if !isAllType(data.a2, "array") { + tb.Fatalf("expected data.a2 to be an array, it wasn't.") + } + if !isAllType(data.b, "bitmap") { + tb.Fatalf("expected data.b to be a bitmap, it wasn't.") + } + if !isAllType(data.r1, "run") { + tb.Fatalf("expected data.r1 to be RLE, it wasn't.") + } + if !isAllType(data.r2, "run") { + tb.Fatalf("expected data.r2 to be RLE, it wasn't.") + } + } return data } @@ -1726,23 +2040,11 @@ func BenchmarkSliceDescendingStriped(b *testing.B) { } } -func BenchmarkUnion(b *testing.B) { - data := getBenchData(b) - for n := 0; n < b.N; n++ { - data.a1. - Union(data.a2). - Union(data.b). - Union(data.r1). - Union(data.r2) - } -} - func BenchmarkUnionBulk(b *testing.B) { - data := getBenchData(b) for n := 0; n < b.N; n++ { - bm := roaring.NewBitmap() - bm. - UnionInPlace(data.a1, data.a2, data.b, data.r1, data.r2) + data := getBenchData(b) + bm := data.a1.Clone() + bm.UnionInPlace(data.a2, data.b, data.r1, data.r2) } } @@ -1789,13 +2091,58 @@ func TestBitmap_DifferenceInPlace(t *testing.T) { } } -func BenchmarkDifferencInPlace(b *testing.B) { - data := getBenchData(b) +func BenchmarkUnion(b *testing.B) { for n := 0; n < b.N; n++ { + data := getBenchData(b) bm := roaring.NewBitmap() - bm. - UnionInPlace(data.a1, data.a2, data.b, data.r1, data.r2) - bm. - DifferenceInPlace(data.a1, data.r2, data.b, data.r1) + bm.Union(data.a1).Union(data.a2).Union(data.b).Union(data.r1).Union(data.r2) + } +} + +func BenchmarkUnionInPlace(b *testing.B) { + for n := 0; n < b.N; n++ { + data := getBenchData(b) + bm := data.a1.Clone() + bm.UnionInPlace(data.a2, data.b, data.r1, data.r2) + } +} + +func BenchmarkDifference(b *testing.B) { + for n := 0; n < b.N; n++ { + data := getBenchData(b) + bm := data.a1.Clone() + bm.UnionInPlace(data.a2, data.b, data.r1, data.r2) + + bm.Difference(data.r2).Difference(data.b).Difference(data.r1) + } +} + +func BenchmarkDifferenceInPlace(b *testing.B) { + for n := 0; n < b.N; n++ { + data := getBenchData(b) + bm := data.a1.Clone() + bm.UnionInPlace(data.a2, data.b, data.r1, data.r2) + + bm.DifferenceInPlace(data.r2, data.b, data.r1) + } +} + +func BenchmarkIntersect(b *testing.B) { + for n := 0; n < b.N; n++ { + data := getBenchData(b) + bm := data.a1.Clone() + bm.UnionInPlace(data.a2, data.b, data.r1, data.r2) + + bm.Intersect(data.a2).Intersect(data.b).Intersect(data.r1).Intersect(data.r2) + } +} + +func BenchmarkIntersectInPlace(b *testing.B) { + for n := 0; n < b.N; n++ { + data := getBenchData(b) + bm := data.a1.Clone() + bm.UnionInPlace(data.a2, data.b, data.r1, data.r2) + + bm.IntersectInPlace(data.a2, data.b, data.r1, data.r2) } }