Skip to content

Commit

Permalink
impl. common sparse array
Browse files Browse the repository at this point in the history
  • Loading branch information
gaissmai committed Nov 10, 2024
1 parent 4beaabf commit 3aa8ddb
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 414 deletions.
34 changes: 34 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
linters:
enable:
- gofmt
- gofumpt
- goimports
- govet
- gosimple
- gocyclo
- gosmopolitan
- errcheck
- staticcheck
- intrange
- ineffassign
- unused
- typecheck
- unconvert
- unparam
- misspell
- decorder
- errorlint
- predeclared
- reassign
- stylecheck
- usestdlibvars
- wastedassign
- nakedret
#
# - gosec
# - dupl
# - wrapcheck

linters-settings:
gocyclo:
min-complexity: 30
10 changes: 5 additions & 5 deletions deprecated.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ func (t *Table[V]) EachLookupPrefix(pfx netip.Prefix, yield func(pfx netip.Prefi
stack[i] = n

// go down in tight loop
c := n.getChild(octet)
if c == nil {
c, ok := n.children.Get(uint(octet))
if !ok {
break
}

Expand All @@ -71,7 +71,7 @@ func (t *Table[V]) EachLookupPrefix(pfx netip.Prefix, yield func(pfx netip.Prefi
n = stack[depth]

// microbenchmarking
if len(n.prefixes) == 0 {
if n.prefixes.Count() == 0 {
continue
}

Expand Down Expand Up @@ -137,8 +137,8 @@ func (t *Table[V]) EachSubnet(pfx netip.Prefix, yield func(pfx netip.Prefix, val
return
}

c := n.getChild(octet)
if c == nil {
c, ok := n.children.Get(uint(octet))
if !ok {
break
}

Expand Down
34 changes: 17 additions & 17 deletions dumper.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ func (n *node[V]) dumpRec(w io.Writer, path [16]byte, depth int, is4 bool) {
addrBacking := make([]uint, maxNodeChildren)

// the node may have childs, the rec-descent monster starts
for i, addr := range n.allChildAddrs(addrBacking) {
for i, addr := range n.children.AllSetBits(addrBacking) {
octet := byte(addr)
child := n.children[i]
child := n.children.Items[i]
path[depth] = octet

child.dumpRec(w, path, depth+1, is4)
Expand All @@ -98,16 +98,16 @@ func (n *node[V]) dump(w io.Writer, path [16]byte, depth int, is4 bool) {
fmt.Fprintf(w, "\n%s[%s] depth: %d path: [%s] / %d\n",
indent, n.hasType(), depth, ipStridePath(path, depth, is4), bits)

if nPfxLen := len(n.prefixes); nPfxLen != 0 {
if nPfxCount := n.prefixes.Count(); nPfxCount != 0 {
// make backing array, no heap allocs
idxBackingArray := [maxNodePrefixes]uint{}
allIndices := n.allStrideIndexes(idxBackingArray[:])
allIndices := n.prefixes.AllSetBits(idxBackingArray[:])

// print the baseIndices for this node.
fmt.Fprintf(w, "%sindexs(#%d): %v\n", indent, nPfxLen, allIndices)
fmt.Fprintf(w, "%sindexs(#%d): %v\n", indent, nPfxCount, allIndices)

// print the prefixes for this node
fmt.Fprintf(w, "%sprefxs(#%d):", indent, nPfxLen)
fmt.Fprintf(w, "%sprefxs(#%d):", indent, nPfxCount)

for _, idx := range allIndices {
octet, pfxLen := idxToPfx(idx)
Expand All @@ -117,21 +117,21 @@ func (n *node[V]) dump(w io.Writer, path [16]byte, depth int, is4 bool) {
fmt.Fprintln(w)

// print the values for this node
fmt.Fprintf(w, "%svalues(#%d):", indent, nPfxLen)
fmt.Fprintf(w, "%svalues(#%d):", indent, nPfxCount)

for _, val := range n.prefixes {
for _, val := range n.prefixes.Items {
fmt.Fprintf(w, " %v", val)
}

fmt.Fprintln(w)
}

if childs := len(n.children); childs != 0 {
if childCount := n.children.Count(); childCount != 0 {
// print the childs for this node
fmt.Fprintf(w, "%schilds(#%d):", indent, childs)
fmt.Fprintf(w, "%schilds(#%d):", indent, childCount)

addrBacking := make([]uint, maxNodeChildren)
for _, addr := range n.allChildAddrs(addrBacking) {
for _, addr := range n.children.AllSetBits(addrBacking) {
octet := byte(addr)
fmt.Fprintf(w, " %s", octetFmt(octet, is4))
}
Expand Down Expand Up @@ -197,22 +197,22 @@ func (nt nodeType) String() string {

// hasType returns the nodeType.
func (n *node[V]) hasType() nodeType {
lenPefixes := len(n.prefixes)
lenChilds := len(n.children)
prefixCount := n.prefixes.Count()
childCount := n.children.Count()

if lenPefixes == 0 && lenChilds != 0 {
if prefixCount == 0 && childCount != 0 {
return intermediateNode
}

if lenPefixes == 0 && lenChilds == 0 {
if prefixCount == 0 && childCount == 0 {
return nullNode
}

if lenPefixes != 0 && lenChilds == 0 {
if prefixCount != 0 && childCount == 0 {
return leafNode
}

if lenPefixes != 0 && lenChilds != 0 {
if prefixCount != 0 && childCount != 0 {
return fullNode
}

Expand Down
129 changes: 129 additions & 0 deletions internal/sparse/array.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package sparse

import (
"slices"

"github.com/bits-and-blooms/bitset"
)

// Array, a generic implementation of a sparse array
// with popcount compression and payload T.
type Array[T any] struct {
*bitset.BitSet
Items []T
}

// NewArray, initialize BitSet with zero value.
func NewArray[T any]() *Array[T] {
return &Array[T]{
BitSet: &bitset.BitSet{},
}
}

// Count, number of items in sparse array.
func (s *Array[T]) Count() int {
// faster than BitSet.Count()
return len(s.Items)
}

// rank is the key of the popcount compression algorithm,
// mapping between bitset index and slice index.
func (s *Array[T]) rank(idx uint) int {
// adjust offset by one to slice index.
return int(s.BitSet.Rank(idx)) - 1
}

// InsertAt a value at idx into the sparse array.
func (s *Array[T]) InsertAt(idx uint, val T) (ok bool) {
// prefix exists, overwrite val
if s.BitSet.Test(idx) {
s.Items[s.rank(idx)] = val

return false
}

// new, insert into bitset and slice
s.BitSet.Set(idx)
s.Items = slices.Insert(s.Items, s.rank(idx), val)

return true
}

// DeleteAt, delete a value at idx from the sparse array.
func (s *Array[T]) DeleteAt(idx uint) (T, bool) {
var zero T
if !s.BitSet.Test(idx) {
return zero, false
}

rnk := s.rank(idx)
val := s.Items[rnk]

// delete from slice
s.Items = slices.Delete(s.Items, rnk, rnk+1)

// delete from bitset, followed by Compact to reduce memory consumption
s.BitSet.Clear(idx)
s.BitSet.Compact()

return val, true
}

// Get, get the value at idx from sparse array.
func (s *Array[T]) Get(idx uint) (T, bool) {
var zero T

if s.BitSet.Test(idx) {
return s.Items[int(s.BitSet.Rank(idx))-1], true
}

return zero, false
}

// MustGet, use it only after a successful test,
// panics otherwise.
func (s *Array[T]) MustGet(idx uint) T {
return s.Items[int(s.BitSet.Rank(idx))-1]
}

// UpdateAt or set the value at idx via callback. The new value is returned
// and true if the val was already present.
func (s *Array[T]) UpdateAt(idx uint, cb func(T, bool) T) (newVal T, wasPresent bool) {
var rnk int

// if already set, get current value
var oldVal T

if wasPresent = s.BitSet.Test(idx); wasPresent {
rnk = s.rank(idx)
oldVal = s.Items[rnk]
}

// callback function to get updated or new value
newVal = cb(oldVal, wasPresent)

// already set, update and return value
if wasPresent {
s.Items[rnk] = newVal

return newVal, wasPresent
}

// new val, insert into bitset ...
s.BitSet.Set(idx)

// bitset has changed, recalc rank
rnk = s.rank(idx)

// ... and insert value into slice
s.Items = slices.Insert(s.Items, rnk, newVal)

return newVal, wasPresent
}

// AllSetBits, retrieve all set bits in the sparse array, panics if the buffer isn't big enough.
func (s *Array[T]) AllSetBits(buffer []uint) []uint {
_, buffer = s.BitSet.NextSetMany(0, buffer)

return buffer
}
Loading

0 comments on commit 3aa8ddb

Please sign in to comment.