Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix ovrlaps bug #170

Merged
merged 10 commits into from
Feb 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 0 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
28 changes: 20 additions & 8 deletions dumper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)

Expand Down Expand Up @@ -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")
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand All @@ -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
Expand All @@ -272,6 +281,9 @@ func (n *node[V]) nodeStatsRec() stats {

case *leaf[V]:
s.leaves++

default:
panic("logic error, wrong node type")
}
}

Expand Down
45 changes: 45 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 44 additions & 18 deletions fulltable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
Expand All @@ -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)
}
})
}
Expand All @@ -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 {
Expand Down Expand Up @@ -466,29 +466,55 @@ 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
}
}
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)
Expand All @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions gold_stride_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
4 changes: 2 additions & 2 deletions gold_table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
28 changes: 22 additions & 6 deletions internal/bitset/bitset.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 7
// 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 (
Expand Down Expand Up @@ -73,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.
Expand Down
Loading
Loading