Abstract away access to container slices

On a 64-bit machine, the slices in a Container consume 72
bytes, and the Container itself is 80. But we only use one
slice at a time! This patch shifts us to keeping a single
slice in the Container, and converting provided slices to
and from that type when we want to update it. (It is not
safe to access the slice through the wrong type.)

We also add some new tests, conditional on a build tag
called `roaringparanoia`. These tests will be optimized
away entirely by the compiler when the tag isn't
present, because the conditionals use a const. These catch
possible errors like trying to access the bitmap slice
of a non-bitmap container.

We also eliminate all direct creation of Container literals,
so we can mess with the internals more. (On reflection
and study, we decided not to go to the fancier design where
references to .n and .typ were also converted to function
calls, which would have allowed packing those attributes
more tightly, because it was a lot more overhead and a lot
of work to keep track of.)

There's some circumstances where we appear to have been
relying on incorrect guesses about the nature of containers.
For instance, in xorBitmapRun, there's logic that makes sense
only if the output's a run container, but it's not, it's a
bitmap container. This creates strange behavior sometimes,
though. Several of these are corrected now.
This commit is contained in:
Seebs 2019-03-15 12:47:32 -05:00
parent 86ea040639
commit 47dcb5b4a7
11 changed files with 1124 additions and 1031 deletions

View file

@ -93,8 +93,8 @@ type updater struct {
mapped bool
}
func (btc *bTreeContainers) PutContainerValues(key uint64, containerType byte, n int, mapped bool) {
a := updater{key, int32(n), containerType, mapped}
func (btc *bTreeContainers) PutContainerValues(key uint64, typ byte, n int, mapped bool) {
a := updater{key, int32(n), typ, mapped}
btc.tree.Put(key, a.update)
}
@ -111,7 +111,7 @@ func (btc *bTreeContainers) GetOrCreate(key uint64) *roaring.Container {
btc.lastKey = key
v, ok := btc.tree.Get(key)
if !ok {
cont := roaring.NewContainer()
cont := roaring.NewContainerArray(nil)
btc.tree.Set(key, cont)
btc.lastContainer = cont
return cont

182
roaring/container_slice.go Normal file
View file

@ -0,0 +1,182 @@
package roaring
import (
"unsafe"
)
// Container represents a Container for uint16 integers.
//
// These are used for storing the low bits of numbers in larger sets of uint64.
// The high bits are stored in a Container's key which is tracked by a separate
// data structure. Integers in a Container can be encoded in one of three ways -
// the encoding used is usually whichever is most compact, though any Container
// type should be able to encode any set of integers safely. For containers with
// less than 4,096 values, an array is often used. Containers with long runs of
// integers would use run length encoding, and more random data usually uses
// bitmap encoding.
type Container struct {
oneSlice []uint16 // and here is where the magic happens
n int32 // number of integers in container
mapped bool // mapped directly to a byte slice when true
typ byte // array, bitmap, or run
}
// NewContainer returns a new instance of container. This trivial function
// may later become more interesting.
func NewContainer() *Container {
statsHit("NewContainer")
return &Container{typ: containerArray}
}
// NewContainerBitmap makes a bitmap container using the provided bitmap, or
// an empty one if provided bitmap is nil. If the provided bitmap is too short,
// it will be padded.
func NewContainerBitmap(n int32, bitmap []uint64) *Container {
if bitmap == nil {
bitmap = make([]uint64, bitmapN)
}
// pad to required length
if len(bitmap) < bitmapN {
bm2 := make([]uint64, bitmapN)
copy(bm2, bitmap)
bitmap = bm2
}
return &Container{typ: containerBitmap, n: n, oneSlice: *(*[]uint16)(unsafe.Pointer(&bitmap))}
}
// NewContainerArray returns an array using the provided set of values. It's
// okay if the slice is nil; that's a length of zero.
func NewContainerArray(set []uint16) *Container {
return &Container{typ: containerArray, n: int32(len(set)), oneSlice: *(*[]uint16)(unsafe.Pointer(&set))}
}
// NewContainerRun creates a new run array using a provided (possibly nil)
// slice of intervals.
func NewContainerRun(set []interval16) *Container {
c := &Container{typ: containerRun, oneSlice: *(*[]uint16)(unsafe.Pointer(&set))}
for _, run := range set {
c.n += int32(run.last-run.start) + 1
}
return c
}
// array yields the data viewed as a slice of intervals.
func (c *Container) array() []uint16 {
if roaringParanoia {
if c.typ != containerArray {
panic("attempt to read non-array's array")
}
}
return *(*[]uint16)(unsafe.Pointer(&c.oneSlice))
}
// setArray stores a set of uint16s as data.
func (c *Container) setArray(array []uint16) {
if roaringParanoia {
if c.typ != containerArray {
panic("attempt to write non-array's array")
}
}
c.oneSlice = *(*[]uint16)(unsafe.Pointer(&array))
}
// bitmap yields the data viewed as a slice of uint64s holding bits.
func (c *Container) bitmap() []uint64 {
if roaringParanoia {
if c.typ != containerBitmap {
panic("attempt to read non-bitmap's bitmap")
}
}
return *(*[]uint64)(unsafe.Pointer(&c.oneSlice))
}
// setBitmap stores a set of uint64s as data.
func (c *Container) setBitmap(bitmap []uint64) {
if roaringParanoia {
if c.typ != containerBitmap {
panic("attempt to write non-bitmap's bitmap")
}
}
c.oneSlice = *(*[]uint16)(unsafe.Pointer(&bitmap))
}
// runs yields the data viewed as a slice of intervals.
func (c *Container) runs() []interval16 {
if roaringParanoia {
if c.typ != containerRun {
panic("attempt to read non-run's runs")
}
}
return *(*[]interval16)(unsafe.Pointer(&c.oneSlice))
}
// setRuns stores a set of intervals as data.
func (c *Container) setRuns(runs []interval16) {
if roaringParanoia {
if c.typ != containerRun {
panic("attempt to write non-run's runs")
}
}
c.oneSlice = *(*[]uint16)(unsafe.Pointer(&runs))
}
// Mapped returns true if the container is mapped directly to a byte slice
func (c *Container) Mapped() bool {
return c.mapped
}
// N returns the cached bit count of the container
func (c *Container) N() int32 {
return c.n
}
// Update updates the container
func (c *Container) Update(typ byte, n int32, mapped bool) {
c.typ = typ
c.n = n
c.mapped = mapped
// we don't know that any existing slice is usable, so let's ditch it
c.oneSlice = nil
}
// isArray returns true if the container is an array container.
func (c *Container) isArray() bool {
return c.typ == containerArray
}
// isBitmap returns true if the container is a bitmap container.
func (c *Container) isBitmap() bool {
return c.typ == containerBitmap
}
// isRun returns true if the container is a run-length-encoded container.
func (c *Container) isRun() bool {
return c.typ == containerRun
}
// unmap creates copies of the containers data in the heap.
//
// This is performed when altering the container since its contents could be
// pointing at a read-only mmap.
func (c *Container) unmap() {
if !c.mapped {
return
}
switch c.typ {
case containerArray:
tmp := make([]uint16, len(c.array()))
copy(tmp, c.array())
c.setArray(tmp)
case containerBitmap:
tmp := make([]uint64, len(c.bitmap()))
copy(tmp, c.bitmap())
c.setBitmap(tmp)
case containerRun:
tmp := make([]interval16, len(c.runs()))
copy(tmp, c.runs())
c.setRuns(tmp)
}
c.mapped = false
}

View file

@ -46,17 +46,17 @@ func (sc *sliceContainers) Put(key uint64, c *Container) {
}
func (sc *sliceContainers) PutContainerValues(key uint64, containerType byte, n int, mapped bool) {
func (sc *sliceContainers) PutContainerValues(key uint64, typ byte, n int, mapped bool) {
i := search64(sc.keys, key)
if i < 0 {
c := NewContainer()
c.containerType = containerType
c.typ = typ
c.n = int32(n)
c.mapped = mapped
sc.insertAt(key, c, -i-1)
} else {
c := sc.containers[i]
c.containerType = containerType
c.typ = typ
c.n = int32(n)
c.mapped = mapped
}
@ -93,7 +93,7 @@ func (sc *sliceContainers) GetOrCreate(key uint64) *Container {
sc.lastKey = key
i := search64(sc.keys, key)
if i < 0 {
c := NewContainer()
c := NewContainerArray(nil)
sc.insertAt(key, c, -i-1)
sc.lastContainer = c
return c

View file

@ -72,24 +72,26 @@ func (btc *bTreeContainers) Put(key uint64, c *Container) {
func (u updater) update(oldV *Container, exists bool) (*Container, bool) {
// update the existing container
if exists {
oldV.Update(u.containerType, u.n, u.mapped)
oldV.Update(u.typ, u.n, u.mapped)
return oldV, false
}
cont := NewContainer()
cont.Update(u.containerType, u.n, u.mapped)
cont.typ = u.typ
cont.n = u.n
cont.mapped = u.mapped
return cont, true
}
// this struct is added to prevent the closure locals from being escaped out to the heap
type updater struct {
key uint64
n int32
containerType byte
mapped bool
key uint64
n int32
typ byte
mapped bool
}
func (btc *bTreeContainers) PutContainerValues(key uint64, containerType byte, n int, mapped bool) {
a := updater{key, int32(n), containerType, mapped}
func (btc *bTreeContainers) PutContainerValues(key uint64, typ byte, n int, mapped bool) {
a := updater{key, int32(n), typ, mapped}
btc.tree.Put(key, a.update)
}
@ -106,7 +108,7 @@ func (btc *bTreeContainers) GetOrCreate(key uint64) *Container {
btc.lastKey = key
v, ok := btc.tree.Get(key)
if !ok {
cont := NewContainer()
cont := NewContainerArray(nil)
btc.tree.Set(key, cont)
btc.lastContainer = cont
return cont

View file

@ -32,8 +32,8 @@ func testContainersIterator(cs Containers, t *testing.T) {
t.Fatal("Next() should be false for empty btc")
}
cs.Put(1, &Container{n: 1})
cs.Put(2, &Container{n: 2})
cs.Put(1, NewContainerArray([]uint16{1}))
cs.Put(2, NewContainerArray([]uint16{1, 2}))
itr, found = cs.Iterator(0)
if found {
@ -57,9 +57,9 @@ func testContainersIterator(cs Containers, t *testing.T) {
t.Fatalf("itr should be done, but got true")
}
cs.Put(3, &Container{n: 3})
cs.Put(5, &Container{n: 5})
cs.Put(6, &Container{n: 6})
cs.Put(3, NewContainerArray([]uint16{1, 2, 3}))
cs.Put(5, NewContainerArray([]uint16{1, 2, 3, 4, 5}))
cs.Put(6, NewContainerArray([]uint16{1, 2, 3, 4, 5, 6}))
itr, found = cs.Iterator(3)
if !itr.Next() {

File diff suppressed because it is too large Load diff

View file

@ -248,22 +248,18 @@ type testOp struct {
exp string
}
func doContainer(containerType byte, data interface{}) *Container {
c := &Container{
containerType: containerType,
}
switch containerType {
func doContainer(typ byte, data interface{}) *Container {
switch typ {
case containerArray:
c.array = data.([]uint16)
return NewContainerArray(data.([]uint16))
case containerBitmap:
c.bitmap = data.([]uint64)
c := NewContainerBitmap(0, data.([]uint64))
c.n = c.count()
return c
case containerRun:
c.runs = data.([]interval16)
return NewContainerRun(data.([]interval16))
}
c.n = c.count()
return c
return nil
}
func setupContainerTests() map[byte]map[string]*Container {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,5 @@
// +build !roaringparanoia
package roaring
const roaringParanoia = false

View file

@ -0,0 +1,5 @@
// +build roaringparanoia
package roaring
const roaringParanoia = true

View file

@ -435,14 +435,14 @@ func TestBitmap_UnionInPlace1(t *testing.T) {
result.UnionInPlace(bm0, bm1)
if n := result.Count(); n != 2682675 {
t.Fatalf("unexpected n: %d", n)
t.Fatalf("unexpected n: got %d, expected 2682675", n)
}
bm := testBM()
result = roaring.NewBitmap()
result.UnionInPlace(bm, bm0)
if n := result.Count(); n != 75009 {
t.Fatalf("unexpected n: %d", n)
t.Fatalf("unexpected n: got %d, expected 75009", n)
}
result = roaring.NewBitmap()