Avoid intermediate List allocations as items added to the list

Pick a reasonable middle ground between allocating larger chunks of
memory (2048 * ~500b for pod slices) and having many small allocations
as the list is resized by preallocating capacity based on the expected
list size. At worst, we'll allocate a 1M slice for pods and only add
a single pod to it (if the selector is very specific).
This commit is contained in:
Clayton Coleman 2017-10-09 22:16:13 -04:00
parent 6a76931e2c
commit ce0dc76901
No known key found for this signature in database
GPG Key ID: 3D16906B4F1C5CB3
2 changed files with 104 additions and 1 deletions

View File

@ -562,6 +562,14 @@ func (s *store) List(ctx context.Context, key, resourceVersion string, pred stor
return fmt.Errorf("no results were found, but etcd indicated there were more values remaining") return fmt.Errorf("no results were found, but etcd indicated there were more values remaining")
} }
// avoid small allocations for the result slice, since this can be called in many
// different contexts and we don't know how significantly the result will be filtered
if pred.Empty() {
growSlice(v, len(getResp.Kvs))
} else {
growSlice(v, 2048, len(getResp.Kvs))
}
// take items from the response until the bucket is full, filtering as we go // take items from the response until the bucket is full, filtering as we go
for _, kv := range getResp.Kvs { for _, kv := range getResp.Kvs {
if paging && int64(v.Len()) >= pred.Limit { if paging && int64(v.Len()) >= pred.Limit {
@ -612,6 +620,37 @@ func (s *store) List(ctx context.Context, key, resourceVersion string, pred stor
return s.versioner.UpdateList(listObj, uint64(returnedRV), "") return s.versioner.UpdateList(listObj, uint64(returnedRV), "")
} }
// growSlice takes a slice value and grows its capacity up
// to the maximum of the passed sizes or maxCapacity, whichever
// is smaller. Above maxCapacity decisions about allocation are left
// to the Go runtime on append. This allows a caller to make an
// educated guess about the potential size of the total list while
// still avoiding overly aggressive initial allocation. If sizes
// is empty maxCapacity will be used as the size to grow.
func growSlice(v reflect.Value, maxCapacity int, sizes ...int) {
cap := v.Cap()
max := cap
for _, size := range sizes {
if size > max {
max = size
}
}
if len(sizes) == 0 || max > maxCapacity {
max = maxCapacity
}
if max <= cap {
return
}
if v.Len() > 0 {
extra := reflect.MakeSlice(v.Type(), 0, max)
reflect.Copy(extra, v)
v.Set(extra)
} else {
extra := reflect.MakeSlice(v.Type(), 0, max)
v.Set(extra)
}
}
// Watch implements storage.Interface.Watch. // Watch implements storage.Interface.Watch.
func (s *store) Watch(ctx context.Context, key string, resourceVersion string, pred storage.SelectionPredicate) (watch.Interface, error) { func (s *store) Watch(ctx context.Context, key string, resourceVersion string, pred storage.SelectionPredicate) (watch.Interface, error) {
return s.watch(ctx, key, resourceVersion, pred, false) return s.watch(ctx, key, resourceVersion, pred, false)

View File

@ -31,7 +31,6 @@ import (
"github.com/coreos/etcd/integration" "github.com/coreos/etcd/integration"
"github.com/coreos/pkg/capnslog" "github.com/coreos/pkg/capnslog"
"golang.org/x/net/context" "golang.org/x/net/context"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
apitesting "k8s.io/apimachinery/pkg/api/testing" apitesting "k8s.io/apimachinery/pkg/api/testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -1236,3 +1235,68 @@ func Test_decodeContinue(t *testing.T) {
}) })
} }
} }
func Test_growSlice(t *testing.T) {
type args struct {
t reflect.Type
initialCapacity int
v reflect.Value
maxCapacity int
sizes []int
}
tests := []struct {
name string
args args
cap int
}{
{
name: "empty",
args: args{v: reflect.ValueOf([]example.Pod{})},
cap: 0,
},
{
name: "no sizes",
args: args{v: reflect.ValueOf([]example.Pod{}), maxCapacity: 10},
cap: 10,
},
{
name: "above maxCapacity",
args: args{v: reflect.ValueOf([]example.Pod{}), maxCapacity: 10, sizes: []int{1, 12}},
cap: 10,
},
{
name: "takes max",
args: args{v: reflect.ValueOf([]example.Pod{}), maxCapacity: 10, sizes: []int{8, 4}},
cap: 8,
},
{
name: "with existing capacity above max",
args: args{initialCapacity: 12, maxCapacity: 10, sizes: []int{8, 4}},
cap: 12,
},
{
name: "with existing capacity below max",
args: args{initialCapacity: 5, maxCapacity: 10, sizes: []int{8, 4}},
cap: 8,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.initialCapacity > 0 {
tt.args.v = reflect.ValueOf(make([]example.Pod, 0, tt.args.initialCapacity))
}
// reflection requires that the value be addressible in order to call set,
// so we must ensure the value we created is available on the heap (not a problem
// for normal usage)
if !tt.args.v.CanAddr() {
x := reflect.New(tt.args.v.Type())
x.Elem().Set(tt.args.v)
tt.args.v = x.Elem()
}
growSlice(tt.args.v, tt.args.maxCapacity, tt.args.sizes...)
if tt.cap != tt.args.v.Cap() {
t.Errorf("Unexpected capacity: got=%d want=%d", tt.args.v.Cap(), tt.cap)
}
})
}
}