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:
parent
86a7064dcf
commit
87d08abe11
|
@ -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.
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue