From 7bcc20744495ebb97cef4e4f5256099dccd9afad Mon Sep 17 00:00:00 2001 From: Karl Gaissmaier Date: Mon, 27 Jan 2025 21:05:02 +0100 Subject: [PATCH 01/10] comment --- lpm_lookuptbl.go | 6 +++--- node.go | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lpm_lookuptbl.go b/lpm_lookuptbl.go index 16b5d14..751a8d5 100644 --- a/lpm_lookuptbl.go +++ b/lpm_lookuptbl.go @@ -2,17 +2,17 @@ package bart import "github.com/gaissmai/bart/internal/bitset" -// lpmLookupTbl is the backtracking sequence as bitstring. +// lpmLookupTbl is the backtracking sequence in the complete binary tree as bitstring. // // for idx := 1; idx > 0; idx >>= 1 { b.Set(idx) } // -// one shot bitset intersection algorithm: +// allows a one shot bitset intersection algorithm: // // func (n *node[V]) lpmTest(idx uint) bool { // return n.prefixes.IntersectsAny(lpmLookupTbl[idx]) // } // -// insted of a sequence of single bitset tests: +// instead of a sequence of single bitset tests: // // func (n *node[V]) lpmTest(idx uint) bool { // for ; idx > 0; idx >>= 1 { diff --git a/node.go b/node.go index 8e0f0db..58d5520 100644 --- a/node.go +++ b/node.go @@ -179,9 +179,12 @@ func (n *node[V]) purgeAndCompress(parentStack []*node[V], childPath []byte, is4 // at this depth and returns (baseIdx, value, true) if a matching // longest prefix exists, or ok=false otherwise. // -// backtracking is fast, it's just a bitset test and, if found, one popcount. -// max steps in backtracking is the stride length. +// The prefixes in the stride form a complete binary tree (CBT) using the baseIndex function. +// In contrast to the ART algorithm, I do not use an allotment approach but map +// the backtracking in the CBT by a bitset operation with a precalculated backtracking path +// for the respective idx. func (n *node[V]) lpmGet(idx uint) (baseIdx uint, val V, ok bool) { + // top is the idx of the longest-prefix-match if top, ok := n.prefixes.IntersectionTop(lpmLookupTbl[idx]); ok { return top, n.prefixes.MustGet(top), true } From 85a61e2c9682152a23a32d247b8b09d0b846cb95 Mon Sep 17 00:00:00 2001 From: Karl Gaissmaier Date: Tue, 28 Jan 2025 13:25:29 +0100 Subject: [PATCH 02/10] clarify the empty interface as child --- node.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/node.go b/node.go index 58d5520..a3870ae 100644 --- a/node.go +++ b/node.go @@ -36,12 +36,19 @@ var zeroPath [16]byte // The sparse child array recursively spans the trie with a branching factor of 256 // 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 contains the routes indexed as a complete binary tree with payload V + // with the help of the baseIndex function from the ART algorithm. prefixes sparse.Array[V] // children, recursively spans the trie with a branching factor of 256 - // the generic child with empty interface is a node (recursive) or + // the generic child as empty interface{} is a node (recursive) or // a path compressed leaf (prefix and value). + // + // The empty interface{} is by intention instead of a e.g. + // type noder interface { isLeaf() bool } + // + // The empty interface{} consumes less memory and type assertions are faster than + // indirect method calls e.g. node.isLeaf() children sparse.Array[interface{}] } From 5da7a9136264cf0ba09118d67a68e77d58809995 Mon Sep 17 00:00:00 2001 From: Karl Gaissmaier Date: Wed, 29 Jan 2025 23:02:22 +0100 Subject: [PATCH 03/10] typo --- node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node.go b/node.go index a3870ae..7d065dc 100644 --- a/node.go +++ b/node.go @@ -130,7 +130,7 @@ func (n *node[V]) insertAtDepth(pfx netip.Prefix, val V, depth int) (exists bool // create new node // push the leaf down - // insert new child at cureent leaf position (addr) + // insert new child at current leaf position (addr) // descend down, replace n with new child c := new(node[V]) c.insertAtDepth(k.prefix, k.value, depth+1) From 2015f4a367fac107b0c40fd4f29f4a23b0d996ee Mon Sep 17 00:00:00 2001 From: Karl Gaissmaier Date: Wed, 29 Jan 2025 23:21:58 +0100 Subject: [PATCH 04/10] package level comment for bitset --- internal/bitset/bitset.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/internal/bitset/bitset.go b/internal/bitset/bitset.go index a4b6ec0..608d042 100644 --- a/internal/bitset/bitset.go +++ b/internal/bitset/bitset.go @@ -9,6 +9,27 @@ // // This implementation is smaller and faster as the more // general [github.com/bits-and-blooms/bitset]. +// +// All functions can be inlined! +// +// can inline BitSet.Set with cost 60 +// can inline BitSet.Test with cost 26 +// can inline BitSet.Clear with cost 24 +// can inline BitSet.Clone with cost 20 +// can inline BitSet.Compact with cost 35 +// can inline BitSet.FirstSet with cost 25 +// can inline BitSet.NextSet with cost 71 +// can inline BitSet.AsSlice with cost 50 +// can inline BitSet.All with cost 62 +// can inline BitSet.IntersectsAny with cost 42 +// can inline BitSet.IntersectionTop with cost 56 +// can inline BitSet.IntersectionCardinality with cost 35 +// can inline BitSet.InPlaceIntersection with cost 71 +// can inline BitSet.InPlaceUnion with cost 77 +// can inline BitSet.Rank0 with cost 66 +// can inline BitSet.Size with cost 16 +// can inline popcount with cost 12 +// can inline popcountAnd with cost 30 package bitset import ( From 3b1d06d59af9d7c71ba5683ad9635b184a44936f Mon Sep 17 00:00:00 2001 From: Karl Gaissmaier Date: Sat, 1 Feb 2025 10:40:06 +0100 Subject: [PATCH 05/10] again, clarify the empty interface as child --- node.go | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/node.go b/node.go index 7d065dc..1cf4004 100644 --- a/node.go +++ b/node.go @@ -40,15 +40,27 @@ type node[V any] struct { // with the help of the baseIndex function from the ART algorithm. prefixes sparse.Array[V] - // children, recursively spans the trie with a branching factor of 256 - // the generic child as empty interface{} is a node (recursive) or + // Sorry, here we have to use a mixture of generics and interfaces: + // + // children, recursively spans the trie with a branching factor of 256. + // + // Without path compression, the definition would naturally be: + // children sparse.Array[*node[V]] + // + // ... but with path compression the child can now be a node (recursive) or // a path compressed leaf (prefix and value). // - // The empty interface{} is by intention instead of a e.g. - // type noder interface { isLeaf() bool } + // With path compression we could define: + // type noder[V any] interface { + // isLeaf[V]() bool + // } + // + // and + // children sparse.Array[noder[V]] // + // But we use the empty interface{} instead, by intention! // The empty interface{} consumes less memory and type assertions are faster than - // indirect method calls e.g. node.isLeaf() + // indirect method calls like node.isLeaf() children sparse.Array[interface{}] } From 79e1f00346c8c3d1e65b93b15d7d088587133758 Mon Sep 17 00:00:00 2001 From: Karl Gaissmaier Date: Sat, 1 Feb 2025 15:02:03 +0100 Subject: [PATCH 06/10] improve cloning --- node.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/node.go b/node.go index 1cf4004..a2d031d 100644 --- a/node.go +++ b/node.go @@ -88,9 +88,6 @@ func cloneOrCopyValue[V any](v V) V { // cloneLeaf returns a copy of the leaf. // If the value implements the Cloner interface, the values are deeply copied. func (l *leaf[V]) cloneLeaf() *leaf[V] { - if l == nil { - return nil - } return &leaf[V]{l.prefix, cloneOrCopyValue(l.value)} } @@ -219,6 +216,9 @@ func (n *node[V]) lpmTest(idx uint) bool { // cloneRec, clones the node recursive. func (n *node[V]) cloneRec() *node[V] { + var zero V + _, isCloner := any(zero).(Cloner[V]) + if n == nil { return nil } @@ -232,8 +232,11 @@ func (n *node[V]) cloneRec() *node[V] { c.prefixes = *(n.prefixes.Copy()) // deep copy if V implements Cloner[V] - for i, v := range c.prefixes.Items { - c.prefixes.Items[i] = cloneOrCopyValue(v) + if isCloner { + for i, val := range c.prefixes.Items { + val := any(val).(Cloner[V]) + c.prefixes.Items[i] = val.Clone() + } } // shallow From 83f20778e8bfdd9c139ea5f1d2a0ac2ba8740ce9 Mon Sep 17 00:00:00 2001 From: Karl Gaissmaier Date: Fri, 7 Feb 2025 21:28:08 +0100 Subject: [PATCH 07/10] example --- example_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/example_test.go b/example_test.go index cd6b16c..e8b692e 100644 --- a/example_test.go +++ b/example_test.go @@ -11,6 +11,51 @@ import ( "github.com/gaissmai/bart" ) +func ExampleTable_Contains() { + // Create a new routing table + table := new(bart.Table[struct{}]) + + // Insert some prefixes + prefixes := []string{ + "192.168.0.0/16", // corporate + "192.168.1.0/24", // department + "2001:7c0:3100::/40", // corporate + "2001:7c0:3100:1::/64", // department + "fc00::/7", // unique local + } + + for _, s := range prefixes { + pfx := netip.MustParsePrefix(s) + table.Insert(pfx, struct{}{}) + } + + // Test some IP addresses for black/whitelist containment + ips := []string{ + "192.168.1.100", // must match, department + "192.168.2.1", // must match, corporate + "2001:7c0:3100:1::1", // must match, department + "2001:7c0:3100:2::1", // must match, corporate + "fc00::1", // must match, unique local + // + "172.16.0.1", // must NOT match + "2003:dead:beef::1", // must NOT match + } + + for _, s := range ips { + ip := netip.MustParseAddr(s) + fmt.Printf("%-20s is contained: %t\n", ip, table.Contains(ip)) + } + + // Output: + // 192.168.1.100 is contained: true + // 192.168.2.1 is contained: true + // 2001:7c0:3100:1::1 is contained: true + // 2001:7c0:3100:2::1 is contained: true + // fc00::1 is contained: true + // 172.16.0.1 is contained: false + // 2003:dead:beef::1 is contained: false +} + var ( a = netip.MustParseAddr p = netip.MustParsePrefix From 825c53254bea3e8c7552c822b0223feb1f63c368 Mon Sep 17 00:00:00 2001 From: Karl Gaissmaier Date: Sun, 9 Feb 2025 13:27:08 +0100 Subject: [PATCH 08/10] lots of naming and formatting --- dumper.go | 28 +++++-- fulltable_test.go | 62 ++++++++++----- gold_stride_test.go | 4 +- gold_table_test.go | 4 +- internal/bitset/bitset.go | 9 +-- internal/sparse/array.go | 11 +-- jsonify.go | 6 +- nocopy.go | 20 ----- node.go | 143 ++++++++++++++++++++-------------- node_test.go | 2 +- overlaps.go | 32 ++++---- stringify.go | 8 +- table.go | 157 +++++++++++++++++++++++++------------- table_cb_test.go | 14 ++-- table_iter_test.go | 12 +-- table_test.go | 2 +- 16 files changed, 298 insertions(+), 216 deletions(-) delete mode 100644 nocopy.go diff --git a/dumper.go b/dumper.go index 02564a3..c308824 100644 --- a/dumper.go +++ b/dumper.go @@ -41,18 +41,18 @@ func (t *Table[V]) dump(w io.Writer) { if t.size4 > 0 { fmt.Fprintln(w) fmt.Fprintf(w, "### IPv4: size(%d), nodes(%d)", t.size4, t.root4.nodeStatsRec().nodes) - t.root4.dumpRec(w, zeroPath, 0, true) + t.root4.dumpRec(w, stridePath{}, 0, true) } if t.size6 > 0 { fmt.Fprintln(w) fmt.Fprintf(w, "### IPv6: size(%d), nodes(%d)", t.size6, t.root6.nodeStatsRec().nodes) - t.root6.dumpRec(w, zeroPath, 0, false) + t.root6.dumpRec(w, stridePath{}, 0, false) } } // dumpRec, rec-descent the trie. -func (n *node[V]) dumpRec(w io.Writer, path [16]byte, depth int, is4 bool) { +func (n *node[V]) dumpRec(w io.Writer, path stridePath, depth int, is4 bool) { // dump this node n.dump(w, path, depth, is4) @@ -68,7 +68,7 @@ func (n *node[V]) dumpRec(w io.Writer, path [16]byte, depth int, is4 bool) { } // dump the node to w. -func (n *node[V]) dump(w io.Writer, path [16]byte, depth int, is4 bool) { +func (n *node[V]) dump(w io.Writer, path stridePath, depth int, is4 bool) { bits := depth * strideLen indent := strings.Repeat(".", depth) @@ -114,8 +114,12 @@ func (n *node[V]) dump(w io.Writer, path [16]byte, depth int, is4 bool) { case *node[V]: nodeAddrs = append(nodeAddrs, addr) continue + case *leaf[V]: leafAddrs = append(leafAddrs, addr) + + default: + panic("logic error, wrong node type") } } @@ -179,7 +183,7 @@ func octetFmt(octet byte, is4 bool) string { // // 127.0.0 // 2001:0d -func ipStridePath(path [16]byte, depth int, is4 bool) string { +func ipStridePath(path stridePath, depth int, is4 bool) string { buf := new(strings.Builder) if is4 { @@ -240,10 +244,15 @@ func (n *node[V]) nodeStats() stats { switch n.children.Items[i].(type) { case *node[V]: s.nodes++ + case *leaf[V]: s.leaves++ + + default: + panic("logic error, wrong node type") } } + return s } @@ -259,11 +268,11 @@ func (n *node[V]) nodeStatsRec() stats { s.nodes = 1 // this node s.leaves = 0 - for _, c := range n.children.Items { - switch k := c.(type) { + for _, kidAny := range n.children.Items { + switch kid := kidAny.(type) { case *node[V]: // rec-descent - rs := k.nodeStatsRec() + rs := kid.nodeStatsRec() s.pfxs += rs.pfxs s.childs += rs.childs @@ -272,6 +281,9 @@ func (n *node[V]) nodeStatsRec() stats { case *leaf[V]: s.leaves++ + + default: + panic("logic error, wrong node type") } } diff --git a/fulltable_test.go b/fulltable_test.go index bc24327..f9dc27a 100644 --- a/fulltable_test.go +++ b/fulltable_test.go @@ -252,17 +252,17 @@ func BenchmarkFullTableOverlapsV4(b *testing.B) { rt.Insert(route.CIDR, i) } - for i := 1; i <= 1024; i *= 2 { - inter := new(Table[int]) - for j := 0; j <= i; j++ { - pfx := randomPrefix4() - inter.Insert(pfx, j) + for i := 1; i <= 1<<20; i *= 2 { + rt2 := new(Table[int]) + for j, pfx := range randomRealWorldPrefixes4(i) { + rt2.Insert(pfx, j) } + b.Log(rt2.String()) b.Run(fmt.Sprintf("With_%4d", i), func(b *testing.B) { b.ResetTimer() for range b.N { - boolSink = rt.Overlaps(inter) + boolSink = rt.Overlaps(rt2) } }) } @@ -275,17 +275,17 @@ func BenchmarkFullTableOverlapsV6(b *testing.B) { rt.Insert(route.CIDR, i) } - for i := 1; i <= 1024; i *= 2 { - inter := new(Table[int]) - for j := 0; j <= i; j++ { - pfx := randomPrefix6() - inter.Insert(pfx, j) + for i := 1; i <= 1<<20; i *= 2 { + rt2 := new(Table[int]) + for j, pfx := range randomRealWorldPrefixes6(i) { + rt2.Insert(pfx, j) } + b.Log(rt2.String()) b.Run(fmt.Sprintf("With_%4d", i), func(b *testing.B) { b.ResetTimer() for range b.N { - boolSink = rt.Overlaps(inter) + boolSink = rt.Overlaps(rt2) } }) } @@ -298,7 +298,7 @@ func BenchmarkFullTableOverlapsPrefix(b *testing.B) { rt.Insert(route.CIDR, i) } - pfx := randomPrefix() + pfx := randomRealWorldPrefixes(1)[0] b.ResetTimer() for range b.N { @@ -466,16 +466,28 @@ func fillRouteTables() { // ######################################################### -func gimmeRandomPrefixes4(n int) []netip.Prefix { +func randomRealWorldPrefixes4(n int) []netip.Prefix { set := map[netip.Prefix]netip.Prefix{} pfxs := make([]netip.Prefix, 0, n) for { pfx := randomPrefix4() + + // skip too small or too big masks + if pfx.Bits() < 8 && pfx.Bits() > 28 { + continue + } + + // skip multicast ... + if pfx.Overlaps(mpp("240.0.0.0/8")) { + continue + } + if _, ok := set[pfx]; !ok { set[pfx] = pfx pfxs = append(pfxs, pfx) } + if len(set) >= n { break } @@ -483,12 +495,26 @@ func gimmeRandomPrefixes4(n int) []netip.Prefix { return pfxs } -func gimmeRandomPrefixes6(n int) []netip.Prefix { +func randomRealWorldPrefixes6(n int) []netip.Prefix { set := map[netip.Prefix]netip.Prefix{} pfxs := make([]netip.Prefix, 0, n) for { pfx := randomPrefix6() + + // skip too small or too big masks + if pfx.Bits() < 16 || pfx.Bits() > 56 { + continue + } + + // skip non global routes seen in the real world + if !pfx.Overlaps(mpp("2000::/3")) { + continue + } + if pfx.Addr().Compare(mpp("2c0f::/16").Addr()) == 1 { + continue + } + if _, ok := set[pfx]; !ok { set[pfx] = pfx pfxs = append(pfxs, pfx) @@ -500,10 +526,10 @@ func gimmeRandomPrefixes6(n int) []netip.Prefix { return pfxs } -func gimmeRandomPrefixes(n int) []netip.Prefix { +func randomRealWorldPrefixes(n int) []netip.Prefix { pfxs := make([]netip.Prefix, 0, n) - pfxs = append(pfxs, gimmeRandomPrefixes4(n/2)...) - pfxs = append(pfxs, gimmeRandomPrefixes6(n-len(pfxs))...) + pfxs = append(pfxs, randomRealWorldPrefixes4(n/2)...) + pfxs = append(pfxs, randomRealWorldPrefixes6(n-len(pfxs))...) prng.Shuffle(n, func(i, j int) { pfxs[i], pfxs[j] = pfxs[j], pfxs[i] diff --git a/gold_stride_test.go b/gold_stride_test.go index 2e19e5e..e3f73dd 100644 --- a/gold_stride_test.go +++ b/gold_stride_test.go @@ -18,8 +18,8 @@ type goldStrideItem[V any] struct { } func (t *goldStrideTbl[V]) insertMany(strides []goldStrideItem[V]) *goldStrideTbl[V] { - cast := goldStrideTbl[V](strides) - t = &cast + conv := goldStrideTbl[V](strides) + t = &conv return t } diff --git a/gold_table_test.go b/gold_table_test.go index d49ea7c..cfc7f30 100644 --- a/gold_table_test.go +++ b/gold_table_test.go @@ -31,8 +31,8 @@ func (t *goldTable[V]) insert(pfx netip.Prefix, val V) { } func (t *goldTable[V]) insertMany(pfxs []goldTableItem[V]) *goldTable[V] { - cast := goldTable[V](pfxs) - t = &cast + conv := goldTable[V](pfxs) + t = &conv return t } diff --git a/internal/bitset/bitset.go b/internal/bitset/bitset.go index 608d042..d835326 100644 --- a/internal/bitset/bitset.go +++ b/internal/bitset/bitset.go @@ -15,7 +15,7 @@ // can inline BitSet.Set with cost 60 // can inline BitSet.Test with cost 26 // can inline BitSet.Clear with cost 24 -// can inline BitSet.Clone with cost 20 +// can inline BitSet.Clone with cost 7 // can inline BitSet.Compact with cost 35 // can inline BitSet.FirstSet with cost 25 // can inline BitSet.NextSet with cost 71 @@ -94,12 +94,7 @@ func (b BitSet) Test(i uint) (ok bool) { // Clone this BitSet, returning a new BitSet that has the same bits set. func (b BitSet) Clone() BitSet { - if b == nil { - return nil - } - c := BitSet(make([]uint64, len(b), cap(b))) - copy(c, b) - return c + return append(b[:0:0], b...) } // Compact, preserve all set bits, while minimizing memory usage. diff --git a/internal/sparse/array.go b/internal/sparse/array.go index 3c4393e..5c32e9f 100644 --- a/internal/sparse/array.go +++ b/internal/sparse/array.go @@ -88,16 +88,9 @@ func (s *Array[T]) Copy() *Array[T] { return nil } - var items []T - - if s.Items != nil { - items = make([]T, len(s.Items), cap(s.Items)) - copy(items, s.Items) // shallow - } - return &Array[T]{ - s.BitSet.Clone(), - items, + BitSet: s.BitSet.Clone(), + Items: append(s.Items[:0:0], s.Items...), } } diff --git a/jsonify.go b/jsonify.go index 44372d0..8e37038 100644 --- a/jsonify.go +++ b/jsonify.go @@ -45,7 +45,7 @@ func (t *Table[V]) DumpList4() []DumpListNode[V] { if t == nil { return nil } - return t.root4.dumpListRec(0, zeroPath, 0, true) + return t.root4.dumpListRec(0, stridePath{}, 0, true) } // DumpList6 dumps the ipv6 tree into a list of roots and their subnets. @@ -54,10 +54,10 @@ func (t *Table[V]) DumpList6() []DumpListNode[V] { if t == nil { return nil } - return t.root6.dumpListRec(0, zeroPath, 0, false) + return t.root6.dumpListRec(0, stridePath{}, 0, false) } -func (n *node[V]) dumpListRec(parentIdx uint, path [16]byte, depth int, is4 bool) []DumpListNode[V] { +func (n *node[V]) dumpListRec(parentIdx uint, path stridePath, depth int, is4 bool) []DumpListNode[V] { // recursion stop condition if n == nil { return nil diff --git a/nocopy.go b/nocopy.go deleted file mode 100644 index 32f9895..0000000 --- a/nocopy.go +++ /dev/null @@ -1,20 +0,0 @@ -package bart - -// noCopy may be added to structs which must not be copied -// after the first use. -// -// type My struct { -// _ noCopy -// A state -// b foo -// } -// -// See https://golang.org/issues/8005#issuecomment-190753527 -// for details. -// -// Note that it must not be embedded, due to the Lock and Unlock methods. -type noCopy struct{} - -// Lock is a no-op used by -copylocks checker from `go vet`. -func (*noCopy) Lock() {} -func (*noCopy) Unlock() {} diff --git a/node.go b/node.go index a2d031d..95d61eb 100644 --- a/node.go +++ b/node.go @@ -17,8 +17,8 @@ const ( maxNodePrefixes = 512 // 512 ) -// a zero value, used manifold -var zeroPath [16]byte +// stridePath, max 16 octets deep +type stridePath [maxTreeDepth]uint8 // node is a level node in the multibit-trie. // A node has prefixes and children, forming the multibit trie. @@ -40,25 +40,25 @@ type node[V any] struct { // with the help of the baseIndex function from the ART algorithm. prefixes sparse.Array[V] - // Sorry, here we have to use a mixture of generics and interfaces: - // // children, recursively spans the trie with a branching factor of 256. // + // Sorry, here we have to use a mixture of generics and interfaces: + // // Without path compression, the definition would naturally be: // children sparse.Array[*node[V]] // - // ... but with path compression the child can now be a node (recursive) or - // a path compressed leaf (prefix and value). + // ... but with path compression the child can now be a node or a path compressed leaf. // // With path compression we could define: // type noder[V any] interface { // isLeaf[V]() bool // } // - // and + // and: // children sparse.Array[noder[V]] // - // But we use the empty interface{} instead, by intention! + // But we use the empty interface{} instead, by intention, see below! + // // The empty interface{} consumes less memory and type assertions are faster than // indirect method calls like node.isLeaf() children sparse.Array[interface{}] @@ -69,26 +69,26 @@ 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 +// leaf is a prefix with value, used as a path compressed child type leaf[V any] struct { prefix netip.Prefix value V } -// cloneOrCopyValue, helper function, +// cloneOrCopy, 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() +func cloneOrCopy[V any](val V) V { + if cloner, ok := any(val).(Cloner[V]); ok { + return cloner.Clone() } - // just copy - return v + // just a shallow copy + return val } -// cloneLeaf returns a copy of the leaf. -// If the value implements the Cloner interface, the values are deeply copied. +// cloneLeaf returns a clone of the leaf +// if the value implements the Cloner interface. func (l *leaf[V]) cloneLeaf() *leaf[V] { - return &leaf[V]{l.prefix, cloneOrCopyValue(l.value)} + return &leaf[V]{l.prefix, cloneOrCopy(l.value)} } // insertAtDepth insert a prefix/val into a node tree at depth. @@ -103,7 +103,7 @@ func (n *node[V]) insertAtDepth(pfx netip.Prefix, val V, depth int) (exists bool lastIdx, lastBits := lastOctetIdxAndBits(bits) - octets := ipAsOctets(ip, ip.Is4()) + octets := ip.AsSlice() octets = octets[:lastIdx+1] // find the proper trie node to insert prefix @@ -123,16 +123,16 @@ func (n *node[V]) insertAtDepth(pfx netip.Prefix, val V, depth int) (exists bool } // get the child: node or leaf - switch k := n.children.MustGet(addr).(type) { + switch kid := n.children.MustGet(addr).(type) { case *node[V]: - // descend down to next trie level - n = k - continue + n = kid + continue // descend down to next trie level + case *leaf[V]: // reached a path compressed prefix // override value in slot if prefixes are equal - if k.prefix == pfx { - k.value = val + if kid.prefix == pfx { + kid.value = val // exists return true } @@ -141,11 +141,14 @@ func (n *node[V]) insertAtDepth(pfx netip.Prefix, val V, depth int) (exists bool // push the leaf down // insert new child at current leaf position (addr) // descend down, replace n with new child - c := new(node[V]) - c.insertAtDepth(k.prefix, k.value, depth+1) + newNode := new(node[V]) + newNode.insertAtDepth(kid.prefix, kid.value, depth+1) + + n.children.InsertAt(addr, newNode) + n = newNode - n.children.InsertAt(addr, c) - n = c + default: + panic("logic error, wrong node type") } } @@ -153,7 +156,7 @@ func (n *node[V]) insertAtDepth(pfx netip.Prefix, val V, depth int) (exists bool } // purgeAndCompress, purge empty nodes or compress nodes with single prefix or leaf. -func (n *node[V]) purgeAndCompress(parentStack []*node[V], childPath []byte, is4 bool) { +func (n *node[V]) purgeAndCompress(parentStack []*node[V], childPath []uint8, is4 bool) { // unwind the stack for i := len(parentStack) - 1; i >= 0; i-- { parent := parentStack[i] @@ -173,7 +176,7 @@ func (n *node[V]) purgeAndCompress(parentStack []*node[V], childPath []byte, is4 idx, _ := n.prefixes.FirstSet() val := n.prefixes.Items[0] - path := [16]byte{} + path := stridePath{} copy(path[:], childPath) pfx := cidrFromPath(path, i+1, is4, idx) @@ -216,9 +219,6 @@ func (n *node[V]) lpmTest(idx uint) bool { // cloneRec, clones the node recursive. func (n *node[V]) cloneRec() *node[V] { - var zero V - _, isCloner := any(zero).(Cloner[V]) - if n == nil { return nil } @@ -231,11 +231,12 @@ func (n *node[V]) cloneRec() *node[V] { // shallow c.prefixes = *(n.prefixes.Copy()) + _, isCloner := any(*new(V)).(Cloner[V]) + // deep copy if V implements Cloner[V] if isCloner { for i, val := range c.prefixes.Items { - val := any(val).(Cloner[V]) - c.prefixes.Items[i] = val.Clone() + c.prefixes.Items[i] = cloneOrCopy(val) } } @@ -243,14 +244,17 @@ func (n *node[V]) cloneRec() *node[V] { c.children = *(n.children.Copy()) // deep copy of nodes and leaves - for i, k := range c.children.Items { - switch k := k.(type) { + for i, kidAny := range c.children.Items { + switch kid := kidAny.(type) { case *node[V]: // clone the child node rec-descent - c.children.Items[i] = k.cloneRec() + c.children.Items[i] = kid.cloneRec() case *leaf[V]: // deep copy if V implements Cloner[V] - c.children.Items[i] = k.cloneLeaf() + c.children.Items[i] = kid.cloneLeaf() + + default: + panic("logic error, wrong node type") } } @@ -263,7 +267,7 @@ 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 stridePath, depth int, is4 bool, yield func(netip.Prefix, V) bool) bool { // for all prefixes in this node do ... allIndices := n.prefixes.AsSlice(make([]uint, 0, maxNodePrefixes)) for _, idx := range allIndices { @@ -279,20 +283,23 @@ func (n *node[V]) allRec(path [16]byte, depth int, is4 bool, yield func(netip.Pr // for all children (nodes and leaves) in this node do ... allChildAddrs := n.children.AsSlice(make([]uint, 0, maxNodeChildren)) for i, addr := range allChildAddrs { - switch k := n.children.Items[i].(type) { + switch kid := n.children.Items[i].(type) { case *node[V]: // rec-descent with this node path[depth] = byte(addr) - if !k.allRec(path, depth+1, is4, yield) { + if !kid.allRec(path, depth+1, is4, yield) { // early exit return false } case *leaf[V]: // callback for this leaf - if !yield(k.prefix, k.value) { + if !yield(kid.prefix, kid.value) { // early exit return false } + + default: + panic("logic error, wrong node type") } } @@ -305,7 +312,7 @@ 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 stridePath, depth int, is4 bool, yield func(netip.Prefix, V) bool) bool { // get slice of all child octets, sorted by addr allChildAddrs := n.children.AsSlice(make([]uint, 0, maxNodeChildren)) @@ -330,16 +337,19 @@ func (n *node[V]) allRecSorted(path [16]byte, depth int, is4 bool, yield func(ne } // yield the node (rec-descent) or leaf - switch k := n.children.Items[j].(type) { + switch kid := n.children.Items[j].(type) { case *node[V]: path[depth] = byte(childAddr) - if !k.allRecSorted(path, depth+1, is4, yield) { + if !kid.allRecSorted(path, depth+1, is4, yield) { return false } case *leaf[V]: - if !yield(k.prefix, k.value) { + if !yield(kid.prefix, kid.value) { return false } + + default: + panic("logic error, wrong node type") } childCursor++ @@ -356,16 +366,19 @@ func (n *node[V]) allRecSorted(path [16]byte, depth int, is4 bool, yield func(ne // yield the rest of leaves and nodes (rec-descent) for j := childCursor; j < len(allChildAddrs); j++ { addr := allChildAddrs[j] - switch k := n.children.Items[j].(type) { + switch kid := n.children.Items[j].(type) { case *node[V]: path[depth] = byte(addr) - if !k.allRecSorted(path, depth+1, is4, yield) { + if !kid.allRecSorted(path, depth+1, is4, yield) { return false } case *leaf[V]: - if !yield(k.prefix, k.value) { + if !yield(kid.prefix, kid.value) { return false } + + default: + panic("logic error, wrong node type") } } @@ -419,6 +432,9 @@ LOOP: n.children.InsertAt(addr, otherChild.cloneLeaf()) continue LOOP } + + default: + panic("logic error, wrong node type") } } @@ -474,6 +490,9 @@ LOOP: n.children.InsertAt(addr, nc) continue LOOP } + + default: + panic("logic error, wrong node type") } } @@ -488,7 +507,7 @@ func (n *node[V]) eachLookupPrefix(octets []byte, depth int, is4 bool, pfxLen in } // octets as array, needed below more than once - var path [16]byte + var path stridePath copy(path[:], octets) // backtracking the CBT @@ -509,7 +528,7 @@ func (n *node[V]) eachLookupPrefix(octets []byte, depth int, is4 bool, pfxLen in // eachSubnet calls yield() for any covered CIDR by parent prefix in natural CIDR sort order. func (n *node[V]) eachSubnet(octets []byte, depth int, is4 bool, pfxLen int, yield func(netip.Prefix, V) bool) bool { // octets as array, needed below more than once - var path [16]byte + var path stridePath copy(path[:], octets) pfxFirstAddr := uint(octets[depth]) @@ -557,18 +576,21 @@ func (n *node[V]) eachSubnet(octets []byte, depth int, is4 bool, pfxLen int, yie } // yield the node or leaf? - switch k := n.children.MustGet(addr).(type) { + switch kid := n.children.MustGet(addr).(type) { case *node[V]: path[depth] = byte(addr) - if !k.allRecSorted(path, depth+1, is4, yield) { + if !kid.allRecSorted(path, depth+1, is4, yield) { return false } case *leaf[V]: - if !yield(k.prefix, k.value) { + if !yield(kid.prefix, kid.value) { return false } + + default: + panic("logic error, wrong node type") } addrCursor++ @@ -585,18 +607,21 @@ func (n *node[V]) eachSubnet(octets []byte, depth int, is4 bool, pfxLen int, yie // yield the rest of leaves and nodes (rec-descent) for _, addr := range allCoveredChildAddrs[addrCursor:] { // yield the node or leaf? - switch k := n.children.MustGet(addr).(type) { + switch kid := n.children.MustGet(addr).(type) { case *node[V]: path[depth] = byte(addr) - if !k.allRecSorted(path, depth+1, is4, yield) { + if !kid.allRecSorted(path, depth+1, is4, yield) { return false } case *leaf[V]: - if !yield(k.prefix, k.value) { + if !yield(kid.prefix, kid.value) { return false } + + default: + panic("logic error, wrong node type") } } diff --git a/node_test.go b/node_test.go index 7aa79dd..151277b 100644 --- a/node_test.go +++ b/node_test.go @@ -112,7 +112,7 @@ func TestOverlapsPrefix(t *testing.T) { for _, tt := range allStridePfxs() { goldOK := gold.strideOverlapsPrefix(tt.octet, tt.bits) - fastOK := fast.overlapsIdx(tt.octet, tt.bits) + fastOK := fast.overlapsIdx(pfxToIdx(tt.octet, tt.bits)) if goldOK != fastOK { t.Fatalf("overlapsPrefix(%d, %d) = %v, want %v", tt.octet, tt.bits, fastOK, goldOK) } diff --git a/overlaps.go b/overlaps.go index f94c6bc..76207ab 100644 --- a/overlaps.go +++ b/overlaps.go @@ -209,9 +209,9 @@ func overlapsTwoChilds[V any](nChild, oChild any, depth int) bool { case *node[V]: switch oKind := oChild.(type) { case *node[V]: // node, node - return nKind.overlaps(oKind, depth+1) // node, node + return nKind.overlaps(oKind, depth+1) case *leaf[V]: // node, leaf - return nKind.overlapsPrefixAtDepth(oKind.prefix, depth) // node, node + return nKind.overlapsPrefixAtDepth(oKind.prefix, depth) } case *leaf[V]: @@ -221,6 +221,9 @@ func overlapsTwoChilds[V any](nChild, oChild any, depth int) bool { case *leaf[V]: // leaf, leaf return oKind.prefix.Overlaps(nKind.prefix) } + + default: + panic("logic error, wrong node type") } return false @@ -234,19 +237,18 @@ func (n *node[V]) overlapsPrefixAtDepth(pfx netip.Prefix, depth int) bool { ip := pfx.Addr() bits := pfx.Bits() - sigOctetIdx := (bits - 1) / strideLen - sigOctetBits := bits - (sigOctetIdx * strideLen) + lastIdx, lastBits := lastOctetIdxAndBits(bits) - octets := ipAsOctets(ip, ip.Is4()) - octets = octets[:sigOctetIdx+1] + octets := ip.AsSlice() + octets = octets[:lastIdx+1] for ; depth < len(octets); depth++ { octet := octets[depth] addr := uint(octet) // full octet path in node trie, check overlap with last prefix octet - if depth == sigOctetIdx { - return n.overlapsIdx(octet, sigOctetBits) + if depth == lastIdx { + return n.overlapsIdx(pfxToIdx(octet, lastBits)) } // test if any route overlaps prefix´ so far @@ -260,22 +262,24 @@ func (n *node[V]) overlapsPrefixAtDepth(pfx netip.Prefix, depth int) bool { } // next child, node or leaf - switch k := n.children.MustGet(addr).(type) { + switch kid := n.children.MustGet(addr).(type) { case *node[V]: - n = k + n = kid continue case *leaf[V]: - return k.prefix.Overlaps(pfx) + return kid.prefix.Overlaps(pfx) + + default: + panic("logic error, wrong node type") } } - panic("unreachable") + panic("unreachable: " + pfx.String()) } // overlapsIdx returns true if node overlaps with prefix. -func (n *node[V]) overlapsIdx(octet byte, pfxLen int) bool { +func (n *node[V]) overlapsIdx(idx uint) bool { // 1. Test if any route in this node overlaps prefix? - idx := pfxToIdx(octet, pfxLen) if n.lpmTest(idx) { return true } diff --git a/stringify.go b/stringify.go index 5ccbac7..68b7dc0 100644 --- a/stringify.go +++ b/stringify.go @@ -20,7 +20,7 @@ type kid[V any] struct { // for traversing n *node[V] is4 bool - path [16]byte + path stridePath depth int idx uint @@ -102,7 +102,7 @@ func (t *Table[V]) fprint(w io.Writer, is4 bool) error { startKid := kid[V]{ n: nil, idx: 0, - path: zeroPath, + path: stridePath{}, is4: is4, } @@ -160,7 +160,7 @@ func (n *node[V]) fprintRec(w io.Writer, parent kid[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) []kid[V] { +func (n *node[V]) getKidsRec(parentIdx uint, path stridePath, depth int, is4 bool) []kid[V] { // recursion stop condition if n == nil { return nil @@ -238,7 +238,7 @@ func cmpPrefix(a, b netip.Prefix) int { } // cidrFromPath, get prefix back from byte path, depth, octet and pfxLen. -func cidrFromPath(path [16]byte, depth int, is4 bool, idx uint) netip.Prefix { +func cidrFromPath(path stridePath, depth int, is4 bool, idx uint) netip.Prefix { octet, pfxLen := idxToPfx(idx) // set masked byte in path at depth diff --git a/table.go b/table.go index 6645f53..e59dece 100644 --- a/table.go +++ b/table.go @@ -121,26 +121,30 @@ func (t *Table[V]) Update(pfx netip.Prefix, cb func(val V, ok bool) V) (newVal V } // get node or leaf for octet - switch k := n.children.MustGet(addr).(type) { + switch kid := n.children.MustGet(addr).(type) { case *node[V]: - n = k - continue + n = kid + continue // descend down to next trie level + case *leaf[V]: // update existing value if prefixes are equal - if k.prefix == pfx { - k.value = cb(k.value, true) - return k.value + if kid.prefix == pfx { + kid.value = cb(kid.value, true) + return kid.value } // create new node // push the leaf down // insert new child at current leaf position (addr) // descend down, replace n with new child - c := new(node[V]) - c.insertAtDepth(k.prefix, k.value, depth+1) + newNode := new(node[V]) + newNode.insertAtDepth(kid.prefix, kid.value, depth+1) - n.children.InsertAt(addr, c) - n = c + n.children.InsertAt(addr, newNode) + n = newNode + + default: + panic("logic error, wrong node type") } } @@ -205,14 +209,14 @@ func (t *Table[V]) getAndDelete(pfx netip.Prefix) (val V, ok bool) { } // get the child: node or leaf - switch k := n.children.MustGet(addr).(type) { + switch kid := n.children.MustGet(addr).(type) { case *node[V]: - // descend down to next trie level - n = k - continue + n = kid + continue // descend down to next trie level + case *leaf[V]: // reached a path compressed prefix, stop traversing - if k.prefix != pfx { + if kid.prefix != pfx { return val, false } @@ -222,7 +226,10 @@ func (t *Table[V]) getAndDelete(pfx netip.Prefix) (val V, ok bool) { t.sizeUpdate(is4, -1) n.purgeAndCompress(stack[:depth], octets, is4) - return k.value, true + return kid.value, true + + default: + panic("logic error, wrong node type") } } @@ -266,17 +273,20 @@ LOOP: } // get the child: node or leaf - switch k := n.children.MustGet(addr).(type) { + switch kid := n.children.MustGet(addr).(type) { case *node[V]: - // descend down to next trie level - n = k - continue + n = kid + continue // descend down to next trie level + case *leaf[V]: // reached a path compressed prefix, stop traversing - if k.prefix == pfx { - return k.value, true + if kid.prefix == pfx { + return kid.value, true } break LOOP + + default: + panic("logic error, wrong node type") } } @@ -312,12 +322,16 @@ func (t *Table[V]) Contains(ip netip.Addr) bool { } // get node or leaf for octet - switch k := n.children.MustGet(addr).(type) { + switch kid := n.children.MustGet(addr).(type) { case *node[V]: - n = k - continue + n = kid + continue // descend down to next trie level + case *leaf[V]: - return k.prefix.Contains(ip) + return kid.prefix.Contains(ip) + + default: + panic("logic error, wrong node type") } } @@ -359,17 +373,20 @@ LOOP: } // get the child: node or leaf - switch k := n.children.MustGet(addr).(type) { + switch kid := n.children.MustGet(addr).(type) { case *node[V]: - // descend down to next trie level - n = k - continue + n = kid + continue // descend down to next trie level + case *leaf[V]: // reached a path compressed prefix, stop traversing - if k.prefix.Contains(ip) { - return k.value, true + if kid.prefix.Contains(ip) { + return kid.value, true } break LOOP + + default: + panic("logic error, wrong node type") } } @@ -451,19 +468,22 @@ LOOP: } // get the child: node or leaf - switch k := n.children.MustGet(addr).(type) { + switch kid := n.children.MustGet(addr).(type) { case *node[V]: - // descend down to next trie level - n = k - continue LOOP + n = kid + continue LOOP // descend down to next trie level + case *leaf[V]: // reached a path compressed prefix, stop traversing // must not be masked for Contains(pfx.Addr) - if k.prefix.Contains(ip) && k.prefix.Bits() <= bits { - return k.prefix, k.value, true + if kid.prefix.Contains(ip) && kid.prefix.Bits() <= bits { + return kid.prefix, kid.value, true } break LOOP + + default: + panic("logic error, wrong node type") } } @@ -551,19 +571,23 @@ func (t *Table[V]) Supernets(pfx netip.Prefix) func(yield func(netip.Prefix, V) break LOOP } - switch k := n.children.MustGet(addr).(type) { + switch kid := n.children.MustGet(addr).(type) { case *node[V]: - n = k - continue LOOP + n = kid + continue LOOP // descend down to next trie level + case *leaf[V]: - if k.prefix.Overlaps(pfx) && k.prefix.Bits() <= pfx.Bits() { - if !yield(k.prefix, k.value) { + if kid.prefix.Overlaps(pfx) && kid.prefix.Bits() <= pfx.Bits() { + if !yield(kid.prefix, kid.value) { // early exit return } } // end of trie along this octets path break LOOP + + default: + panic("logic error, wrong node type") } } @@ -627,15 +651,19 @@ func (t *Table[V]) Subnets(pfx netip.Prefix) func(yield func(netip.Prefix, V) bo } // node or leaf? - switch k := n.children.MustGet(addr).(type) { + switch kid := n.children.MustGet(addr).(type) { case *node[V]: - n = k - continue + n = kid + continue // descend down to next trie level + case *leaf[V]: - if pfx.Overlaps(k.prefix) && pfx.Bits() <= k.prefix.Bits() { - _ = yield(k.prefix, k.value) + if pfx.Overlaps(kid.prefix) && pfx.Bits() <= kid.prefix.Bits() { + _ = yield(kid.prefix, kid.value) } return + + default: + panic("logic error, wrong node type") } } } @@ -744,43 +772,43 @@ func (t *Table[V]) Size6() int { // next. func (t *Table[V]) All() func(yield func(pfx netip.Prefix, val V) bool) { return func(yield func(netip.Prefix, V) bool) { - _ = t.root4.allRec(zeroPath, 0, true, yield) && t.root6.allRec(zeroPath, 0, false, yield) + _ = t.root4.allRec(stridePath{}, 0, true, yield) && t.root6.allRec(stridePath{}, 0, false, yield) } } // All4, like [Table.All] but only for the v4 routing table. func (t *Table[V]) All4() func(yield func(pfx netip.Prefix, val V) bool) { return func(yield func(netip.Prefix, V) bool) { - _ = t.root4.allRec(zeroPath, 0, true, yield) + _ = t.root4.allRec(stridePath{}, 0, true, yield) } } // All6, like [Table.All] but only for the v6 routing table. func (t *Table[V]) All6() func(yield func(pfx netip.Prefix, val V) bool) { return func(yield func(netip.Prefix, V) bool) { - _ = t.root6.allRec(zeroPath, 0, false, yield) + _ = t.root6.allRec(stridePath{}, 0, false, yield) } } // AllSorted returns an iterator over key-value pairs from Table2 in natural CIDR sort order. func (t *Table[V]) AllSorted() func(yield func(pfx netip.Prefix, val V) bool) { return func(yield func(netip.Prefix, V) bool) { - _ = t.root4.allRecSorted(zeroPath, 0, true, yield) && - t.root6.allRecSorted(zeroPath, 0, false, yield) + _ = t.root4.allRecSorted(stridePath{}, 0, true, yield) && + t.root6.allRecSorted(stridePath{}, 0, false, yield) } } // AllSorted4, like [Table.AllSorted] but only for the v4 routing table. func (t *Table[V]) AllSorted4() func(yield func(pfx netip.Prefix, val V) bool) { return func(yield func(netip.Prefix, V) bool) { - _ = t.root4.allRecSorted(zeroPath, 0, true, yield) + _ = t.root4.allRecSorted(stridePath{}, 0, true, yield) } } // AllSorted6, like [Table.AllSorted] but only for the v6 routing table. func (t *Table[V]) AllSorted6() func(yield func(pfx netip.Prefix, val V) bool) { return func(yield func(netip.Prefix, V) bool) { - _ = t.root6.allRecSorted(zeroPath, 0, false, yield) + _ = t.root6.allRecSorted(stridePath{}, 0, false, yield) } } @@ -816,3 +844,22 @@ func lastOctetIdxAndBits(bits int) (lastIdx, lastBits int) { return } + +// noCopy may be added to structs which must not be copied +// after the first use. +// +// type My struct { +// _ noCopy +// A state +// b foo +// } +// +// See https://golang.org/issues/8005#issuecomment-190753527 +// for details. +// +// Note that it must not be embedded, due to the Lock and Unlock methods. +type noCopy struct{} + +// Lock is a no-op used by -copylocks checker from `go vet`. +func (*noCopy) Lock() {} +func (*noCopy) Unlock() {} diff --git a/table_cb_test.go b/table_cb_test.go index 8c91907..0c93d7a 100644 --- a/table_cb_test.go +++ b/table_cb_test.go @@ -62,7 +62,7 @@ func TestSupernetsEdgeCaseCB(t *testing.T) { func TestSupernetsCompareCB(t *testing.T) { t.Parallel() - pfxs := gimmeRandomPrefixes(10_000) + pfxs := randomRealWorldPrefixes(10_000) fast := new(Table[int]) gold := new(goldTable[int]) @@ -75,7 +75,7 @@ func TestSupernetsCompareCB(t *testing.T) { tests := randomPrefixes(200) for _, tt := range tests { gotGold := gold.supernets(tt.pfx) - gotFast := []netip.Prefix{} + var gotFast []netip.Prefix fast.Supernets(tt.pfx)(func(p netip.Prefix, _ int) bool { gotFast = append(gotFast, p) @@ -137,10 +137,10 @@ func TestSubnetsCB(t *testing.T) { want6 := 105_555 rtbl := new(Table[int]) - for i, pfx := range gimmeRandomPrefixes4(want4) { + for i, pfx := range randomRealWorldPrefixes4(want4) { rtbl.Insert(pfx, i) } - for i, pfx := range gimmeRandomPrefixes6(want6) { + for i, pfx := range randomRealWorldPrefixes6(want6) { rtbl.Insert(pfx, i) } @@ -172,7 +172,7 @@ func TestSubnetsCB(t *testing.T) { func TestSubnetsCompareCB(t *testing.T) { t.Parallel() - pfxs := gimmeRandomPrefixes(10_000) + pfxs := randomRealWorldPrefixes(10_000) fast := new(Table[int]) gold := new(goldTable[int]) @@ -380,7 +380,7 @@ func BenchmarkSubnetsCB(b *testing.B) { n := 1_000_000 rtbl := new(Table[int]) - for i, pfx := range gimmeRandomPrefixes(n) { + for i, pfx := range randomRealWorldPrefixes(n) { rtbl.Insert(pfx, i) } @@ -399,7 +399,7 @@ func BenchmarkSupernetsCB(b *testing.B) { n := 1_000_000 rtbl := new(Table[int]) - for i, pfx := range gimmeRandomPrefixes(n) { + for i, pfx := range randomRealWorldPrefixes(n) { rtbl.Insert(pfx, i) } diff --git a/table_iter_test.go b/table_iter_test.go index a968e7e..c937bea 100644 --- a/table_iter_test.go +++ b/table_iter_test.go @@ -366,7 +366,7 @@ func TestSupernetsEdgeCase(t *testing.T) { func TestSupernetsCompare(t *testing.T) { t.Parallel() - pfxs := gimmeRandomPrefixes(10_000) + pfxs := randomRealWorldPrefixes(10_000) fast := new(Table[int]) gold := new(goldTable[int]) @@ -437,10 +437,10 @@ func TestSubnets(t *testing.T) { want6 := 105_555 rtbl := new(Table[int]) - for i, pfx := range gimmeRandomPrefixes4(want4) { + for i, pfx := range randomRealWorldPrefixes4(want4) { rtbl.Insert(pfx, i) } - for i, pfx := range gimmeRandomPrefixes6(want6) { + for i, pfx := range randomRealWorldPrefixes6(want6) { rtbl.Insert(pfx, i) } @@ -470,7 +470,7 @@ func TestSubnets(t *testing.T) { func TestSubnetsCompare(t *testing.T) { t.Parallel() - pfxs := gimmeRandomPrefixes(10_000) + pfxs := randomRealWorldPrefixes(10_000) fast := new(Table[int]) gold := new(goldTable[int]) @@ -514,7 +514,7 @@ func BenchmarkSubnets(b *testing.B) { n := 1_000_000 rtbl := new(Table[int]) - for i, pfx := range gimmeRandomPrefixes(n) { + for i, pfx := range randomRealWorldPrefixes(n) { rtbl.Insert(pfx, i) } @@ -533,7 +533,7 @@ func BenchmarkSupernets(b *testing.B) { n := 1_000_000 rtbl := new(Table[int]) - for i, pfx := range gimmeRandomPrefixes(n) { + for i, pfx := range randomRealWorldPrefixes(n) { rtbl.Insert(pfx, i) } diff --git a/table_test.go b/table_test.go index 55a4f0d..6bcbf1e 100644 --- a/table_test.go +++ b/table_test.go @@ -1821,7 +1821,7 @@ func BenchmarkTableInsertRandom(b *testing.B) { var startMem, endMem runtime.MemStats for _, n := range []int{10_000, 100_000, 1_000_000, 2_000_000} { - randomPfxs := gimmeRandomPrefixes(n) + randomPfxs := randomRealWorldPrefixes(n) var rt Table[struct{}] From e7f447592596022c0007f9e3add39011c170ea6d Mon Sep 17 00:00:00 2001 From: Karl Gaissmaier Date: Sun, 9 Feb 2025 14:09:31 +0100 Subject: [PATCH 09/10] fix: overlap bug, slipped in with path compression --- overlaps.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/overlaps.go b/overlaps.go index 76207ab..f0ce66f 100644 --- a/overlaps.go +++ b/overlaps.go @@ -199,7 +199,7 @@ func (n *node[V]) overlapsSameChildren(o *node[V], depth int) bool { func overlapsTwoChilds[V any](nChild, oChild any, depth int) bool { // 4 possible different combinations for n and o // - // node, node --> overlapsRec, increment depth + // node, node --> overlapsRec // node, leaf --> overlapsPrefixAtDepth // leaf, node --> overlapsPrefixAtDepth // leaf, leaf --> netip.Prefix.Overlaps @@ -209,7 +209,7 @@ func overlapsTwoChilds[V any](nChild, oChild any, depth int) bool { case *node[V]: switch oKind := oChild.(type) { case *node[V]: // node, node - return nKind.overlaps(oKind, depth+1) + return nKind.overlaps(oKind, depth) case *leaf[V]: // node, leaf return nKind.overlapsPrefixAtDepth(oKind.prefix, depth) } From 19621fbe006af4da6ebad46b20e239ad059dbe87 Mon Sep 17 00:00:00 2001 From: Karl Gaissmaier Date: Sun, 9 Feb 2025 14:20:08 +0100 Subject: [PATCH 10/10] update README --- README.md | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/README.md b/README.md index 22def59..0cd10f6 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ comparable or even better lookup times for longest prefix match. The algorithm is also excellent for determining whether two tables contain overlapping IP addresses. -All this happens within nanoseconds without memory allocation. ## Example @@ -171,38 +170,6 @@ PASS ok github.com/gaissmai/bart 10.455s ``` -and Overlaps with randomly generated tables of different size: -``` -goos: linux -goarch: amd64 -pkg: github.com/gaissmai/bart -cpu: Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz -BenchmarkFullTableOverlapsV4/With____1 9086344 123.5 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV4/With____2 68859405 17.27 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV4/With____4 68697332 17.29 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV4/With____8 6341209 189.8 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV4/With___16 5453186 221.0 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV4/With___32 58935297 20.47 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV4/With___64 43856942 27.76 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV4/With__128 42872038 27.63 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV4/With__256 42910443 27.62 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV4/With__512 126998767 9.362 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV4/With_1024 128460864 9.363 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV6/With____1 146886393 8.216 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV6/With____2 146285103 8.183 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV6/With____4 18488910 64.98 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV6/With____8 144183597 8.258 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV6/With___16 14775404 80.97 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV6/With___32 21450390 55.98 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV6/With___64 23702264 51.76 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV6/With__128 22386841 53.63 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV6/With__256 22390033 54.09 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV6/With__512 22338945 53.57 ns/op 0 B/op 0 allocs/op -BenchmarkFullTableOverlapsV6/With_1024 22369528 53.67 ns/op 0 B/op 0 allocs/op -PASS -ok github.com/gaissmai/bart 48.594s -``` - ## Compatibility Guarantees The package is currently released as a pre-v1 version, which gives the author the freedom to break