labels: Reduce allocated memory by Hash method in edge cases; Added tests.

Old:
/tmp/___BenchmarkLabels_Hash_in_github_com_prometheus_prometheus_pkg_labels -test.v -test.bench ^\QBenchmarkLabels_Hash\E$ -test.run ^$
goos: linux
goarch: amd64
pkg: github.com/prometheus/prometheus/pkg/labels
BenchmarkLabels_Hash
BenchmarkLabels_Hash/typical_labels_under_1KB
BenchmarkLabels_Hash/typical_labels_under_1KB-12         	 5366161	       259 ns/op	       0 B/op	       0 allocs/op
BenchmarkLabels_Hash/bigger_labels_over_1KB
BenchmarkLabels_Hash/bigger_labels_over_1KB-12           	 1700371	       767 ns/op	    2048 B/op	       1 allocs/op
BenchmarkLabels_Hash/extremely_large_label_value_10MB
BenchmarkLabels_Hash/extremely_large_label_value_10MB-12 	     356	   3743115 ns/op	10523442 B/op	       1 allocs/op
PASS

New:
/tmp/___BenchmarkLabels_Hash_in_github_com_prometheus_prometheus_pkg_labels -test.v -test.bench ^\QBenchmarkLabels_Hash\E$ -test.run ^$
goos: linux
goarch: amd64
pkg: github.com/prometheus/prometheus/pkg/labels
BenchmarkLabels_Hash
BenchmarkLabels_Hash/typical_labels_under_1KB
BenchmarkLabels_Hash/typical_labels_under_1KB-12         	 4758883	       259 ns/op	       0 B/op	       0 allocs/op
BenchmarkLabels_Hash/bigger_labels_over_1KB
BenchmarkLabels_Hash/bigger_labels_over_1KB-12           	 3324492	       357 ns/op	      80 B/op	       1 allocs/op
BenchmarkLabels_Hash/extremely_large_label_value_10MB
BenchmarkLabels_Hash/extremely_large_label_value_10MB-12 	    1087	   1083949 ns/op	    9734 B/op	       1 allocs/op
PASS

Process finished with exit code 0

Signed-off-by: Bartlomiej Plotka <bwplotka@gmail.com>
This commit is contained in:
Bartlomiej Plotka 2020-10-07 21:14:51 +02:00
parent 7e2db3d092
commit 61160463cc
2 changed files with 104 additions and 1 deletions

View file

@ -18,6 +18,7 @@ import (
"encoding/json"
"sort"
"strconv"
"unsafe"
"github.com/cespare/xxhash"
)
@ -33,6 +34,8 @@ const (
labelSep = '\xfe'
)
var seps = []byte{'\xff'}
// Label is a key/value pair of strings.
type Label struct {
Name, Value string
@ -132,11 +135,44 @@ func (ls Labels) MatchLabels(on bool, names ...string) Labels {
return matchedLabels
}
// noescape hides a pointer from escape analysis. noescape is
// the identity function but escape analysis doesn't think the
// output depends on the input. noescape is inlined and currently
// compiles down to zero instructions.
// USE CAREFULLY!
// This was copied from the runtime; see issues 23382 and 7921.
//go:nosplit
//go:nocheckptr
func noescape(p unsafe.Pointer) unsafe.Pointer {
x := uintptr(p)
return unsafe.Pointer(x ^ 0) //nolint:staticcheck
}
//go:nosplit
//go:checkptr
func noAllocBytes(buf string) []byte {
return *(*[]byte)(unsafe.Pointer(&buf))
}
// Hash returns a hash value for the label set.
func (ls Labels) Hash() uint64 {
b := make([]byte, 0, 1024)
for i, v := range ls {
if len(b)+len(v.Name)+len(v.Value)+2 >= cap(b) {
// If labels entry is 1KB+ allocate do not allocate whole entry.
h := xxhash.New()
// This allows b to be still on stack and reused. This is safe as xxhash.x
// is never used outside of this function.
_, _ = h.Write(*(*[]byte)(noescape(unsafe.Pointer(&b))))
for _, v := range ls[i:] {
_, _ = h.Write(noAllocBytes(v.Name))
_, _ = h.Write(seps)
_, _ = h.Write(noAllocBytes(v.Value))
_, _ = h.Write(seps)
}
return h.Sum64()
}
for _, v := range ls {
b = append(b, v.Name...)
b = append(b, sep)
b = append(b, v.Value...)

View file

@ -14,6 +14,8 @@
package labels
import (
"fmt"
"strings"
"testing"
"github.com/prometheus/prometheus/util/testutil"
@ -629,3 +631,68 @@ func TestBuilder_Labels(t *testing.T) {
}).Labels(),
)
}
func TestLabels_Hash(t *testing.T) {
lbls := Labels{
{Name: "foo", Value: "bar"},
{Name: "baz", Value: "qux"},
}
testutil.Equals(t, lbls.Hash(), lbls.Hash())
testutil.Assert(t, lbls.Hash() != Labels{lbls[1], lbls[0]}.Hash(), "unordered labels match.")
testutil.Assert(t, lbls.Hash() != Labels{lbls[0]}.Hash(), "different labels match.")
}
var benchmarkLabelsResult uint64
func BenchmarkLabels_Hash(b *testing.B) {
for _, tcase := range []struct {
name string
lbls Labels
}{
{
name: "typical labels under 1KB",
lbls: func() Labels {
lbls := make(Labels, 10)
for i := 0; i < len(lbls); i++ {
// Label ~20B name, 50B value.
lbls[i] = Label{Name: fmt.Sprintf("abcdefghijabcdefghijabcdefghij%d", i), Value: fmt.Sprintf("abcdefghijabcdefghijabcdefghijabcdefghijabcdefghij%d", i)}
}
return lbls
}(),
},
{
name: "bigger labels over 1KB",
lbls: func() Labels {
lbls := make(Labels, 10)
for i := 0; i < len(lbls); i++ {
//Label ~50B name, 50B value.
lbls[i] = Label{Name: fmt.Sprintf("abcdefghijabcdefghijabcdefghijabcdefghijabcdefghij%d", i), Value: fmt.Sprintf("abcdefghijabcdefghijabcdefghijabcdefghijabcdefghij%d", i)}
}
return lbls
}(),
},
{
name: "extremely large label value 10MB",
lbls: func() Labels {
lbl := &strings.Builder{}
lbl.Grow(1024 * 1024 * 10) // 10MB.
word := "abcdefghij"
for i := 0; i < lbl.Cap()/len(word); i++ {
_, _ = lbl.WriteString(word)
}
return Labels{{Name: "__name__", Value: lbl.String()}}
}(),
},
} {
b.Run(tcase.name, func(b *testing.B) {
var h uint64
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
h = tcase.lbls.Hash()
}
benchmarkLabelsResult = h
})
}
}