labels: faster Compare function when using -tags stringlabels (#12451)

Instead of unpacking every individual string, we skip to the point
where there is a difference, going 8 bytes at a time where possible.

Add benchmark for Compare; extend tests too.

---------

Signed-off-by: Bryan Boreham <bjboreham@gmail.com>
Co-authored-by: Oleg Zaytsev <mail@olegzaytsev.com>
This commit is contained in:
Bryan Boreham 2023-06-20 20:58:47 +01:00 committed by GitHub
parent 86a7064dcf
commit 87d08abe11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 94 additions and 46 deletions

View File

@ -422,37 +422,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)
}
}
@ -468,27 +486,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 +523,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())
}