Skip to content

Commit 3be7559

Browse files
Ivan-Pokhabovfujita
authored andcommitted
perf(table): bitmap-based fast path for standard BGP CommunitySet
Replace the regexp-per-community hot loop with a precomputed bitmap index. At policy compile time each pattern is classified: - communityMatchExact: single uint32 equality check - communityMatchFixedASWildcard: uint16 AS-only check (any local) - communityMatchFixedASBitmap: AS check + 8 KB per-AS bitmap built by running the regexp against all 65536 local-admin values once - communityMatchLocalIndependent: bit-test on low 16 bits, any AS (e.g. ^[0-9]*:300$) - communityMatchRegexp: last-resort full regexp (rare) At evaluation time the MATCH_OPTION_ANY / INVERT fast path merges all per-AS bitmaps into anyBitmaps and all AS-independent patterns into anyLocalIndependentBitmap. Checking a path with N communities costs O(N × distinct_AS) integer ops with zero allocations. Also remove redundant copy in GetCommunities / GetExtCommunities / GetLargeCommunities (return the stored slice directly). Also fix ParseExtCommunity guard condition: the inner `len(elems) < 1` sub-expression was always false (strings.SplitN never returns an empty slice), making the whole guard dead code.
1 parent ace3800 commit 3be7559

5 files changed

Lines changed: 697 additions & 15 deletions

File tree

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
package table
2+
3+
import (
4+
"fmt"
5+
"net/netip"
6+
"os"
7+
"regexp"
8+
"testing"
9+
"text/tabwriter"
10+
"time"
11+
12+
"github.com/osrg/gobgp/v4/pkg/config/oc"
13+
"github.com/osrg/gobgp/v4/pkg/packet/bgp"
14+
)
15+
16+
// createPathWithCommunities builds a path that carries the given community values.
17+
func createPathWithCommunities(communities []uint32) *Path {
18+
p := netip.MustParsePrefix("10.0.0.0/24")
19+
nlri, _ := bgp.NewIPAddrPrefix(p)
20+
nexthop, _ := bgp.NewPathAttributeNextHop(netip.MustParseAddr("10.0.0.1"))
21+
commAttr := bgp.NewPathAttributeCommunities(communities)
22+
attrs := []bgp.PathAttributeInterface{
23+
bgp.NewPathAttributeOrigin(0),
24+
nexthop,
25+
commAttr,
26+
}
27+
return NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
28+
}
29+
30+
// communityChartCase describes one benchmark scenario for community matching.
31+
type communityChartCase struct {
32+
// Bench is a short b.Run() sub-name (ASCII, no spaces) so console output stays narrow.
33+
Bench string
34+
Name string // human-readable; used in failure messages
35+
Patterns []string
36+
Regexps []string
37+
WantHit bool
38+
}
39+
40+
func communityChartCases() []communityChartCase {
41+
return []communityChartCase{
42+
{
43+
Bench: "exact_1",
44+
Name: "Exact / 1 pattern / match",
45+
Patterns: []string{"65000:100"},
46+
Regexps: []string{"^65000:100$"},
47+
WantHit: true,
48+
},
49+
{
50+
Bench: "exact_10_last",
51+
Name: "Exact / 10 patterns / last matches",
52+
Patterns: func() []string {
53+
s := make([]string, 10)
54+
for i := range 9 {
55+
s[i] = fmt.Sprintf("65099:%d", i)
56+
}
57+
s[9] = "65002:999"
58+
return s
59+
}(),
60+
Regexps: func() []string {
61+
s := make([]string, 10)
62+
for i := range 9 {
63+
s[i] = fmt.Sprintf("^65099:%d$", i)
64+
}
65+
s[9] = "^65002:999$"
66+
return s
67+
}(),
68+
WantHit: true,
69+
},
70+
{
71+
Bench: "exact_10_none",
72+
Name: "Exact / 10 patterns / no match",
73+
Patterns: func() []string {
74+
s := make([]string, 10)
75+
for i := range 10 {
76+
s[i] = fmt.Sprintf("65099:%d", i)
77+
}
78+
return s
79+
}(),
80+
Regexps: func() []string {
81+
s := make([]string, 10)
82+
for i := range 10 {
83+
s[i] = fmt.Sprintf("^65099:%d$", i)
84+
}
85+
return s
86+
}(),
87+
WantHit: false,
88+
},
89+
{
90+
Bench: "wildcard_yes",
91+
Name: "Wildcard regexp / match",
92+
Patterns: []string{"^65000:.*$"},
93+
Regexps: []string{"^65000:.*$"},
94+
WantHit: true,
95+
},
96+
{
97+
Bench: "wildcard_no",
98+
Name: "Wildcard regexp / no match",
99+
Patterns: []string{"^65099:.*$"},
100+
Regexps: []string{"^65099:.*$"},
101+
WantHit: false,
102+
},
103+
{
104+
Bench: "mixed_miss_then_hit",
105+
Name: "Mixed: regex(miss) + exact(hit)",
106+
Patterns: []string{"^65099:.*$", "65001:300"},
107+
Regexps: []string{"^65099:.*$", "^65001:300$"},
108+
WantHit: true,
109+
},
110+
{
111+
Bench: "local_star_100",
112+
Name: "Local-independent / ^[0-9]*:local / match",
113+
Patterns: []string{"^[0-9]*:100$"},
114+
Regexps: []string{"^[0-9]*:100$"},
115+
WantHit: true,
116+
},
117+
{
118+
Bench: "local_star_alt",
119+
Name: "Local-independent / ^[0-9]*:(a|b) / match",
120+
Patterns: []string{"^[0-9]*:(999|888)$"},
121+
Regexps: []string{"^[0-9]*:(999|888)$"},
122+
WantHit: true,
123+
},
124+
{
125+
Bench: "local_star_miss",
126+
Name: "Local-independent / no match",
127+
Patterns: []string{"^[0-9]*:42$"},
128+
Regexps: []string{"^[0-9]*:42$"},
129+
WantHit: false,
130+
},
131+
{
132+
Bench: "local_dplus",
133+
Name: "Local-independent / \\d+ form / match",
134+
Patterns: []string{`^\d+:300$`},
135+
Regexps: []string{`^\d+:300$`},
136+
WantHit: true,
137+
},
138+
}
139+
}
140+
141+
func communityBenchmarkPath() *Path {
142+
return createPathWithCommunities([]uint32{
143+
65000<<16 | 100,
144+
65000<<16 | 200,
145+
65001<<16 | 100,
146+
65001<<16 | 300,
147+
65002<<16 | 999,
148+
})
149+
}
150+
151+
// communityMatchLegacyLoop is the naive fmt.Sprintf + regexp baseline (not semantically identical to MATCH_OPTION_ANY).
152+
func communityMatchLegacyLoop(path *Path, regs []*regexp.Regexp) {
153+
cs := path.GetCommunities()
154+
for _, x := range regs {
155+
for _, y := range cs {
156+
if x.MatchString(fmt.Sprintf("%d:%d", y>>16, y&0xffff)) {
157+
break
158+
}
159+
}
160+
}
161+
}
162+
163+
// BenchmarkCommunityCondition runs New and Legacy side by side per scenario (names like exact_1/New, exact_1/Legacy).
164+
//
165+
// go test ./internal/pkg/table/ -run '^$' -bench 'BenchmarkCommunityCondition' -benchmem -count=5
166+
func BenchmarkCommunityCondition(b *testing.B) {
167+
path := communityBenchmarkPath()
168+
for _, sc := range communityChartCases() {
169+
b.Run(sc.Bench+"/New", func(b *testing.B) {
170+
cs, err := NewCommunitySet(oc.CommunitySet{
171+
CommunitySetName: "bench",
172+
CommunityList: sc.Patterns,
173+
})
174+
if err != nil {
175+
b.Fatal(err)
176+
}
177+
cond := &CommunityCondition{set: cs, option: MATCH_OPTION_ANY}
178+
if got := cond.Evaluate(path, nil); got != sc.WantHit {
179+
b.Fatalf("%s: expected match=%v got %v", sc.Name, sc.WantHit, got)
180+
}
181+
b.ResetTimer()
182+
for range b.N {
183+
cond.Evaluate(path, nil)
184+
}
185+
})
186+
b.Run(sc.Bench+"/Legacy", func(b *testing.B) {
187+
regs := make([]*regexp.Regexp, len(sc.Regexps))
188+
for i, p := range sc.Regexps {
189+
regs[i] = regexp.MustCompile(p)
190+
}
191+
b.ResetTimer()
192+
for range b.N {
193+
communityMatchLegacyLoop(path, regs)
194+
}
195+
})
196+
}
197+
}
198+
199+
func TestCommunityConditionCompareSummary(t *testing.T) {
200+
if os.Getenv("GOBGP_COMMUNITY_BENCH_COMPARE") != "1" {
201+
t.Skip(`set GOBGP_COMMUNITY_BENCH_COMPARE=1 to print New vs Legacy ns/op and speedup (legacy/new)`)
202+
}
203+
path := communityBenchmarkPath()
204+
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
205+
_, err := fmt.Fprintln(tw, "bench\tnew ns/op\tlegacy ns/op\tlegacy/new\tname")
206+
if err != nil {
207+
t.Fatal(err)
208+
}
209+
_, err = fmt.Fprintln(tw, "-----\t---------\t-----------\t--------\t----")
210+
if err != nil {
211+
t.Fatal(err)
212+
}
213+
for _, sc := range communityChartCases() {
214+
rNew := testing.Benchmark(func(b *testing.B) {
215+
cs, err := NewCommunitySet(oc.CommunitySet{
216+
CommunitySetName: "bench",
217+
CommunityList: sc.Patterns,
218+
})
219+
if err != nil {
220+
b.Fatal(err)
221+
}
222+
cond := &CommunityCondition{set: cs, option: MATCH_OPTION_ANY}
223+
if got := cond.Evaluate(path, nil); got != sc.WantHit {
224+
b.Fatalf("%s: expected match=%v got %v", sc.Name, sc.WantHit, got)
225+
}
226+
b.ResetTimer()
227+
for range b.N {
228+
cond.Evaluate(path, nil)
229+
}
230+
})
231+
regs := make([]*regexp.Regexp, len(sc.Regexps))
232+
for i, p := range sc.Regexps {
233+
regs[i] = regexp.MustCompile(p)
234+
}
235+
rLeg := testing.Benchmark(func(b *testing.B) {
236+
b.ResetTimer()
237+
for range b.N {
238+
communityMatchLegacyLoop(path, regs)
239+
}
240+
})
241+
newNs := float64(rNew.NsPerOp())
242+
legNs := float64(rLeg.NsPerOp())
243+
ratio := legNs / newNs
244+
_, err := fmt.Fprintf(tw, "%s\t%.0f\t%.0f\t%.2fx\t%s\n", sc.Bench, newNs, legNs, ratio, sc.Name)
245+
if err != nil {
246+
t.Fatal(err)
247+
}
248+
}
249+
_ = tw.Flush()
250+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package table
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"testing"
7+
8+
"github.com/osrg/gobgp/v4/pkg/config/oc"
9+
)
10+
11+
// legacyEvaluate is the reference implementation of community matching using standard regexp.
12+
func legacyEvaluate(cs []uint32, regs []*regexp.Regexp, option MatchOption) bool {
13+
if len(regs) == 0 {
14+
return false
15+
}
16+
result := false
17+
for _, x := range regs {
18+
result = false
19+
for _, y := range cs {
20+
if x.MatchString(fmt.Sprintf("%d:%d", y>>16, y&0xffff)) {
21+
result = true
22+
break
23+
}
24+
}
25+
if option == MATCH_OPTION_ALL && !result {
26+
break
27+
}
28+
if (option == MATCH_OPTION_ANY || option == MATCH_OPTION_INVERT) && result {
29+
break
30+
}
31+
}
32+
if option == MATCH_OPTION_INVERT {
33+
result = !result
34+
}
35+
return result
36+
}
37+
38+
func FuzzCommunityCondition(f *testing.F) {
39+
// Add some seed corpus
40+
f.Add("65000:100", uint32(65000<<16|100), uint8(MATCH_OPTION_ANY))
41+
f.Add("^65000:.*$", uint32(65000<<16|200), uint8(MATCH_OPTION_ANY))
42+
f.Add("^[0-9]*:100$", uint32(65001<<16|100), uint8(MATCH_OPTION_ANY))
43+
f.Add(`^\d+:300$`, uint32(65001<<16|300), uint8(MATCH_OPTION_ANY))
44+
f.Add("65000:100", uint32(65001<<16|100), uint8(MATCH_OPTION_ANY))
45+
f.Add("65000:100", uint32(65000<<16|100), uint8(MATCH_OPTION_INVERT))
46+
f.Add("65000:100", uint32(65000<<16|100), uint8(MATCH_OPTION_ALL))
47+
48+
f.Fuzz(func(t *testing.T, pattern string, comm uint32, opt uint8) {
49+
option := MatchOption(opt % 3) // 0: ANY, 1: ALL, 2: INVERT
50+
51+
// Map 0, 1, 2 to the actual constants if they differ
52+
switch option {
53+
case 0:
54+
option = MATCH_OPTION_ANY
55+
case 1:
56+
option = MATCH_OPTION_ALL
57+
case 2:
58+
option = MATCH_OPTION_INVERT
59+
}
60+
61+
// Try to compile the pattern as a regexp. If it fails, we skip this input.
62+
// We use ParseCommunityRegexp to handle the ^ and $ additions if needed,
63+
// but to be safe we just use ParseCommunityRegexp logic or just regexp.Compile.
64+
// Actually, NewCommunitySet does some parsing. Let's see if it errors.
65+
cs, err := NewCommunitySet(oc.CommunitySet{
66+
CommunitySetName: "fuzz",
67+
CommunityList: []string{pattern},
68+
})
69+
if err != nil {
70+
// Invalid pattern, skip
71+
return
72+
}
73+
74+
// For legacy, we need to mimic how ParseCommunityRegexp transforms the pattern.
75+
// ParseCommunityRegexp in gobgp usually adds ^ and $ if not present, but let's just use the compiled regexp from cs if possible, or compile it ourselves.
76+
// Wait, NewCommunitySet parses it. Let's just use the exact same regexp string that gobgp uses.
77+
// Actually, if we just compile the pattern directly, it might not match exactly what gobgp does (e.g., exact match vs partial).
78+
// Let's look at ParseCommunityRegexp.
79+
exp, err := ParseCommunityRegexp(pattern)
80+
if err != nil {
81+
return
82+
}
83+
reg := exp
84+
85+
cond := &CommunityCondition{set: cs, option: option}
86+
path := createPathWithCommunities([]uint32{comm})
87+
88+
newResult := cond.Evaluate(path, nil)
89+
legacyResult := legacyEvaluate([]uint32{comm}, []*regexp.Regexp{reg}, option)
90+
91+
if newResult != legacyResult {
92+
t.Errorf("Mismatch for pattern %q, comm %d, option %v: new=%v, legacy=%v", pattern, comm, option, newResult, legacyResult)
93+
}
94+
})
95+
}

internal/pkg/table/path.go

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -829,12 +829,10 @@ func (path *Path) ReplaceAS(localAS, peerAS uint32) *Path {
829829
}
830830

831831
func (path *Path) GetCommunities() []uint32 {
832-
communityList := []uint32{}
833832
if attr := path.getPathAttr(bgp.BGP_ATTR_TYPE_COMMUNITIES); attr != nil {
834-
communities := attr.(*bgp.PathAttributeCommunities)
835-
communityList = append(communityList, communities.Value...)
833+
return attr.(*bgp.PathAttributeCommunities).Value
836834
}
837-
return communityList
835+
return nil
838836
}
839837

840838
// SetCommunities adds or replaces communities with new ones.
@@ -899,12 +897,10 @@ func (path *Path) RemoveCommunities(communities []uint32) int {
899897
}
900898

901899
func (path *Path) GetExtCommunities() []bgp.ExtendedCommunityInterface {
902-
eCommunityList := make([]bgp.ExtendedCommunityInterface, 0)
903900
if attr := path.getPathAttr(bgp.BGP_ATTR_TYPE_EXTENDED_COMMUNITIES); attr != nil {
904-
eCommunities := attr.(*bgp.PathAttributeExtendedCommunities).Value
905-
eCommunityList = append(eCommunityList, eCommunities...)
901+
return attr.(*bgp.PathAttributeExtendedCommunities).Value
906902
}
907-
return eCommunityList
903+
return nil
908904
}
909905

910906
func (path *Path) SetExtCommunities(exts []bgp.ExtendedCommunityInterface, doReplace bool) {
@@ -934,10 +930,7 @@ func (path *Path) GetRouteTargets() []bgp.ExtendedCommunityInterface {
934930

935931
func (path *Path) GetLargeCommunities() []*bgp.LargeCommunity {
936932
if a := path.getPathAttr(bgp.BGP_ATTR_TYPE_LARGE_COMMUNITY); a != nil {
937-
v := a.(*bgp.PathAttributeLargeCommunities).Values
938-
ret := make([]*bgp.LargeCommunity, 0, len(v))
939-
ret = append(ret, v...)
940-
return ret
933+
return a.(*bgp.PathAttributeLargeCommunities).Values
941934
}
942935
return nil
943936
}

0 commit comments

Comments
 (0)