Skip to content

Commit

Permalink
labels/cidr: Cache GetCIDRLabels computation
Browse files Browse the repository at this point in the history
Cache the computation of intermediate CIDR labels to speed up
GetCIDRLabels and reduce memory usage by deduplicating CIDR strings.
Even though now most of the cost is in building up the resulting
"labels.Labels", it is not memoized yet as it is mutable and mutated by
e.g. MergeLabels.

Before:
goos: linux
goarch: amd64
pkg: github.com/cilium/cilium/pkg/labels/cidr
cpu: AMD Ryzen 9 5950X 16-Core Processor
BenchmarkGetCIDRLabels/0.0.0.0/0                 6005072               199.4 ns/op           640 B/op          3 allocs/op
BenchmarkGetCIDRLabels/10.16.0.0/16               402415              2876 ns/op            3748 B/op         38 allocs/op
BenchmarkGetCIDRLabels/192.0.2.3/32               216280              5457 ns/op            8032 B/op         70 allocs/op
BenchmarkGetCIDRLabels/192.0.2.3/24               285751              4113 ns/op            5056 B/op         54 allocs/op
BenchmarkGetCIDRLabels/192.0.2.0/24               286141              4116 ns/op            5055 B/op         54 allocs/op
BenchmarkGetCIDRLabels/::/0                      6016551               199.6 ns/op           640 B/op          3 allocs/op
BenchmarkGetCIDRLabels/fdff::ff/128                37502             31938 ns/op           30786 B/op        450 allocs/op
BenchmarkGetCIDRLabels/f00d:42::ff/128             35725             33607 ns/op           33658 B/op        450 allocs/op
BenchmarkGetCIDRLabels/f00d:42::ff/96              50270             23798 ns/op           20231 B/op        297 allocs/op

After:
goos: linux
goarch: amd64
pkg: github.com/cilium/cilium/pkg/labels/cidr
cpu: AMD Ryzen 9 5950X 16-Core Processor
BenchmarkGetCIDRLabels/0.0.0.0/0                 7320565               164.0 ns/op           624 B/op          2 allocs/op
BenchmarkGetCIDRLabels/10.16.0.0/16              1000000              1083 ns/op            2396 B/op          2 allocs/op
BenchmarkGetCIDRLabels/192.0.2.3/32               593683              1948 ns/op            5008 B/op          2 allocs/op
BenchmarkGetCIDRLabels/192.0.2.3/24               337100              3498 ns/op            7728 B/op          3 allocs/op
BenchmarkGetCIDRLabels/192.0.2.0/24               793645              1427 ns/op            2767 B/op          2 allocs/op
BenchmarkGetCIDRLabels/::/0                      7213646               166.1 ns/op           624 B/op          2 allocs/op
BenchmarkGetCIDRLabels/fdff::ff/128               168543              7064 ns/op           18515 B/op          3 allocs/op
BenchmarkGetCIDRLabels/f00d:42::ff/128            165129              7184 ns/op           18516 B/op          3 allocs/op
BenchmarkGetCIDRLabels/f00d:42::ff/96              91777             13056 ns/op           29283 B/op          6 allocs/op

Signed-off-by: Jussi Maki <jussi@isovalent.com>
Signed-off-by: Fabio Falzoi <fabio.falzoi@isovalent.com>
  • Loading branch information
joamaki authored and joestringer committed Oct 17, 2023
1 parent f44999d commit e0f6c47
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 26 deletions.
110 changes: 84 additions & 26 deletions pkg/labels/cidr/cidr.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/netip"
"strconv"
"strings"
"sync"

"github.com/cilium/cilium/pkg/labels"
"github.com/cilium/cilium/pkg/option"
Expand All @@ -18,7 +19,7 @@ import (
//
// For IPv6 addresses, it converts ":" into "-" as EndpointSelectors don't
// support colons inside the name section of a label.
func maskedIPToLabelString(ip netip.Addr, prefix int) string {
func maskedIPToLabel(ip netip.Addr, prefix int) labels.Label {
ipStr := ip.String()
ipNoColons := strings.Replace(ipStr, ":", "-", -1)

Expand All @@ -35,28 +36,24 @@ func maskedIPToLabelString(ip netip.Addr, prefix int) string {
}
var str strings.Builder
str.Grow(
len(labels.LabelSourceCIDR) +
len(preZero) +
len(preZero) +
len(ipNoColons) +
len(postZero) +
2 /*len of prefix*/ +
2, /* ':' '/' */
1, /* '/' */
)
str.WriteString(labels.LabelSourceCIDR)
str.WriteRune(':')
str.WriteString(preZero)
str.WriteString(ipNoColons)
str.WriteString(postZero)
str.WriteRune('/')
str.WriteString(strconv.Itoa(prefix))
return str.String()
return labels.Label{Key: str.String(), Source: labels.LabelSourceCIDR}
}

// IPStringToLabel parses a string and returns it as a CIDR label.
//
// If ip is not a valid IP address or CIDR Prefix, returns an error.
func IPStringToLabel(ip string) (labels.Label, error) {
var lblString string
// factored out of netip.ParsePrefix to avoid allocating an empty netip.Prefix in case it's
// an IP and not a CIDR.
i := strings.LastIndexByte(ip, '/')
Expand All @@ -65,15 +62,14 @@ func IPStringToLabel(ip string) (labels.Label, error) {
if err != nil {
return labels.Label{}, fmt.Errorf("%q is not an IP address: %w", ip, err)
}
lblString = maskedIPToLabelString(parsedIP, parsedIP.BitLen())
return maskedIPToLabel(parsedIP, parsedIP.BitLen()), nil
} else {
parsedPrefix, err := netip.ParsePrefix(ip)
if err != nil {
return labels.Label{}, fmt.Errorf("%q is not a CIDR: %w", ip, err)
}
lblString = maskedIPToLabelString(parsedPrefix.Masked().Addr(), parsedPrefix.Bits())
return maskedIPToLabel(parsedPrefix.Masked().Addr(), parsedPrefix.Bits()), nil
}
return labels.ParseLabel(lblString), nil
}

// GetCIDRLabels turns a CIDR into a set of labels representing the cidr itself
Expand All @@ -86,31 +82,93 @@ func IPStringToLabel(ip string) (labels.Label, error) {
//
// The identity reserved:world is always added as it includes any CIDR.
func GetCIDRLabels(prefix netip.Prefix) labels.Labels {
addr := prefix.Addr()
ones := prefix.Bits()
result := make([]string, 0, ones+2)
lbls := make(labels.Labels, 1 /* this CIDR */ +ones /* the prefixes */ +1 /*world label*/)

// If ones is zero, then it's the default CIDR prefix /0 which should
// just be regarded as reserved:world. In all other cases, we need
// to generate the set of prefixes starting from the /0 up to the
// specified prefix length.
if ones > 0 {
ip := prefix.Addr()
for i := 0; i <= ones; i++ {
p := netip.PrefixFrom(ip, i)
label := maskedIPToLabelString(p.Masked().Addr(), i)
result = append(result, label)
}
if ones == 0 {
addWorldLabel(addr, lbls)
return lbls
}

if option.Config.IsDualStack() {
if prefix.Addr().Is4() {
result = append(result, labels.LabelSourceReserved+":"+labels.IDNameWorldIPv4)
cache := cidrLabelsCache.Get().(map[netip.Prefix][]labels.Label)
computeCIDRLabels(
cache,
lbls,
nil, // avoid allocating space for the intermediate results until we need it
addr,
ones,
0,
)
cidrLabelsCache.Put(cache)
addWorldLabel(addr, lbls)

return lbls
}

// cidrLabelsCache stores the partial computations for CIDR labels.
// This both avoids repeatedly computing the prefixes and makes sure the
// CIDR strings are reused to reduce memory usage.
// Stored in a sync.Pool to allow GC to garbage collect the cache if needed.
// With lots of contention, multiple cache maps might exist.
//
// Stores e.g. for prefix "10.0.0.0/8" the labels ["10.0.0.0/8", ..., "0.0.0.0/0"].
var cidrLabelsCache = sync.Pool{
New: func() any { return make(map[netip.Prefix][]labels.Label) },
}

func addWorldLabel(addr netip.Addr, lbls labels.Labels) {
switch {
case !option.Config.IsDualStack():
lbls[worldLabelNonDualStack.Key] = worldLabelNonDualStack
case addr.Is4():
lbls[worldLabelV4.Key] = worldLabelV4
default:
lbls[worldLabelV6.Key] = worldLabelV6
}
}

var (
worldLabelNonDualStack = labels.Label{Key: labels.IDNameWorld, Source: labels.LabelSourceReserved}
worldLabelV4 = labels.Label{Source: labels.LabelSourceReserved, Key: labels.IDNameWorldIPv4}
worldLabelV6 = labels.Label{Source: labels.LabelSourceReserved, Key: labels.IDNameWorldIPv6}
)

func computeCIDRLabels(cache map[netip.Prefix][]labels.Label, lbls labels.Labels, results []labels.Label, addr netip.Addr, ones, i int) []labels.Label {
if i > ones {
return results
}

prefix := netip.PrefixFrom(addr, i)

if cachedLbls, ok := cache[prefix]; ok {
for _, lbl := range cachedLbls {
lbls[lbl.Key] = lbl
}
if results == nil {
return cachedLbls
} else {
result = append(result, labels.LabelSourceReserved+":"+labels.IDNameWorldIPv6)
return append(results, cachedLbls...)
}
} else {
result = append(result, labels.LabelSourceReserved+":"+labels.IDNameWorld)
}

return labels.NewLabelsFromModel(result)
// Compute the label for this prefix (e.g. "cidr:10.0.0.0/8")
prefixLabel := maskedIPToLabel(prefix.Masked().Addr(), i)
lbls[prefixLabel.Key] = prefixLabel

// Keep computing the rest (e.g. "cidr:10.0.0.0/7", ...).
results = computeCIDRLabels(
cache,
lbls,
append(results, prefixLabel),
addr, ones, i+1,
)
// Cache the resulting labels derived from this prefix, e.g. /8, /7, ...
cache[prefix] = results[i:]

return results
}
9 changes: 9 additions & 0 deletions pkg/labels/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,15 @@ func NewLabelsFromModel(base []string) Labels {
return lbls
}

// FromSlice creates labels from a slice of labels.
func FromSlice(labels []Label) Labels {
lbls := make(Labels, len(labels))
for _, lbl := range labels {
lbls[lbl.Key] = lbl
}
return lbls
}

// NewLabelsFromSortedList returns labels based on the output of SortedList()
func NewLabelsFromSortedList(list string) Labels {
return NewLabelsFromModel(strings.Split(list, ";"))
Expand Down

0 comments on commit e0f6c47

Please sign in to comment.