Merge pull request #510 from grafana/cherrypick-stringlabels

Cherrypick stringlabels optimisations
This commit is contained in:
Bryan Boreham 2023-07-04 11:38:15 +01:00 committed by GitHub
commit 3a61decb3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 117 additions and 53 deletions

View file

@ -273,13 +273,27 @@ func (ls Labels) Copy() Labels {
// Get returns the value for the label with the given name.
// Returns an empty string if the label doesn't exist.
func (ls Labels) Get(name string) string {
if name == "" { // Avoid crash in loop if someone asks for "".
return "" // Prometheus does not store blank label names.
}
for i := 0; i < len(ls.data); {
var lName, lValue string
lName, i = decodeString(ls.data, i)
lValue, i = decodeString(ls.data, i)
if lName == name {
return lValue
var size int
size, i = decodeSize(ls.data, i)
if ls.data[i] == name[0] {
lName := ls.data[i : i+size]
i += size
if lName == name {
lValue, _ := decodeString(ls.data, i)
return lValue
}
} else {
if ls.data[i] > name[0] { // Stop looking if we've gone past.
break
}
i += size
}
size, i = decodeSize(ls.data, i)
i += size
}
return ""
}
@ -422,37 +436,49 @@ func FromStrings(ss ...string) Labels {
// Compare compares the two label sets.
// The result will be 0 if a==b, <0 if a < b, and >0 if a > b.
// TODO: replace with Less function - Compare is never needed.
// TODO: just compare the underlying strings when we don't need alphanumeric sorting.
func Compare(a, b Labels) int {
l := len(a.data)
if len(b.data) < l {
l = len(b.data)
// Find the first byte in the string where a and b differ.
shorter, longer := a.data, b.data
if len(b.data) < len(a.data) {
shorter, longer = b.data, a.data
}
i := 0
// First, go 8 bytes at a time. Data strings are expected to be 8-byte aligned.
sp := unsafe.Pointer((*reflect.StringHeader)(unsafe.Pointer(&shorter)).Data)
lp := unsafe.Pointer((*reflect.StringHeader)(unsafe.Pointer(&longer)).Data)
for ; i < len(shorter)-8; i += 8 {
if *(*uint64)(unsafe.Add(sp, i)) != *(*uint64)(unsafe.Add(lp, i)) {
break
}
}
// Now go 1 byte at a time.
for ; i < len(shorter); i++ {
if shorter[i] != longer[i] {
break
}
}
if i == len(shorter) {
// One Labels was a prefix of the other; the set with fewer labels compares lower.
return len(a.data) - len(b.data)
}
ia, ib := 0, 0
for ia < l {
var aName, bName string
aName, ia = decodeString(a.data, ia)
bName, ib = decodeString(b.data, ib)
if aName != bName {
if aName < bName {
return -1
}
return 1
}
var aValue, bValue string
aValue, ia = decodeString(a.data, ia)
bValue, ib = decodeString(b.data, ib)
if aValue != bValue {
if aValue < bValue {
return -1
}
return 1
// Now we know that there is some difference before the end of a and b.
// Go back through the fields and find which field that difference is in.
firstCharDifferent := i
for i = 0; ; {
size, nextI := decodeSize(a.data, i)
if nextI+size > firstCharDifferent {
break
}
i = nextI + size
}
// If all labels so far were in common, the set with fewer labels comes first.
return len(a.data) - len(b.data)
// Difference is inside this entry.
aStr, _ := decodeString(a.data, i)
bStr, _ := decodeString(b.data, i)
if aStr < bStr {
return -1
}
return +1
}
// Copy labels from b on top of whatever was in ls previously, reusing memory or expanding if needed.

View file

@ -361,6 +361,18 @@ func TestLabels_Compare(t *testing.T) {
"bbc", "222"),
expected: -1,
},
{
compared: FromStrings(
"aaa", "111",
"bb", "222"),
expected: 1,
},
{
compared: FromStrings(
"aaa", "111",
"bbbb", "222"),
expected: -1,
},
{
compared: FromStrings(
"aaa", "111"),
@ -380,6 +392,10 @@ func TestLabels_Compare(t *testing.T) {
"bbb", "222"),
expected: 0,
},
{
compared: EmptyLabels(),
expected: 1,
},
}
sign := func(a int) int {
@ -395,6 +411,8 @@ func TestLabels_Compare(t *testing.T) {
for i, test := range tests {
got := Compare(labels, test.compared)
require.Equal(t, sign(test.expected), sign(got), "unexpected comparison result for test case %d", i)
got = Compare(test.compared, labels)
require.Equal(t, -sign(test.expected), sign(got), "unexpected comparison result for reverse test case %d", i)
}
}
@ -425,7 +443,8 @@ func TestLabels_Has(t *testing.T) {
func TestLabels_Get(t *testing.T) {
require.Equal(t, "", FromStrings("aaa", "111", "bbb", "222").Get("foo"))
require.Equal(t, "111", FromStrings("aaa", "111", "bbb", "222").Get("aaa"))
require.Equal(t, "111", FromStrings("aaaa", "111", "bbb", "222").Get("aaaa"))
require.Equal(t, "222", FromStrings("aaaa", "111", "bbb", "222").Get("bbb"))
}
// BenchmarkLabels_Get was written to check whether a binary search can improve the performance vs the linear search implementation
@ -445,7 +464,7 @@ func BenchmarkLabels_Get(b *testing.B) {
maxLabels := 30
allLabels := make([]Label, maxLabels)
for i := 0; i < maxLabels; i++ {
allLabels[i] = Label{Name: strings.Repeat(string('a'+byte(i)), 5)}
allLabels[i] = Label{Name: strings.Repeat(string('a'+byte(i)), 5+(i%5))}
}
for _, size := range []int{5, 10, maxLabels} {
b.Run(fmt.Sprintf("with %d labels", size), func(b *testing.B) {
@ -456,6 +475,7 @@ func BenchmarkLabels_Get(b *testing.B) {
{"get first label", allLabels[0].Name},
{"get middle label", allLabels[size/2].Name},
{"get last label", allLabels[size-1].Name},
{"get not-found label", "benchmark"},
} {
b.Run(scenario.desc, func(b *testing.B) {
b.ResetTimer()
@ -468,27 +488,34 @@ func BenchmarkLabels_Get(b *testing.B) {
}
}
var comparisonBenchmarkScenarios = []struct {
desc string
base, other Labels
}{
{
"equal",
FromStrings("a_label_name", "a_label_value", "another_label_name", "another_label_value"),
FromStrings("a_label_name", "a_label_value", "another_label_name", "another_label_value"),
},
{
"not equal",
FromStrings("a_label_name", "a_label_value", "another_label_name", "another_label_value"),
FromStrings("a_label_name", "a_label_value", "another_label_name", "a_different_label_value"),
},
{
"different sizes",
FromStrings("a_label_name", "a_label_value", "another_label_name", "another_label_value"),
FromStrings("a_label_name", "a_label_value"),
},
{
"lots",
FromStrings("aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii", "jjj", "kkk", "lll", "mmm", "nnn", "ooo", "ppp", "qqq", "rrz"),
FromStrings("aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii", "jjj", "kkk", "lll", "mmm", "nnn", "ooo", "ppp", "qqq", "rrr"),
},
}
func BenchmarkLabels_Equals(b *testing.B) {
for _, scenario := range []struct {
desc string
base, other Labels
}{
{
"equal",
FromStrings("a_label_name", "a_label_value", "another_label_name", "another_label_value"),
FromStrings("a_label_name", "a_label_value", "another_label_name", "another_label_value"),
},
{
"not equal",
FromStrings("a_label_name", "a_label_value", "another_label_name", "another_label_value"),
FromStrings("a_label_name", "a_label_value", "another_label_name", "a_different_label_value"),
},
{
"different sizes",
FromStrings("a_label_name", "a_label_value", "another_label_name", "another_label_value"),
FromStrings("a_label_name", "a_label_value"),
},
} {
for _, scenario := range comparisonBenchmarkScenarios {
b.Run(scenario.desc, func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
@ -498,6 +525,17 @@ func BenchmarkLabels_Equals(b *testing.B) {
}
}
func BenchmarkLabels_Compare(b *testing.B) {
for _, scenario := range comparisonBenchmarkScenarios {
b.Run(scenario.desc, func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Compare(scenario.base, scenario.other)
}
})
}
}
func TestLabels_Copy(t *testing.T) {
require.Equal(t, FromStrings("aaa", "111", "bbb", "222"), FromStrings("aaa", "111", "bbb", "222").Copy())
}