Skip to content

Commit

Permalink
minor comments and more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
gaissmai committed Jan 3, 2025
1 parent 2c1de7c commit c270959
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 165 deletions.
25 changes: 18 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ build the complete-binary-tree (CBT) of prefixes for each stride.
|:--:|
| *example from artlookup.pdf for a 4bit stride* |

The CBT is implemented as a bitvector, backtracking is just
The CBT is implemented as a bit-vector, backtracking is just
a matter of fast cache friendly bitmask operations.

The Table is implemented with popcount compressed sparse arrays
Expand Down Expand Up @@ -97,19 +97,30 @@ The API has changed in ..., v0.10.1, v0.11.0, v0.12.0, v0.12.6, v0.16.0

Please see the extensive [benchmarks](https://github.com/gaissmai/iprbench) comparing `bart` with other IP routing table implementations.

Just a teaser, LPM lookups against the full Internet routing table with random probes:
Just a teaser, Contains and Lookups against the full Internet routing table with random IP address probes:

```
goos: linux
goarch: amd64
pkg: github.com/gaissmai/bart
cpu: Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz
BenchmarkFullMatchV4/Contains 37375450 29.71 ns/op
BenchmarkFullMatchV6/Contains 41348316 26.85 ns/op
BenchmarkFullMissV4/Contains 38583682 29.66 ns/op
BenchmarkFullMissV6/Contains 83315865 12.64 ns/op
BenchmarkFullMatchV4/Contains 38003740 27.69 ns/op
BenchmarkFullMatchV6/Contains 49389355 23.00 ns/op
BenchmarkFullMissV4/Contains 42902048 26.57 ns/op
BenchmarkFullMissV6/Contains 128219610 9.375 ns/op
PASS
ok github.com/gaissmai/bart 11.248s
ok github.com/gaissmai/bart 12.195s

goos: linux
goarch: amd64
pkg: github.com/gaissmai/bart
cpu: Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz
BenchmarkFullMatchV4/Lookup 37453425 30.55 ns/op
BenchmarkFullMatchV6/Lookup 36819931 30.48 ns/op
BenchmarkFullMissV4/Lookup 37223881 30.70 ns/op
BenchmarkFullMissV6/Lookup 94333762 12.32 ns/op
PASS
ok github.com/gaissmai/bart 11.215s
```

## Compatibility Guarantees
Expand Down
6 changes: 3 additions & 3 deletions internal/sparse/array.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ func (s *Array[T]) Len() int {
return len(s.Items)
}

// Clone returns a copy of the Array.
// The elements are copied using assignment, so this is a shallow clone.
func (s *Array[T]) Clone() *Array[T] {
// Copy returns a shallow copy of the Array.
// The elements are copied using assignment, this is no deep clone.
func (s *Array[T]) Copy() *Array[T] {
if s == nil {
return nil
}
Expand Down
31 changes: 31 additions & 0 deletions internal/sparse/array_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,34 @@ func TestSparseArrayCompact(t *testing.T) {
t.Errorf("cap, expected 3_000, got %d", c)
}
}

func TestSparseArrayCopy(t *testing.T) {
t.Parallel()
a := new(Array[int])

for i := range 10_000 {
a.InsertAt(uint(i), i)
}

// shallow copy
b := a.Copy()

// basic values identity
for i, v := range a.Items {
if b.Items[i] != v {
t.Errorf("Clone, expect value: %v, got: %v", v, b.Items[i])
}
}

// update array a
for i := range 10_000 {
a.UpdateAt(uint(i), func(u int, _ bool) int { return u + 1 })
}

// cloned array must now differ
for i, v := range a.Items {
if b.Items[i] == v {
t.Errorf("update a after Clone, b must now differ: aValue: %v, bValue: %v", b.Items[i], v)
}
}
}
2 changes: 1 addition & 1 deletion jsonify.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (n *node[V]) dumpListRec(parentIdx uint, path [16]byte, depth int, is4 bool
}

directKids := n.getKidsRec(parentIdx, path, depth, is4)
slices.SortFunc(directKids, cmpKidByPrefix2[V])
slices.SortFunc(directKids, cmpKidByPrefix[V])

nodes := make([]DumpListNode[V], 0, len(directKids))
for _, kid := range directKids {
Expand Down
33 changes: 18 additions & 15 deletions node.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ var zeroPath [16]byte
// (popcount-compressed slices) are used instead of fixed-size arrays.
//
// The array slots are also not pre-allocated (alloted) as described
// in the ART algorithm, but backtracking is used for the longest-prefix-match.
// in the ART algorithm, fast backtracking with a bitset vector is used
// to get the longest-prefix-match.
//
// The sparse child array recursively spans the trie with a branching factor of 256
// and also records path-compressed leaves in the free child slots.
// and also records path-compressed leaves in the free node slots.
type node[V any] struct {
// prefixes contains the routes as complete binary tree with payload V
prefixes sparse.Array[V]
Expand All @@ -44,17 +45,24 @@ type node[V any] struct {
children sparse.Array[interface{}]
}

// leaf is prefix and value together as path compressed child
// isEmpty returns true if node has neither prefixes nor children
func (n *node[V]) isEmpty() bool {
return n.prefixes.Len() == 0 && n.children.Len() == 0
}

// leaf is a prefix and value together, it's a path compressed child
type leaf[V any] struct {
prefix netip.Prefix
value V
}

// cloneValue, deep copy if v implements the Cloner interface.
func cloneValue[V any](v V) V {
// cloneOrCopyValue, helper function,
// deep copy if v implements the Cloner interface.
func cloneOrCopyValue[V any](v V) V {
if k, ok := any(v).(Cloner[V]); ok {
return k.Clone()
}
// just copy
return v
}

Expand All @@ -64,15 +72,10 @@ func (l *leaf[V]) cloneLeaf() *leaf[V] {
if l == nil {
return nil
}
return &leaf[V]{l.prefix, cloneValue(l.value)}
}

// isEmpty returns true if node has neither prefixes nor children
func (n *node[V]) isEmpty() bool {
return n.prefixes.Len() == 0 && n.children.Len() == 0
return &leaf[V]{l.prefix, cloneOrCopyValue(l.value)}
}

// nodeAndLeafCount
// nodeAndLeafCount for this node
func (n *node[V]) nodeAndLeafCount() (nodes int, leaves int) {
for i := range n.children.AsSlice(make([]uint, 0, maxNodeChildren)) {
switch n.children.Items[i].(type) {
Expand Down Expand Up @@ -240,15 +243,15 @@ func (n *node[V]) cloneRec() *node[V] {
}

// shallow
c.prefixes = *(n.prefixes.Clone())
c.prefixes = *(n.prefixes.Copy())

// deep copy if V implements Cloner[V]
for i, v := range c.prefixes.Items {
c.prefixes.Items[i] = cloneValue(v)
c.prefixes.Items[i] = cloneOrCopyValue(v)
}

// shallow
c.children = *(n.children.Clone())
c.children = *(n.children.Copy())

// deep copy of nodes and leaves
for i, k := range c.children.Items {
Expand Down
20 changes: 10 additions & 10 deletions stringify.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
// kid, a node has no path information about its predecessors,
// we collect this during the recursive descent.
// The path/depth/idx is needed to get the CIDR back.
type kid2[V any] struct {
type kid[V any] struct {
// for traversing
n *node[V]
is4 bool
Expand Down Expand Up @@ -99,7 +99,7 @@ func (t *Table[V]) fprint(w io.Writer, is4 bool) error {
return err
}

startKid := kid2[V]{
startKid := kid[V]{
n: nil,
idx: 0,
path: zeroPath,
Expand All @@ -114,7 +114,7 @@ func (t *Table[V]) fprint(w io.Writer, is4 bool) error {
}

// fprintRec, the output is a hierarchical CIDR tree starting with this kid.
func (n *node[V]) fprintRec(w io.Writer, parent kid2[V], pad string) error {
func (n *node[V]) fprintRec(w io.Writer, parent kid[V], pad string) error {
// recursion stop condition
if n == nil {
return nil
Expand All @@ -124,7 +124,7 @@ func (n *node[V]) fprintRec(w io.Writer, parent kid2[V], pad string) error {
directKids := n.getKidsRec(parent.idx, parent.path, parent.depth, parent.is4)

// sort them by netip.Prefix, not by baseIndex
slices.SortFunc(directKids, cmpKidByPrefix2[V])
slices.SortFunc(directKids, cmpKidByPrefix[V])

// symbols used in tree
glyphe := "├─ "
Expand Down Expand Up @@ -160,13 +160,13 @@ func (n *node[V]) fprintRec(w io.Writer, parent kid2[V], pad string) error {
//
// See the artlookup.pdf paper in the doc folder,
// the baseIndex function is the key.
func (n *node[V]) getKidsRec(parentIdx uint, path [16]byte, depth int, is4 bool) []kid2[V] {
func (n *node[V]) getKidsRec(parentIdx uint, path [16]byte, depth int, is4 bool) []kid[V] {
// recursion stop condition
if n == nil {
return nil
}

var directKids []kid2[V]
var directKids []kid[V]

allIndices := n.prefixes.AsSlice(make([]uint, 0, maxNodePrefixes))

Expand All @@ -183,7 +183,7 @@ func (n *node[V]) getKidsRec(parentIdx uint, path [16]byte, depth int, is4 bool)
if lpmIdx == parentIdx {
cidr, _ := cidrFromPath(path, depth, is4, idx)

kid := kid2[V]{
kid := kid[V]{
n: n,
is4: is4,
path: path,
Expand Down Expand Up @@ -211,7 +211,7 @@ func (n *node[V]) getKidsRec(parentIdx uint, path [16]byte, depth int, is4 bool)
// traverse, rec-descent call with next child node
directKids = append(directKids, k.getKidsRec(0, path, depth+1, is4)...)
case *leaf[V]:
kid := kid2[V]{
kid := kid[V]{
n: nil, // path compressed item, stop recursion
is4: is4,
cidr: k.prefix,
Expand All @@ -226,8 +226,8 @@ func (n *node[V]) getKidsRec(parentIdx uint, path [16]byte, depth int, is4 bool)
return directKids
}

// cmpKidByPrefix2, all prefixes are already normalized (Masked).
func cmpKidByPrefix2[V any](a, b kid2[V]) int {
// cmpKidByPrefix, all prefixes are already normalized (Masked).
func cmpKidByPrefix[V any](a, b kid[V]) int {
return cmpPrefix(a.cidr, b.cidr)
}

Expand Down
Loading

0 comments on commit c270959

Please sign in to comment.