From 3aa8ddb647d162edabaf5bcb6188a5615c555b21 Mon Sep 17 00:00:00 2001 From: Karl Gaissmaier Date: Fri, 8 Nov 2024 20:18:31 +0100 Subject: [PATCH] impl. common sparse array --- .golangci.yml | 34 +++++ deprecated.go | 10 +- dumper.go | 34 ++--- internal/sparse/array.go | 129 ++++++++++++++++ node.go | 318 ++++++++++----------------------------- node_test.go | 102 ++++--------- overlaps.go | 74 ++++----- stringify.go | 8 +- table.go | 65 ++++---- table_iter.go | 10 +- 10 files changed, 370 insertions(+), 414 deletions(-) create mode 100644 .golangci.yml create mode 100644 internal/sparse/array.go diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..487db91 --- /dev/null +++ b/.golangci.yml @@ -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 diff --git a/deprecated.go b/deprecated.go index 33ddc10..19cfa8c 100644 --- a/deprecated.go +++ b/deprecated.go @@ -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 } @@ -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 } @@ -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 } diff --git a/dumper.go b/dumper.go index 1748330..8c22d9e 100644 --- a/dumper.go +++ b/dumper.go @@ -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) @@ -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) @@ -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)) } @@ -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 } diff --git a/internal/sparse/array.go b/internal/sparse/array.go new file mode 100644 index 0000000..6ec5204 --- /dev/null +++ b/internal/sparse/array.go @@ -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 +} diff --git a/node.go b/node.go index f0791fe..8f02837 100644 --- a/node.go +++ b/node.go @@ -7,17 +7,17 @@ import ( "net/netip" "slices" - "github.com/bits-and-blooms/bitset" + "github.com/gaissmai/bart/internal/sparse" ) const ( - strideLen = 8 // octet - maxTreeDepth = 128 / strideLen // 16 - maxNodeChildren = 1 << strideLen // 256 - maxNodePrefixes = 1 << (strideLen + 1) // 512 + strideLen = 8 // octet + maxTreeDepth = 16 // 16 for IPv6 + maxNodeChildren = 256 // 256 + maxNodePrefixes = 512 // 512 ) -// zero value, used manifold +// a zero value, used manifold var zeroPath [16]byte // node is a level node in the multibit-trie. @@ -26,137 +26,33 @@ var zeroPath [16]byte // The prefixes form a complete binary tree, see the artlookup.pdf // paper in the doc folder to understand the data structure. // -// In contrast to the ART algorithm, popcount-compressed slices are used -// instead of fixed-size arrays. +// In contrast to the ART algorithm, sparse arrays +// (popcount-compressed slices) are used instead of fixed-size arrays. // -// The array slots are also not pre-allocated as in the ART algorithm, -// but backtracking is used for the longest-prefix-match. +// The array slots are also not pre-allocated (alloted) as described +// in the ART algorithm, but backtracking is used for the longest-prefix-match. // // The lookup is then slower by a factor of about 2, but this is // the intended trade-off to prevent memory consumption from exploding. type node[V any] struct { - // prefixes contains the payload V - prefixes []V + // prefixes contains the routes with payload V + prefixes *sparse.Array[V] // children, recursively spans the trie with a branching factor of 256 - children []*node[V] - - // Here we would be done if they were fixed arrays, but since they - // are popcount compressed slices we need bitsets. - // --- - // To address a specific element in prefixes or children - // the popcount of the bitset is calculated up to the desired element, - // this gives the position of the element in the corresponding slice. - // - // e.g. find the value V for prefix 10/7: - // pfxToIdx(10/7) -> 133; popcount(133) -> i; V = prefixes[i] - // - // e.g. find the next node for octet(253): - // popcount(253) -> i; *n = children[i] - // - prefixesBitset *bitset.BitSet - childrenBitset *bitset.BitSet + children *sparse.Array[*node[V]] } -// newNode, the zero-value of BitSet is ready to use, -// not using bitset.New(), this would be not inlineable. +// newNode with sparse arrays for prefixes and children. func newNode[V any]() *node[V] { return &node[V]{ - prefixesBitset: &bitset.BitSet{}, - childrenBitset: &bitset.BitSet{}, + prefixes: sparse.NewArray[V](), + children: sparse.NewArray[*node[V]](), } } // isEmpty returns true if node has neither prefixes nor children. func (n *node[V]) isEmpty() bool { - return len(n.prefixes) == 0 && len(n.children) == 0 -} - -// ################## prefixes ################################ - -// prefixRank, Rank() is the key of the popcount compression algorithm, -// mapping between bitset index and slice index. -func (n *node[V]) prefixRank(idx uint) int { - // adjust offset by one to slice index - return int(n.prefixesBitset.Rank(idx)) - 1 -} - -// insertPrefix adds the route as baseIdx, with value val. -// If the value already exists, overwrite it with val and return false. -func (n *node[V]) insertPrefix(idx uint, val V) (ok bool) { - // prefix exists, overwrite val - if n.prefixesBitset.Test(idx) { - n.prefixes[n.prefixRank(idx)] = val - - return false - } - - // new, insert into bitset and slice - n.prefixesBitset.Set(idx) - n.prefixes = slices.Insert(n.prefixes, n.prefixRank(idx), val) - - return true -} - -// deletePrefix removes the route octet/prefixLen and returns the associated value and true -// or false if there was no prefix to delete (and no value to return). -func (n *node[V]) deletePrefix(octet byte, prefixLen int) (val V, ok bool) { - idx := pfxToIdx(octet, prefixLen) - - // no route entry - if !n.prefixesBitset.Test(idx) { - return val, false - } - - rnk := n.prefixRank(idx) - val = n.prefixes[rnk] - - // delete from slice - n.prefixes = slices.Delete(n.prefixes, rnk, rnk+1) - - // delete from bitset, followed by Compact to reduce memory consumption - n.prefixesBitset.Clear(idx) - n.prefixesBitset.Compact() - - return val, true -} - -// updatePrefix, update or set the value at prefix via callback. The new value returned -// and a bool wether the prefix was already present in the node. -func (n *node[V]) updatePrefix(octet byte, prefixLen int, cb func(V, bool) V) (newVal V, wasPresent bool) { - // calculate idx once - idx := pfxToIdx(octet, prefixLen) - - var rnk int - - // if prefix is set, get current value - var oldVal V - - if wasPresent = n.prefixesBitset.Test(idx); wasPresent { - rnk = n.prefixRank(idx) - oldVal = n.prefixes[rnk] - } - - // callback function to get updated or new value - newVal = cb(oldVal, wasPresent) - - // prefix is already set, update and return value - if wasPresent { - n.prefixes[rnk] = newVal - - return - } - - // new prefix, insert into bitset ... - n.prefixesBitset.Set(idx) - - // bitset has changed, recalc rank - rnk = n.prefixRank(idx) - - // ... and insert value into slice - n.prefixes = slices.Insert(n.prefixes, rnk, newVal) - - return + return n.prefixes.Count() == 0 && n.children.Count() == 0 } // lpm does a route lookup for idx in the 8-bit (stride) routing table @@ -168,9 +64,9 @@ func (n *node[V]) updatePrefix(octet byte, prefixLen int, cb func(V, bool) V) (n func (n *node[V]) lpm(idx uint) (baseIdx uint, val V, ok bool) { // backtracking the CBT, make it as fast as possible for baseIdx = idx; baseIdx > 0; baseIdx >>= 1 { - // practically it's getValueOK, but getValueOK is not inlined - if n.prefixesBitset.Test(baseIdx) { - return baseIdx, n.prefixes[n.prefixRank(baseIdx)], true + // practically it's get, but get is not inlined + if n.prefixes.BitSet.Test(baseIdx) { + return baseIdx, n.prefixes.MustGet(baseIdx), true } } @@ -182,7 +78,7 @@ func (n *node[V]) lpm(idx uint) (baseIdx uint, val V, ok bool) { func (n *node[V]) lpmTest(idx uint) bool { // backtracking the CBT for idx := idx; idx > 0; idx >>= 1 { - if n.prefixesBitset.Test(idx) { + if n.prefixes.BitSet.Test(idx) { return true } } @@ -190,92 +86,21 @@ func (n *node[V]) lpmTest(idx uint) bool { return false } -// getValueOK for idx.. -func (n *node[V]) getValueOK(idx uint) (val V, ok bool) { - if n.prefixesBitset.Test(idx) { - return n.prefixes[n.prefixRank(idx)], true - } - - return -} - -// mustGetValue for idx, use it only after a successful bitset test. -// n.prefixesBitset.Test(idx) must be true -func (n *node[V]) mustGetValue(idx uint) V { - return n.prefixes[n.prefixRank(idx)] -} - -// allStrideIndexes returns all baseIndexes set in this stride node in ascending order. -func (n *node[V]) allStrideIndexes(buffer []uint) []uint { - _, buffer = n.prefixesBitset.NextSetMany(0, buffer) - - return buffer -} - -// ################## children ################################ - -// childRank, Rank() is the key of the popcount compression algorithm, -// mapping between bitset index and slice index. -func (n *node[V]) childRank(octet byte) int { - // adjust offset by one to slice index - return int(n.childrenBitset.Rank(uint(octet))) - 1 -} - -// insertChild, insert the child -func (n *node[V]) insertChild(octet byte, child *node[V]) { - // child exists, overwrite it - if n.childrenBitset.Test(uint(octet)) { - n.children[n.childRank(octet)] = child - - return - } - - // new insert into bitset and slice - n.childrenBitset.Set(uint(octet)) - n.children = slices.Insert(n.children, n.childRank(octet), child) -} - -// deleteChild, delete the child at octet. It is valid to delete a non-existent child. -func (n *node[V]) deleteChild(octet byte) { - if !n.childrenBitset.Test(uint(octet)) { - return - } - - rnk := n.childRank(octet) - - // delete from slice - n.children = slices.Delete(n.children, rnk, rnk+1) - - // delete from bitset, followed by Compact to reduce memory consumption - n.childrenBitset.Clear(uint(octet)) - n.childrenBitset.Compact() -} - -// getChild returns the child pointer for octet, or nil if none. -func (n *node[V]) getChild(octet byte) *node[V] { - if !n.childrenBitset.Test(uint(octet)) { - return nil - } - - return n.children[n.childRank(octet)] -} - -// allChildAddrs fills the buffer with the octets of all child nodes in ascending order, -// panics if the buffer isn't big enough. -func (n *node[V]) allChildAddrs(buffer []uint) []uint { - _, buffer = n.childrenBitset.NextSetMany(0, buffer) - - return buffer -} - -// #################### nodes ############################################# +// ### more complex functions than routing table lookups ### // eachLookupPrefix does an all prefix match in the 8-bit (stride) routing table // at this depth and calls yield() for any matching CIDR. -func (n *node[V]) eachLookupPrefix(path [16]byte, depth int, is4 bool, octet byte, bits int, yield func(netip.Prefix, V) bool) bool { +func (n *node[V]) eachLookupPrefix( + path [16]byte, + depth int, + is4 bool, + octet byte, + bits int, + yield func(netip.Prefix, V) bool, +) bool { // backtracking the CBT for idx := pfxToIdx(octet, bits); idx > 0; idx >>= 1 { - if val, ok := n.getValueOK(idx); ok { + if val, ok := n.prefixes.Get(idx); ok { cidr, _ := cidrFromPath(path, depth, is4, idx) if !yield(cidr, val) { @@ -289,7 +114,14 @@ func (n *node[V]) eachLookupPrefix(path [16]byte, depth int, is4 bool, octet byt } // eachSubnet calls yield() for any covered CIDR by parent prefix in natural CIDR sort order. -func (n *node[V]) eachSubnet(path [16]byte, depth int, is4 bool, octet byte, pfxLen int, yield func(netip.Prefix, V) bool) bool { +func (n *node[V]) eachSubnet( + path [16]byte, + depth int, + is4 bool, + octet byte, + pfxLen int, + yield func(netip.Prefix, V) bool, +) bool { // ############################################################### // 1. collect all indices in n covered by prefix // ############################################################### @@ -304,7 +136,7 @@ func (n *node[V]) eachSubnet(path [16]byte, depth int, is4 bool, octet byte, pfx var ok bool for { - if idx, ok = n.prefixesBitset.NextSet(idx); !ok { + if idx, ok = n.prefixes.BitSet.NextSet(idx); !ok { break } @@ -334,7 +166,7 @@ func (n *node[V]) eachSubnet(path [16]byte, depth int, is4 bool, octet byte, pfx var addr uint for { - if addr, ok = n.childrenBitset.NextSet(addr); !ok { + if addr, ok = n.children.BitSet.NextSet(addr); !ok { break } @@ -371,7 +203,7 @@ func (n *node[V]) eachSubnet(path [16]byte, depth int, is4 bool, octet byte, pfx // yield child octet = byte(addr) - c := n.getChild(octet) + c, _ := n.children.Get(uint(octet)) // add (set) this octet to path path[depth] = octet @@ -387,7 +219,7 @@ func (n *node[V]) eachSubnet(path [16]byte, depth int, is4 bool, octet byte, pfx // yield the prefix for this idx cidr, _ := cidrFromPath(path, depth, is4, idx) - if !yield(cidr, n.mustGetValue(idx)) { + if !yield(cidr, n.prefixes.MustGet(idx)) { // early exit return false } @@ -401,7 +233,7 @@ func (n *node[V]) eachSubnet(path [16]byte, depth int, is4 bool, octet byte, pfx addr = allCoveredAddrs[j] octet = byte(addr) - c := n.getChild(octet) + c, _ := n.children.Get(uint(octet)) // add (set) this octet to path path[depth] = octet @@ -424,9 +256,9 @@ func (n *node[V]) unionRec(o *node[V]) (duplicates int) { idxBacking := make([]uint, maxNodePrefixes) // for all prefixes in other node do ... - for i, oIdx := range o.allStrideIndexes(idxBacking) { + for i, oIdx := range o.prefixes.AllSetBits(idxBacking) { // insert/overwrite prefix/value from oNode to nNode - ok := n.insertPrefix(oIdx, o.prefixes[i]) + ok := n.prefixes.InsertAt(oIdx, o.prefixes.Items[i]) // this prefix is duplicate in n and o if !ok { @@ -438,19 +270,17 @@ func (n *node[V]) unionRec(o *node[V]) (duplicates int) { addrBacking := make([]uint, maxNodeChildren) // for all children in other node do ... - for i, oOctet := range o.allChildAddrs(addrBacking) { + for i, oOctet := range o.children.AllSetBits(addrBacking) { octet := byte(oOctet) // we know the slice index, faster as o.getChild(octet) - oc := o.children[i] + oc := o.children.Items[i] // get n child with same octet, // we don't know the slice index in n.children - nc := n.getChild(octet) - - if nc == nil { + if nc, ok := n.children.Get(uint(octet)); !ok { // insert cloned child from oNode into nNode - n.insertChild(octet, oc.cloneRec()) + n.children.InsertAt(uint(octet), oc.cloneRec()) } else { // both nodes have child with octet, call union rec-descent duplicates += nc.unionRec(oc) @@ -467,24 +297,24 @@ func (n *node[V]) cloneRec() *node[V] { return c } - c.prefixesBitset = n.prefixesBitset.Clone() // deep - c.prefixes = slices.Clone(n.prefixes) // values, shallow copy + c.prefixes.BitSet = n.prefixes.BitSet.Clone() // deep + c.prefixes.Items = slices.Clone(n.prefixes.Items) // values, shallow copy // deep copy if V implements Cloner[V] - for i, v := range c.prefixes { + for i, v := range c.prefixes.Items { if v, ok := any(v).(Cloner[V]); ok { - c.prefixes[i] = v.Clone() + c.prefixes.Items[i] = v.Clone() } else { break } } - c.childrenBitset = n.childrenBitset.Clone() // deep - c.children = slices.Clone(n.children) // children, shallow copy + c.children.BitSet = n.children.BitSet.Clone() // deep + c.children.Items = slices.Clone(n.children.Items) // children, shallow copy // deep copy of children - for i, child := range c.children { - c.children[i] = child.cloneRec() + for i, child := range c.children.Items { + c.children.Items[i] = child.cloneRec() } return c @@ -496,14 +326,19 @@ func (n *node[V]) cloneRec() *node[V] { // false value is propagated. // // The iteration order is not defined, just the simplest and fastest recursive implementation. -func (n *node[V]) allRec(path [16]byte, depth int, is4 bool, yield func(netip.Prefix, V) bool) bool { +func (n *node[V]) allRec( + path [16]byte, + depth int, + is4 bool, + yield func(netip.Prefix, V) bool, +) bool { idxBacking := make([]uint, maxNodePrefixes) // for all prefixes in this node do ... - for _, idx := range n.allStrideIndexes(idxBacking) { + for _, idx := range n.prefixes.AllSetBits(idxBacking) { cidr, _ := cidrFromPath(path, depth, is4, idx) // make the callback for this prefix - if !yield(cidr, n.mustGetValue(idx)) { + if !yield(cidr, n.prefixes.MustGet(idx)) { // early exit return false } @@ -511,8 +346,8 @@ func (n *node[V]) allRec(path [16]byte, depth int, is4 bool, yield func(netip.Pr addrBacking := make([]uint, maxNodeChildren) // for all children in this node do ... - for i, addr := range n.allChildAddrs(addrBacking) { - child := n.children[i] + for i, addr := range n.children.AllSetBits(addrBacking) { + child := n.children.Items[i] path[depth] = byte(addr) if !child.allRec(path, depth+1, is4, yield) { @@ -530,16 +365,21 @@ func (n *node[V]) allRec(path [16]byte, depth int, is4 bool, yield func(netip.Pr // // If the yield function returns false the recursion ends prematurely and the // false value is propagated. -func (n *node[V]) allRecSorted(path [16]byte, depth int, is4 bool, yield func(netip.Prefix, V) bool) bool { +func (n *node[V]) allRecSorted( + path [16]byte, + depth int, + is4 bool, + yield func(netip.Prefix, V) bool, +) bool { // make backing arrays, no heap allocs addrBacking := make([]uint, maxNodeChildren) idxBacking := make([]uint, maxNodePrefixes) // get slice of all child octets, sorted by addr - childAddrs := n.allChildAddrs(addrBacking) + childAddrs := n.children.AllSetBits(addrBacking) // get slice of all indexes, sorted by idx - allIndices := n.allStrideIndexes(idxBacking) + allIndices := n.prefixes.AllSetBits(idxBacking) // sort indices in CIDR sort order slices.SortFunc(allIndices, cmpIndexRank) @@ -559,7 +399,7 @@ func (n *node[V]) allRecSorted(path [16]byte, depth int, is4 bool, yield func(ne } // yield the child for this addr - c := n.children[j] + c := n.children.Items[j] // add (set) this octet to path path[depth] = byte(addr) @@ -575,7 +415,7 @@ func (n *node[V]) allRecSorted(path [16]byte, depth int, is4 bool, yield func(ne // yield the prefix for this idx cidr, _ := cidrFromPath(path, depth, is4, idx) - if !yield(cidr, n.mustGetValue(idx)) { + if !yield(cidr, n.prefixes.MustGet(idx)) { // early exit return false } @@ -584,7 +424,7 @@ func (n *node[V]) allRecSorted(path [16]byte, depth int, is4 bool, yield func(ne // yield the rest of childs, if any for j := childCursor; j < len(childAddrs); j++ { addr := childAddrs[j] - c := n.children[j] + c := n.children.Items[j] path[depth] = byte(addr) if !c.allRecSorted(path, depth+1, is4, yield) { diff --git a/node_test.go b/node_test.go index 905fac0..120005a 100644 --- a/node_test.go +++ b/node_test.go @@ -22,7 +22,7 @@ func (n *node[V]) numNodesRec() int { } size := 1 // this node - for _, c := range n.children { + for _, c := range n.children.Items { size += c.numNodesRec() } return size @@ -65,7 +65,7 @@ func TestPrefixInsert(t *testing.T) { fast := newNode[int]() for _, pfx := range pfxs { - fast.insertPrefix(pfxToIdx(pfx.octet, pfx.bits), pfx.val) + fast.prefixes.InsertAt(pfxToIdx(pfx.octet, pfx.bits), pfx.val) } for i := range 256 { @@ -86,13 +86,13 @@ func TestPrefixDelete(t *testing.T) { fast := newNode[int]() for _, pfx := range pfxs { - fast.insertPrefix(pfxToIdx(pfx.octet, pfx.bits), pfx.val) + fast.prefixes.InsertAt(pfxToIdx(pfx.octet, pfx.bits), pfx.val) } toDelete := pfxs[:50] for _, pfx := range toDelete { gold.delete(pfx.octet, pfx.bits) - fast.deletePrefix(pfx.octet, pfx.bits) + fast.prefixes.DeleteAt(pfxToIdx(pfx.octet, pfx.bits)) } // Sanity check that slow table seems to have done the right thing. @@ -118,7 +118,7 @@ func TestOverlapsPrefix(t *testing.T) { fast := newNode[int]() for _, pfx := range pfxs { - fast.insertPrefix(pfxToIdx(pfx.octet, pfx.bits), pfx.val) + fast.prefixes.InsertAt(pfxToIdx(pfx.octet, pfx.bits), pfx.val) } for _, tt := range allStridePfxs() { @@ -147,7 +147,7 @@ func TestOverlapsNode(t *testing.T) { fast := newNode[int]() for _, pfx := range pfxs { - fast.insertPrefix(pfxToIdx(pfx.octet, pfx.bits), pfx.val) + fast.prefixes.InsertAt(pfxToIdx(pfx.octet, pfx.bits), pfx.val) } inter := all[numEntries : 2*numEntries] @@ -155,7 +155,7 @@ func TestOverlapsNode(t *testing.T) { fastInter := newNode[int]() for _, pfx := range inter { - fastInter.insertPrefix(pfxToIdx(pfx.octet, pfx.bits), pfx.val) + fastInter.prefixes.InsertAt(pfxToIdx(pfx.octet, pfx.bits), pfx.val) } gotGold := gold.strideOverlaps(&goldInter) @@ -185,7 +185,7 @@ func BenchmarkNodePrefixInsert(b *testing.B) { if i >= nroutes { break } - node.insertPrefix(pfxToIdx(route.octet, route.bits), 0) + node.prefixes.InsertAt(pfxToIdx(route.octet, route.bits), 0) } b.Run(fmt.Sprintf("Into %d", nroutes), func(b *testing.B) { @@ -193,7 +193,7 @@ func BenchmarkNodePrefixInsert(b *testing.B) { b.ResetTimer() for range b.N { - node.insertPrefix(pfxToIdx(route.octet, route.bits), 0) + node.prefixes.InsertAt(pfxToIdx(route.octet, route.bits), 0) } }) } @@ -209,7 +209,7 @@ func BenchmarkNodePrefixUpdate(b *testing.B) { if i >= nroutes { break } - node.insertPrefix(pfxToIdx(route.octet, route.bits), 0) + node.prefixes.InsertAt(pfxToIdx(route.octet, route.bits), 0) } b.Run(fmt.Sprintf("In %d", nroutes), func(b *testing.B) { @@ -217,7 +217,7 @@ func BenchmarkNodePrefixUpdate(b *testing.B) { b.ResetTimer() for range b.N { - node.updatePrefix(route.octet, route.bits, func(int, bool) int { return 1 }) + node.prefixes.UpdateAt(pfxToIdx(route.octet, route.bits), func(int, bool) int { return 1 }) } }) } @@ -233,7 +233,7 @@ func BenchmarkNodePrefixDelete(b *testing.B) { if i >= nroutes { break } - node.insertPrefix(pfxToIdx(route.octet, route.bits), 0) + node.prefixes.InsertAt(pfxToIdx(route.octet, route.bits), 0) } b.Run(fmt.Sprintf("From %d", nroutes), func(b *testing.B) { @@ -241,7 +241,7 @@ func BenchmarkNodePrefixDelete(b *testing.B) { b.ResetTimer() for range b.N { - node.deletePrefix(route.octet, route.bits) + node.prefixes.DeleteAt(pfxToIdx(route.octet, route.bits)) } }) } @@ -259,7 +259,7 @@ func BenchmarkNodePrefixLPM(b *testing.B) { if i >= nroutes { break } - node.insertPrefix(pfxToIdx(route.octet, route.bits), 0) + node.prefixes.InsertAt(pfxToIdx(route.octet, route.bits), 0) } b.Run(fmt.Sprintf("IN %d", nroutes), func(b *testing.B) { @@ -273,31 +273,6 @@ func BenchmarkNodePrefixLPM(b *testing.B) { } } -func BenchmarkNodePrefixRank(b *testing.B) { - routes := shuffleStridePfxs(allStridePfxs()) - - for _, nroutes := range prefixCount { - node := newNode[int]() - - for i, route := range routes { - if i >= nroutes { - break - } - node.insertPrefix(pfxToIdx(route.octet, route.bits), 0) - } - - b.Run(fmt.Sprintf("IN %d", nroutes), func(b *testing.B) { - route := routes[rand.Intn(len(routes))] - idx := pfxToIdx(route.octet, route.bits) - - b.ResetTimer() - for range b.N { - writeSink = node.prefixRank(idx) - } - }) - } -} - func BenchmarkNodePrefixNextSetMany(b *testing.B) { routes := shuffleStridePfxs(allStridePfxs()) @@ -308,14 +283,14 @@ func BenchmarkNodePrefixNextSetMany(b *testing.B) { if i >= nroutes { break } - node.insertPrefix(pfxToIdx(route.octet, route.bits), 0) + node.prefixes.InsertAt(pfxToIdx(route.octet, route.bits), 0) } b.Run(fmt.Sprintf("IN %d", nroutes), func(b *testing.B) { idxBackingArray := [maxNodePrefixes]uint{} b.ResetTimer() for range b.N { - node.allStrideIndexes(idxBackingArray[:]) + node.prefixes.AllSetBits(idxBackingArray[:]) } }) } @@ -333,20 +308,20 @@ func BenchmarkNodePrefixIntersectionCardinality(b *testing.B) { if i >= nroutes { break } - node.insertPrefix(pfxToIdx(route.octet, route.bits), 0) + node.prefixes.InsertAt(pfxToIdx(route.octet, route.bits), 0) } for i, route := range routes2 { if i >= nroutes { break } - other.insertPrefix(pfxToIdx(route.octet, route.bits), 0) + other.prefixes.InsertAt(pfxToIdx(route.octet, route.bits), 0) } b.Run(fmt.Sprintf("With %d", nroutes), func(b *testing.B) { b.ResetTimer() for range b.N { - node.prefixesBitset.IntersectionCardinality(other.prefixesBitset) + node.prefixes.BitSet.IntersectionCardinality(other.prefixes.BitSet) } }) } @@ -358,7 +333,7 @@ func BenchmarkNodeChildInsert(b *testing.B) { for range nchilds { octet := rand.Intn(maxNodeChildren) - node.insertChild(byte(octet), nil) + node.children.InsertAt(uint(octet), nil) } b.Run(fmt.Sprintf("Into %d", nchilds), func(b *testing.B) { @@ -366,7 +341,7 @@ func BenchmarkNodeChildInsert(b *testing.B) { b.ResetTimer() for range b.N { - node.insertChild(byte(octet), nil) + node.children.InsertAt(uint(octet), nil) } }) } @@ -378,7 +353,7 @@ func BenchmarkNodeChildDelete(b *testing.B) { for range nchilds { octet := rand.Intn(maxNodeChildren) - node.insertChild(byte(octet), nil) + node.children.InsertAt(uint(octet), nil) } b.Run(fmt.Sprintf("From %d", nchilds), func(b *testing.B) { @@ -386,28 +361,7 @@ func BenchmarkNodeChildDelete(b *testing.B) { b.ResetTimer() for range b.N { - node.deleteChild(byte(octet)) - } - }) - } -} - -func BenchmarkNodeChildRank(b *testing.B) { - for _, nchilds := range childCount { - node := newNode[int]() - - for range nchilds { - octet := byte(rand.Intn(maxNodeChildren)) - node.insertChild(octet, nil) - } - - b.Run(fmt.Sprintf("In %d", nchilds), func(b *testing.B) { - octet := byte(rand.Intn(maxNodeChildren)) - idx := hostIndex(octet) - - b.ResetTimer() - for range b.N { - node.childRank(byte(idx)) + node.children.DeleteAt(uint(octet)) } }) } @@ -419,14 +373,14 @@ func BenchmarkNodeChildNextSetMany(b *testing.B) { for range nchilds { octet := byte(rand.Intn(maxNodeChildren)) - node.insertChild(octet, nil) + node.children.InsertAt(uint(octet), nil) } b.Run(fmt.Sprintf("In %d", nchilds), func(b *testing.B) { addrBacking := make([]uint, maxNodeChildren) b.ResetTimer() for range b.N { - node.allChildAddrs(addrBacking) + node.children.AllSetBits(addrBacking) } }) } @@ -439,16 +393,16 @@ func BenchmarkNodeChildIntersectionCardinality(b *testing.B) { for range nchilds { octet := byte(rand.Intn(maxNodeChildren)) - node.insertChild(octet, nil) + node.children.InsertAt(uint(octet), nil) octet = byte(rand.Intn(maxNodeChildren)) - other.insertChild(octet, nil) + other.children.InsertAt(uint(octet), nil) } b.Run(fmt.Sprintf("With %d", nchilds), func(b *testing.B) { b.ResetTimer() for range b.N { - node.childrenBitset.IntersectionCardinality(other.childrenBitset) + node.children.BitSet.IntersectionCardinality(other.children.BitSet) } }) } diff --git a/overlaps.go b/overlaps.go index ecb0b51..a50ed4b 100644 --- a/overlaps.go +++ b/overlaps.go @@ -4,34 +4,34 @@ import "github.com/bits-and-blooms/bitset" // overlapsRec returns true if any IP in the nodes n or o overlaps. func (n *node[V]) overlapsRec(o *node[V]) bool { - nPfxLen := len(n.prefixes) - oPfxLen := len(o.prefixes) + nPfxCount := n.prefixes.Count() + oPfxCount := o.prefixes.Count() - nChildLen := len(n.children) - oChildLen := len(o.children) + nChildCount := n.children.Count() + oChildCount := o.children.Count() // ############################## // 1. Test if any routes overlaps // ############################## // special case, overlapsPrefix is faster - if nPfxLen == 1 && nChildLen == 0 { + if nPfxCount == 1 && nChildCount == 0 { // get the single prefix from n - idx, _ := n.prefixesBitset.NextSet(0) + idx, _ := n.prefixes.BitSet.NextSet(0) return o.overlapsPrefix(idxToPfx(idx)) } // special case, overlapsPrefix is faster - if oPfxLen == 1 && oChildLen == 0 { + if oPfxCount == 1 && oChildCount == 0 { // get the single prefix from o - idx, _ := o.prefixesBitset.NextSet(0) + idx, _ := o.prefixes.BitSet.NextSet(0) return n.overlapsPrefix(idxToPfx(idx)) } // full cross check - if nPfxLen > 0 && oPfxLen > 0 { + if nPfxCount > 0 && oPfxCount > 0 { if n.overlapsRoutes(o) { return true } @@ -41,14 +41,14 @@ func (n *node[V]) overlapsRec(o *node[V]) bool { // 2. Test if routes overlaps any child // #################################### - if nPfxLen > 0 && oChildLen > 0 { + if nPfxCount > 0 && oChildCount > 0 { if n.overlapsChildsIn(o) { return true } } // symmetric reverse - if oPfxLen > 0 && nChildLen > 0 { + if oPfxCount > 0 && nChildCount > 0 { if o.overlapsChildsIn(n) { return true } @@ -59,20 +59,20 @@ func (n *node[V]) overlapsRec(o *node[V]) bool { // ################################################################ // stop condition, n or o have no childs - if nChildLen == 0 || oChildLen == 0 { + if nChildCount == 0 || oChildCount == 0 { return false } - if oChildLen == 1 { + if oChildCount == 1 { return n.overlapsOneChildIn(o) } - if nChildLen == 1 { + if nChildCount == 1 { return o.overlapsOneChildIn(n) } // stop condition, no child with identical octet in n and o - if n.childrenBitset.IntersectionCardinality(o.childrenBitset) == 0 { + if n.children.BitSet.IntersectionCardinality(o.children.BitSet) == 0 { return false } @@ -82,17 +82,17 @@ func (n *node[V]) overlapsRec(o *node[V]) bool { // overlapsRoutes, test if n overlaps o prefixes and vice versa func (n *node[V]) overlapsRoutes(o *node[V]) bool { // one node has just one prefix, use bitset algo - if len(n.prefixes) == 1 { + if n.prefixes.Count() == 1 { return o.overlapsOneRouteIn(n) } // one node has just one prefix, use bitset algo - if len(o.prefixes) == 1 { + if o.prefixes.Count() == 1 { return n.overlapsOneRouteIn(o) } // some prefixes are identical, trivial overlap - if n.prefixesBitset.IntersectionCardinality(o.prefixesBitset) > 0 { + if n.prefixes.BitSet.IntersectionCardinality(o.prefixes.BitSet) > 0 { return true } @@ -106,7 +106,7 @@ func (n *node[V]) overlapsRoutes(o *node[V]) bool { for nOK || oOK { if nOK { // does any route in o overlap this prefix from n - if nIdx, nOK = n.prefixesBitset.NextSet(nIdx); nOK { + if nIdx, nOK = n.prefixes.BitSet.NextSet(nIdx); nOK { if o.lpmTest(nIdx) { return true } @@ -117,7 +117,7 @@ func (n *node[V]) overlapsRoutes(o *node[V]) bool { if oOK { // does any route in n overlap this prefix from o - if oIdx, oOK = o.prefixesBitset.NextSet(oIdx); oOK { + if oIdx, oOK = o.prefixes.BitSet.NextSet(oIdx); oOK { if n.lpmTest(oIdx) { return true } @@ -132,13 +132,13 @@ func (n *node[V]) overlapsRoutes(o *node[V]) bool { // overlapsChildsIn, test if prefixes in n overlaps child octets in o. func (n *node[V]) overlapsChildsIn(o *node[V]) bool { - pfxLen := len(n.prefixes) - childLen := len(o.children) + pfxCount := n.prefixes.Count() + childCount := o.children.Count() // heuristic, compare benchmarks // when will re range over the children and when will we do bitset calc? magicNumber := 15 - doRange := childLen < magicNumber || pfxLen > magicNumber + doRange := childCount < magicNumber || pfxCount > magicNumber // do range over, not so many childs and maybe to many prefixes if doRange { @@ -147,7 +147,7 @@ func (n *node[V]) overlapsChildsIn(o *node[V]) bool { ok := true for ok { // does any route in o overlap this child from n - if oAddr, ok = o.childrenBitset.NextSet(oAddr); ok { + if oAddr, ok = o.children.BitSet.NextSet(oAddr); ok { if n.lpmTest(hostIndex(byte(oAddr))) { return true } @@ -172,14 +172,14 @@ func (n *node[V]) overlapsChildsIn(o *node[V]) bool { prefixRoutes := bitset.From(prefixBacking) idxBacking := make([]uint, maxNodePrefixes) - for _, idx := range n.allStrideIndexes(idxBacking) { + for _, idx := range n.prefixes.AllSetBits(idxBacking) { a8 := allotLookupTbl[idx] prefixRoutes.InPlaceUnion(bitset.From(a8[:])) } // shift children bitset by firstHostIndex c8 := make([]uint64, 8) - copy(c8[4:], o.childrenBitset.Bytes()) // 4*64= 256 + copy(c8[4:], o.children.BitSet.Bytes()) // 4*64= 256 hostRoutes := bitset.From(c8) return prefixRoutes.IntersectionCardinality(hostRoutes) > 0 @@ -191,11 +191,11 @@ func (n *node[V]) overlapsSameChildsRec(o *node[V]) bool { // gimmicks, clone a bitset without heap allocation // 4*64=256, maxNodeChildren a4 := make([]uint64, 4) - copy(a4, n.childrenBitset.Bytes()) + copy(a4, n.children.BitSet.Bytes()) nChildrenBitsetCloned := bitset.From(a4) // intersect in place the child bitsets from n and o - nChildrenBitsetCloned.InPlaceIntersection(o.childrenBitset) + nChildrenBitsetCloned.InPlaceIntersection(o.children.BitSet) // gimmick, don't allocate addrBuf := [maxNodeChildren]uint{} @@ -203,8 +203,8 @@ func (n *node[V]) overlapsSameChildsRec(o *node[V]) bool { // range over all child addrs, common in n and o for _, addr := range allCommonChilds { - oChild := o.getChild(byte(addr)) - nChild := n.getChild(byte(addr)) + oChild, _ := o.children.Get(addr) + nChild, _ := n.children.Get(addr) // rec-descent with same child if nChild.overlapsRec(oChild) { @@ -217,10 +217,10 @@ func (n *node[V]) overlapsSameChildsRec(o *node[V]) bool { func (n *node[V]) overlapsOneChildIn(o *node[V]) bool { // get the single addr and child - addr, _ := o.childrenBitset.NextSet(0) - oChild := o.children[0] + addr, _ := o.children.BitSet.NextSet(0) + oChild := o.children.Items[0] - if nChild := n.getChild(byte(addr)); nChild != nil { + if nChild, ok := n.children.Get(addr); ok { return nChild.overlapsRec(oChild) } @@ -229,7 +229,7 @@ func (n *node[V]) overlapsOneChildIn(o *node[V]) bool { func (n *node[V]) overlapsOneRouteIn(o *node[V]) bool { // get the single prefix from o - idx, _ := o.prefixesBitset.NextSet(0) + idx, _ := o.prefixes.BitSet.NextSet(0) // 1. Test if any route in this node overlaps prefix? if n.lpmTest(idx) { @@ -244,7 +244,7 @@ func (n *node[V]) overlapsOneRouteIn(o *node[V]) bool { allotedPrefixRoutes := bitset.From(pfxBuf[:]) // use bitset intersection instead of range loops - return allotedPrefixRoutes.IntersectionCardinality(n.prefixesBitset) > 0 + return allotedPrefixRoutes.IntersectionCardinality(n.prefixes.BitSet) > 0 } // overlapsPrefix returns true if node overlaps with prefix. @@ -263,7 +263,7 @@ func (n *node[V]) overlapsPrefix(octet byte, pfxLen int) bool { allotedPrefixRoutes := bitset.From(allotmentForIdx[:]) // use bitset intersection instead of range loops - if allotedPrefixRoutes.IntersectionCardinality(n.prefixesBitset) != 0 { + if allotedPrefixRoutes.IntersectionCardinality(n.prefixes.BitSet) != 0 { return true } @@ -272,7 +272,7 @@ func (n *node[V]) overlapsPrefix(octet byte, pfxLen int) bool { // shift children bitset by firstHostIndex c8 := make([]uint64, 8) - copy(c8[4:], n.childrenBitset.Bytes()) // 4*64= 256 + copy(c8[4:], n.children.BitSet.Bytes()) // 4*64= 256 hostRoutes := bitset.From(c8) // use bitsets intersection instead of range loops diff --git a/stringify.go b/stringify.go index 4bbc5ed..cc5b40b 100644 --- a/stringify.go +++ b/stringify.go @@ -173,7 +173,7 @@ func (n *node[V]) getKidsRec(parentIdx uint, path [16]byte, depth int, is4 bool) // make backing array, no heap allocs idxBacking := make([]uint, maxNodePrefixes) - for _, idx := range n.allStrideIndexes(idxBacking) { + for _, idx := range n.prefixes.AllSetBits(idxBacking) { // parent or self, handled alreday in an upper stack frame. if idx <= parentIdx { continue @@ -193,7 +193,7 @@ func (n *node[V]) getKidsRec(parentIdx uint, path [16]byte, depth int, is4 bool) depth: depth, idx: idx, cidr: cidr, - val: n.mustGetValue(idx), + val: n.prefixes.MustGet(idx), } directKids = append(directKids, kid) @@ -202,13 +202,13 @@ func (n *node[V]) getKidsRec(parentIdx uint, path [16]byte, depth int, is4 bool) // the node may have childs, the rec-descent monster starts addrBacking := make([]uint, maxNodeChildren) - for i, addr := range n.allChildAddrs(addrBacking) { + for i, addr := range n.children.AllSetBits(addrBacking) { octet := byte(addr) // do a longest-prefix-match lpmIdx, _, _ := n.lpm(hostIndex(octet)) if lpmIdx == parentIdx { - c := n.children[i] + c := n.children.Items[i] path[depth] = octet // traverse, rec-descent call with next child node diff --git a/table.go b/table.go index 34bfd54..b5dc02e 100644 --- a/table.go +++ b/table.go @@ -128,11 +128,11 @@ func (t *Table[V]) Insert(pfx netip.Prefix, val V) { // find the proper trie node to insert prefix for _, octet := range octets[:lastOctetIdx] { // descend down to next trie level - c := n.getChild(octet) - if c == nil { + c, ok := n.children.Get(uint(octet)) + if !ok { // create and insert missing intermediate child c = newNode[V]() - n.insertChild(octet, c) + n.children.InsertAt(uint(octet), c) } // proceed with next level @@ -140,7 +140,7 @@ func (t *Table[V]) Insert(pfx netip.Prefix, val V) { } // insert prefix/val into node - if ok := n.insertPrefix(pfxToIdx(lastOctet, lastOctetBits), val); ok { + if ok := n.prefixes.InsertAt(pfxToIdx(lastOctet, lastOctetBits), val); ok { t.sizeUpdate(is4, 1) } } @@ -184,11 +184,11 @@ func (t *Table[V]) Update(pfx netip.Prefix, cb func(val V, ok bool) V) (newVal V // find the proper trie node to update prefix for _, octet := range octets[:lastOctetIdx] { // descend down to next trie level - c := n.getChild(octet) - if c == nil { + c, ok := n.children.Get(uint(octet)) + if !ok { // create and insert missing intermediate child c = newNode[V]() - n.insertChild(octet, c) + n.children.InsertAt(uint(octet), c) } // proceed with next level @@ -198,7 +198,7 @@ func (t *Table[V]) Update(pfx netip.Prefix, cb func(val V, ok bool) V) (newVal V // update/insert prefix into node var wasPresent bool - newVal, wasPresent = n.updatePrefix(lastOctet, lastOctetBits, cb) + newVal, wasPresent = n.prefixes.UpdateAt(pfxToIdx(lastOctet, lastOctetBits), cb) if !wasPresent { t.sizeUpdate(is4, 1) } @@ -210,7 +210,7 @@ func (t *Table[V]) Update(pfx netip.Prefix, cb func(val V, ok bool) V) (newVal V // prefix is not set in the routing table. func (t *Table[V]) Get(pfx netip.Prefix) (val V, ok bool) { if !pfx.IsValid() || !t.isInit() { - return + return val, ok } // values derived from pfx @@ -238,16 +238,15 @@ func (t *Table[V]) Get(pfx netip.Prefix) (val V, ok bool) { // find the proper trie node for _, octet := range octets[:lastOctetIdx] { - c := n.getChild(octet) - if c == nil { - // not found - return + c, ok := n.children.Get(uint(octet)) + if !ok { + return val, ok } n = c } - return n.getValueOK(pfxToIdx(lastOctet, lastOctetBits)) + return n.prefixes.Get(pfxToIdx(lastOctet, lastOctetBits)) } // Delete removes pfx from the tree, pfx does not have to be present. @@ -263,7 +262,7 @@ func (t *Table[V]) GetAndDelete(pfx netip.Prefix) (val V, ok bool) { func (t *Table[V]) getAndDelete(pfx netip.Prefix) (val V, ok bool) { if !pfx.IsValid() || !t.isInit() { - return + return val, ok } // values derived from pfx @@ -306,17 +305,17 @@ func (t *Table[V]) getAndDelete(pfx netip.Prefix) (val V, ok bool) { } // descend down to next level - c := n.getChild(octets[i]) - if c == nil { - return + c, ok := n.children.Get(uint(octets[i])) + if !ok { + return val, ok } n = c } // try to delete prefix in trie node - if val, ok = n.deletePrefix(lastOctet, lastOctetBits); !ok { - return + if val, ok = n.prefixes.DeleteAt(pfxToIdx(lastOctet, lastOctetBits)); !ok { + return val, ok } t.sizeUpdate(is4, -1) @@ -326,7 +325,7 @@ func (t *Table[V]) getAndDelete(pfx netip.Prefix) (val V, ok bool) { if n.isEmpty() { // purge empty node from parents children parent := stack[i-1] - parent.deleteChild(octets[i-1]) + parent.children.DeleteAt(uint(octets[i-1])) } // unwind the stack @@ -341,7 +340,7 @@ func (t *Table[V]) getAndDelete(pfx netip.Prefix) (val V, ok bool) { // returns the associated value and true, or false if no route matched. func (t *Table[V]) Lookup(ip netip.Addr) (val V, ok bool) { if !ip.IsValid() || !t.isInit() { - return + return val, ok } is4 := ip.Is4() @@ -369,8 +368,8 @@ func (t *Table[V]) Lookup(ip netip.Addr) (val V, ok bool) { stack[i] = n // go down in tight loop to leaf node - c := n.getChild(octet) - if c == nil { + c, ok := n.children.Get(uint(octet)) + if !ok { break } @@ -384,14 +383,14 @@ func (t *Table[V]) Lookup(ip netip.Addr) (val V, ok bool) { // longest prefix match // micro benchmarking: skip if node has no prefixes - if len(n.prefixes) != 0 { + if n.prefixes.Count() != 0 { if _, val, ok = n.lpm(hostIndex(octet)); ok { - return val, true + return val, ok } } } - return + return val, ok } // LookupPrefix does a route lookup (longest prefix match) for pfx and @@ -471,8 +470,8 @@ func (t *Table[V]) lpmPrefix(pfx netip.Prefix) (depth int, baseIdx uint, val V, 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 } @@ -486,7 +485,7 @@ func (t *Table[V]) lpmPrefix(pfx netip.Prefix) (depth int, baseIdx uint, val V, // longest prefix match // micro benchmarking: skip if node has no prefixes - if len(n.prefixes) != 0 { + if n.prefixes.Count() != 0 { // only the lastOctet may have a different prefix len // all others are just host routes var idx uint @@ -503,7 +502,7 @@ func (t *Table[V]) lpmPrefix(pfx netip.Prefix) (depth int, baseIdx uint, val V, } } - return + return depth, baseIdx, val, ok } // OverlapsPrefix reports whether any IP in pfx is matched by a route in the table or vice versa. @@ -543,8 +542,8 @@ func (t *Table[V]) OverlapsPrefix(pfx netip.Prefix) bool { } // no overlap so far, go down to next c - c := n.getChild(octet) - if c == nil { + c, ok := n.children.Get(uint(octet)) + if !ok { return false } diff --git a/table_iter.go b/table_iter.go index bc51230..e500f56 100644 --- a/table_iter.go +++ b/table_iter.go @@ -55,8 +55,8 @@ func (t *Table[V]) Supernets(pfx netip.Prefix) func(yield func(netip.Prefix, V) 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 } @@ -68,7 +68,7 @@ func (t *Table[V]) Supernets(pfx netip.Prefix) func(yield func(netip.Prefix, V) n = stack[depth] // microbenchmarking - if len(n.prefixes) == 0 { + if n.prefixes.Count() == 0 { continue } @@ -132,8 +132,8 @@ func (t *Table[V]) Subnets(pfx netip.Prefix) func(yield func(netip.Prefix, V) bo return } - c := n.getChild(octet) - if c == nil { + c, ok := n.children.Get(uint(octet)) + if !ok { break }