Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion arraycontainer.go
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@ func (ac *arrayContainer) ixorArray(value2 *arrayContainer) container {
}

func (ac *arrayContainer) ixorBitmap(value2 *bitmapContainer) container {
return value2.ixor(ac)
return value2.xorArray(ac)
}

func (ac *arrayContainer) ixorRun16(value2 *runContainer16) container {
Expand Down
86 changes: 86 additions & 0 deletions inplace_ownership_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package roaring

import "testing"

// Every in-place set operation must leave its argument untouched, and the
// two bitmaps must not share storage afterwards.
func TestInPlaceOpsPreserveArgument(t *testing.T) {
const hi = 1 << 16
dense := func(base uint32) *Bitmap {
b := New()
for i := uint32(0); i < 65536; i += 2 {
b.Add(base | i)
}
return b
}
run := func(base uint32) *Bitmap {
b := New()
b.AddRange(uint64(base)+100, uint64(base)+60000)
return b
}
twoKeys := func(b *Bitmap) *Bitmap {
b = b.Clone()
b.AddRange(hi, hi+5)
return b
}
shapes := []struct {
name string
bm *Bitmap
}{
{"empty", New()}, {"array", BitmapOf(1, 3, 65535)}, {"bitmap", dense(0)}, {"run", run(0)},
{"array+key", twoKeys(BitmapOf(2, 4))}, {"run+key", twoKeys(run(0))},
{"hi-array", BitmapOf(hi|1, hi|3)}, {"hi-bitmap", dense(hi)}, {"hi-run", run(hi)},
}
ops := []struct {
name string
inPlace func(a, b *Bitmap)
pure func(a, b *Bitmap) *Bitmap
}{
{"Or", func(a, b *Bitmap) { a.Or(b) }, Or},
{"And", func(a, b *Bitmap) { a.And(b) }, And},
{"AndNot", func(a, b *Bitmap) { a.AndNot(b) }, AndNot},
{"Xor", func(a, b *Bitmap) { a.Xor(b) }, Xor},
}
for _, op := range ops {
for _, left := range shapes {
for _, right := range shapes {
l, r := left.bm, right.bm
for _, cow := range []bool{false, true} {
name := op.name + "/" + left.name + "/" + right.name
a, b := l.Clone(), r.Clone()
a.SetCopyOnWrite(cow)
b.SetCopyOnWrite(cow)
snapshot := a.Clone()
want := op.pure(l, r)
op.inPlace(a, b)
if !snapshot.Equals(l) {
t.Fatalf("%s: a clone of the receiver changed (copy-on-write %v)", name, cow)
}
if !a.Equals(want) {
t.Fatalf("%s: wrong result", name)
}
if err := a.Validate(); err != nil {
t.Fatalf("%s: %v", name, err)
}
if !b.Equals(r) {
t.Fatalf("%s: argument modified (copy-on-write %v)", name, cow)
}
it := r.Iterator()
for n := 0; n < 50 && it.HasNext(); n++ {
a.Remove(it.Next())
}
a.Add(65533)
a.AddRange(hi+70000, hi+70050)
if !b.Equals(r) {
t.Fatalf("%s: editing the result changed the argument (copy-on-write %v)", name, cow)
}
}
}
a := left.bm.Clone()
op.inPlace(a, a)
if !a.Equals(op.pure(left.bm, left.bm)) {
t.Fatalf("%s/%s: wrong result for a bitmap applied to itself", op.name, left.name)
}
}
}
}
86 changes: 86 additions & 0 deletions roaring64/inplace_ownership_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package roaring64

import "testing"

// Every in-place set operation must leave its argument untouched, and the
// two bitmaps must not share storage afterwards.
func TestInPlaceOpsPreserveArgument(t *testing.T) {
const hi = 1 << 32
dense := func(base uint64) *Bitmap {
b := New()
for i := uint64(0); i < 65536; i += 2 {
b.Add(base | i)
}
return b
}
run := func(base uint64) *Bitmap {
b := New()
b.AddRange(base+100, base+60000)
return b
}
twoKeys := func(b *Bitmap) *Bitmap {
b = b.Clone()
b.AddRange(hi, hi+5)
return b
}
shapes := []struct {
name string
bm *Bitmap
}{
{"empty", New()}, {"array", BitmapOf(1, 3, 65535)}, {"bitmap", dense(0)}, {"run", run(0)},
{"array+key", twoKeys(BitmapOf(2, 4))}, {"run+key", twoKeys(run(0))},
{"hi-array", BitmapOf(hi|1, hi|3)}, {"hi-bitmap", dense(hi)}, {"hi-run", run(hi)},
}
ops := []struct {
name string
inPlace func(a, b *Bitmap)
pure func(a, b *Bitmap) *Bitmap
}{
{"Or", func(a, b *Bitmap) { a.Or(b) }, Or},
{"And", func(a, b *Bitmap) { a.And(b) }, And},
{"AndNot", func(a, b *Bitmap) { a.AndNot(b) }, AndNot},
{"Xor", func(a, b *Bitmap) { a.Xor(b) }, Xor},
}
for _, op := range ops {
for _, left := range shapes {
for _, right := range shapes {
l, r := left.bm, right.bm
for _, cow := range []bool{false, true} {
name := op.name + "/" + left.name + "/" + right.name
a, b := l.Clone(), r.Clone()
a.SetCopyOnWrite(cow)
b.SetCopyOnWrite(cow)
snapshot := a.Clone()
want := op.pure(l, r)
op.inPlace(a, b)
if !snapshot.Equals(l) {
t.Fatalf("%s: a clone of the receiver changed (copy-on-write %v)", name, cow)
}
if !a.Equals(want) {
t.Fatalf("%s: wrong result", name)
}
if err := a.Validate(); err != nil {
t.Fatalf("%s: %v", name, err)
}
if !b.Equals(r) {
t.Fatalf("%s: argument modified (copy-on-write %v)", name, cow)
}
it := r.Iterator()
for n := 0; n < 50 && it.HasNext(); n++ {
a.Remove(it.Next())
}
a.Add(65533)
a.AddRange(hi+70000, hi+70050)
if !b.Equals(r) {
t.Fatalf("%s: editing the result changed the argument (copy-on-write %v)", name, cow)
}
}
}
a := left.bm.Clone()
op.inPlace(a, a)
if !a.Equals(op.pure(left.bm, left.bm)) {
t.Fatalf("%s/%s: wrong result for a bitmap applied to itself", op.name, left.name)
}
}
}
}
10 changes: 7 additions & 3 deletions roaring64/roaring64.go
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,10 @@ main:

// Xor computes the symmetric difference between two bitmaps and stores the result in the current bitmap
func (rb *Bitmap) Xor(x2 *Bitmap) {
if rb == x2 {
rb.Clear()
return
}
pos1 := 0
pos2 := 0
length1 := rb.highlowcontainer.size()
Expand All @@ -646,14 +650,14 @@ func (rb *Bitmap) Xor(x2 *Bitmap) {
break
}
} else if s1 > s2 {
c := x2.highlowcontainer.getWritableContainerAtIndex(pos2)
c := x2.highlowcontainer.getContainerAtIndex(pos2).Clone()
rb.highlowcontainer.insertNewKeyValueAt(pos1, x2.highlowcontainer.getKeyAtIndex(pos2), c)
length1++
pos1++
pos2++
} else {
// TODO: couple be computed in-place for reduced memory usage
c := roaring.Xor(rb.highlowcontainer.getContainerAtIndex(pos1), x2.highlowcontainer.getContainerAtIndex(pos2))
c := rb.highlowcontainer.getWritableContainerAtIndex(pos1)
c.Xor(x2.highlowcontainer.getContainerAtIndex(pos2))
if !c.IsEmpty() {
rb.highlowcontainer.setContainerAtIndex(pos1, c)
pos1++
Expand Down
93 changes: 93 additions & 0 deletions roaring64/xor_inplace_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package roaring64

import "testing"

func TestXorInPlaceDenseAllocations(t *testing.T) {
x, y := New(), New()
for i := uint64(0); i < 65536; i++ {
if i%2 == 0 {
x.Add(1<<40 | i)
}
if i%4 < 2 {
y.Add(1<<40 | i)
}
}
original := x.Clone()
allocs := testing.AllocsPerRun(10, func() {
x.Xor(y)
x.Xor(y)
})
if allocs != 0 {
t.Fatalf("dense in-place XOR allocated %g times", allocs)
}
if !x.Equals(original) {
t.Fatal("two XORs must restore the receiver")
}
}

func BenchmarkXorInPlace64(b *testing.B) {
for _, kind := range []string{"dense", "array", "run", "disjoint-keys"} {
x, y := New(), New()
for high := uint64(1); high <= 4; high++ {
base := high << 40
switch kind {
case "dense":
for i := uint64(0); i < 65536; i++ {
if i%2 == 0 {
x.Add(base | i)
}
if i%4 < 2 {
y.Add(base | i)
}
}
case "array":
for i := uint64(0); i < 128; i++ {
if i%2 == 0 {
x.Add(base | i)
}
if i%4 < 2 {
y.Add(base | i)
}
}
case "run":
x.AddRange(base+100, base+30000)
y.AddRange(base+20000, base+50000)
case "disjoint-keys":
x.Add(base | 1)
y.Add((base - 1<<32) | 2)
}
}
for _, clone := range []bool{false, true} {
name := kind + "/reuse"
if clone {
name = kind + "/clone-and-xor"
}
b.Run(name, func(b *testing.B) {
x, y := x.Clone(), y.Clone()
original := x.Clone()
want := Xor(x, y)
b.ReportAllocs()
var result *Bitmap
for b.Loop() {
if clone {
result = x.Clone()
result.Xor(y)
} else {
x.Xor(y)
x.Xor(y)
}
}
if clone {
if !result.Equals(want) {
b.Fatal("wrong XOR result")
}
} else {
if !x.Equals(original) {
b.Fatal("two XORs did not restore the receiver")
}
b.ReportMetric(2, "xors/op")
}
})
}
}
}
2 changes: 1 addition & 1 deletion runcontainer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2452,7 +2452,7 @@ func (rc *runContainer16) ixorArray(value2 *arrayContainer) container {
}

func (rc *runContainer16) ixorBitmap(value2 *bitmapContainer) container {
return value2.ixor(rc)
return rc.toBitmapContainer().ixorBitmap(value2)
}

func (rc *runContainer16) ixorRunContainer16(value2 *runContainer16) container {
Expand Down