Skip to content

Commit f98fce2

Browse files
committed
go/types/objectpath: add encoder type, to amortize allocation
This change adds a new encoder type with For method, that is functionally equivalent to the old For function but avoids the surprising cost of repeated calls to Scope.Names and canonicalize (now called namedMethods), both of which allocate and sorts a slice. The former canonicalize function was previously applied to interface types too, but this was costly and unnecessary as they are already sorted. The public API change will have to wait for proposal 58668 to be accepted and thus fix issue 51017; this change uses the linkname hack to expose the function to x/tools to fix a pressing bug in gopls. See golang/go#58668 Updates golang/go#51017 Change-Id: Id3e8726517d31371b9376b0e47e320f8b6c5e085 Reviewed-on: https://go-review.googlesource.com/c/tools/+/470679 TryBot-Result: Gopher Robot <[email protected]> gopls-CI: kokoro <[email protected]> Run-TryBot: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent 2e10748 commit f98fce2

File tree

4 files changed

+93
-36
lines changed

4 files changed

+93
-36
lines changed

go/types/objectpath/objectpath.go

+76-34
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import (
3131
"strings"
3232

3333
"golang.org/x/tools/internal/typeparams"
34+
35+
_ "unsafe" // for go:linkname
3436
)
3537

3638
// A Path is an opaque name that identifies a types.Object
@@ -111,7 +113,7 @@ const (
111113
opObj = 'O' // .Obj() (Named, TypeParam)
112114
)
113115

114-
// The For function returns the path to an object relative to its package,
116+
// For returns the path to an object relative to its package,
115117
// or an error if the object is not accessible from the package's Scope.
116118
//
117119
// The For function guarantees to return a path only for the following objects:
@@ -144,6 +146,23 @@ const (
144146
//
145147
// where p is the package (*types.Package) to which X belongs.
146148
func For(obj types.Object) (Path, error) {
149+
return newEncoderFor()(obj)
150+
}
151+
152+
// An encoder amortizes the cost of encoding the paths of multiple objects.
153+
// Nonexported pending approval of proposal 58668.
154+
type encoder struct {
155+
scopeNamesMemo map[*types.Scope][]string // memoization of Scope.Names()
156+
namedMethodsMemo map[*types.Named][]*types.Func // memoization of namedMethods()
157+
}
158+
159+
// Exposed to gopls via golang.org/x/tools/internal/typesinternal
160+
// pending approval of proposal 58668.
161+
//
162+
//go:linkname newEncoderFor
163+
func newEncoderFor() func(types.Object) (Path, error) { return new(encoder).For }
164+
165+
func (enc *encoder) For(obj types.Object) (Path, error) {
147166
pkg := obj.Pkg()
148167

149168
// This table lists the cases of interest.
@@ -225,7 +244,7 @@ func For(obj types.Object) (Path, error) {
225244
return "", fmt.Errorf("func is not a method: %v", obj)
226245
}
227246

228-
if path, ok := concreteMethod(obj); ok {
247+
if path, ok := enc.concreteMethod(obj); ok {
229248
// Fast path for concrete methods that avoids looping over scope.
230249
return path, nil
231250
}
@@ -241,7 +260,7 @@ func For(obj types.Object) (Path, error) {
241260
// the best paths because non-types may
242261
// refer to types, but not the reverse.
243262
empty := make([]byte, 0, 48) // initial space
244-
names := scope.Names()
263+
names := enc.scopeNames(scope)
245264
for _, name := range names {
246265
o := scope.Lookup(name)
247266
tname, ok := o.(*types.TypeName)
@@ -294,9 +313,7 @@ func For(obj types.Object) (Path, error) {
294313
// Note that method index here is always with respect
295314
// to canonical ordering of methods, regardless of how
296315
// they appear in the underlying type.
297-
canonical := canonicalize(T)
298-
for i := 0; i < len(canonical); i++ {
299-
m := canonical[i]
316+
for i, m := range enc.namedMethods(T) {
300317
path2 := appendOpArg(path, opMethod, i)
301318
if m == obj {
302319
return Path(path2), nil // found declared method
@@ -324,7 +341,7 @@ func appendOpArg(path []byte, op byte, arg int) []byte {
324341
// This function is just an optimization that avoids the general scope walking
325342
// approach. You are expected to fall back to the general approach if this
326343
// function fails.
327-
func concreteMethod(meth *types.Func) (Path, bool) {
344+
func (enc *encoder) concreteMethod(meth *types.Func) (Path, bool) {
328345
// Concrete methods can only be declared on package-scoped named types. For
329346
// that reason we can skip the expensive walk over the package scope: the
330347
// path will always be package -> named type -> method. We can trivially get
@@ -397,8 +414,7 @@ func concreteMethod(meth *types.Func) (Path, bool) {
397414
path := make([]byte, 0, len(name)+8)
398415
path = append(path, name...)
399416
path = append(path, opType)
400-
canonical := canonicalize(named)
401-
for i, m := range canonical {
417+
for i, m := range enc.namedMethods(named) {
402418
if m == meth {
403419
path = appendOpArg(path, opMethod, i)
404420
return Path(path), true
@@ -663,15 +679,23 @@ func Object(pkg *types.Package, p Path) (types.Object, error) {
663679
t = nil
664680

665681
case opMethod:
666-
hasMethods, ok := t.(hasMethods) // Interface or Named
667-
if !ok {
682+
switch t := t.(type) {
683+
case *types.Interface:
684+
if index >= t.NumMethods() {
685+
return nil, fmt.Errorf("method index %d out of range [0-%d)", index, t.NumMethods())
686+
}
687+
obj = t.Method(index) // Id-ordered
688+
689+
case *types.Named:
690+
methods := namedMethods(t) // (unmemoized)
691+
if index >= len(methods) {
692+
return nil, fmt.Errorf("method index %d out of range [0-%d)", index, len(methods))
693+
}
694+
obj = methods[index] // Id-ordered
695+
696+
default:
668697
return nil, fmt.Errorf("cannot apply %q to %s (got %T, want interface or named)", code, t, t)
669698
}
670-
canonical := canonicalize(hasMethods)
671-
if n := len(canonical); index >= n {
672-
return nil, fmt.Errorf("method index %d out of range [0-%d)", index, n)
673-
}
674-
obj = canonical[index]
675699
t = nil
676700

677701
case opObj:
@@ -694,27 +718,45 @@ func Object(pkg *types.Package, p Path) (types.Object, error) {
694718
return obj, nil // success
695719
}
696720

697-
// hasMethods is an abstraction of *types.{Interface,Named}. This is pulled up
698-
// because it is used by methodOrdering, which is in turn used by both encoding
699-
// and decoding.
700-
type hasMethods interface {
701-
Method(int) *types.Func
702-
NumMethods() int
721+
// namedMethods returns the methods of a Named type in ascending Id order.
722+
func namedMethods(named *types.Named) []*types.Func {
723+
methods := make([]*types.Func, named.NumMethods())
724+
for i := range methods {
725+
methods[i] = named.Method(i)
726+
}
727+
sort.Slice(methods, func(i, j int) bool {
728+
return methods[i].Id() < methods[j].Id()
729+
})
730+
return methods
703731
}
704732

705-
// canonicalize returns a canonical order for the methods in a hasMethod.
706-
func canonicalize(hm hasMethods) []*types.Func {
707-
count := hm.NumMethods()
708-
if count <= 0 {
709-
return nil
733+
// scopeNames is a memoization of scope.Names. Callers must not modify the result.
734+
func (enc *encoder) scopeNames(scope *types.Scope) []string {
735+
m := enc.scopeNamesMemo
736+
if m == nil {
737+
m = make(map[*types.Scope][]string)
738+
enc.scopeNamesMemo = m
739+
}
740+
names, ok := m[scope]
741+
if !ok {
742+
names = scope.Names() // allocates and sorts
743+
m[scope] = names
710744
}
711-
canon := make([]*types.Func, count)
712-
for i := 0; i < count; i++ {
713-
canon[i] = hm.Method(i)
745+
return names
746+
}
747+
748+
// namedMethods is a memoization of the namedMethods function. Callers must not modify the result.
749+
func (enc *encoder) namedMethods(named *types.Named) []*types.Func {
750+
m := enc.namedMethodsMemo
751+
if m == nil {
752+
m = make(map[*types.Named][]*types.Func)
753+
enc.namedMethodsMemo = m
714754
}
715-
less := func(i, j int) bool {
716-
return canon[i].Id() < canon[j].Id()
755+
methods, ok := m[named]
756+
if !ok {
757+
methods = namedMethods(named) // allocates and sorts
758+
m[named] = methods
717759
}
718-
sort.Slice(canon, less)
719-
return canon
760+
return methods
761+
720762
}

gopls/internal/lsp/source/methodsets/methodsets.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import (
5454
"golang.org/x/tools/go/types/objectpath"
5555
"golang.org/x/tools/gopls/internal/lsp/safetoken"
5656
"golang.org/x/tools/internal/typeparams"
57+
"golang.org/x/tools/internal/typesinternal"
5758
)
5859

5960
// An Index records the non-empty method sets of all package-level
@@ -202,6 +203,8 @@ func (b *indexBuilder) build(fset *token.FileSet, pkg *types.Package) *Index {
202203
return gobPosition{b.string(posn.Filename), posn.Offset, len(obj.Name())}
203204
}
204205

206+
objectpathFor := typesinternal.NewObjectpathFunc()
207+
205208
// setindexInfo sets the (Posn, PkgPath, ObjectPath) fields for each method declaration.
206209
setIndexInfo := func(m *gobMethod, method *types.Func) {
207210
// error.Error has empty Position, PkgPath, and ObjectPath.
@@ -214,7 +217,7 @@ func (b *indexBuilder) build(fset *token.FileSet, pkg *types.Package) *Index {
214217

215218
// Instantiations of generic methods don't have an
216219
// object path, so we use the generic.
217-
if p, err := objectpath.For(typeparams.OriginMethod(method)); err != nil {
220+
if p, err := objectpathFor(typeparams.OriginMethod(method)); err != nil {
218221
panic(err) // can't happen for a method of a package-level type
219222
} else {
220223
m.ObjectPath = b.string(string(p))

gopls/internal/lsp/source/xrefs/xrefs.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"golang.org/x/tools/go/types/objectpath"
2020
"golang.org/x/tools/gopls/internal/lsp/protocol"
2121
"golang.org/x/tools/gopls/internal/lsp/source"
22+
"golang.org/x/tools/internal/typesinternal"
2223
)
2324

2425
// Index constructs a serializable index of outbound cross-references
@@ -41,6 +42,8 @@ func Index(files []*source.ParsedGoFile, pkg *types.Package, info *types.Info) [
4142
return objects
4243
}
4344

45+
objectpathFor := typesinternal.NewObjectpathFunc()
46+
4447
for fileIndex, pgf := range files {
4548

4649
nodeRange := func(n ast.Node) protocol.Range {
@@ -65,7 +68,7 @@ func Index(files []*source.ParsedGoFile, pkg *types.Package, info *types.Info) [
6568
objects := getObjects(obj.Pkg())
6669
gobObj, ok := objects[obj]
6770
if !ok {
68-
path, err := objectpath.For(obj)
71+
path, err := objectpathFor(obj)
6972
if err != nil {
7073
// Capitalized but not exported
7174
// (e.g. local const/var/type).

internal/typesinternal/types.go

+9
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"go/types"
1212
"reflect"
1313
"unsafe"
14+
15+
"golang.org/x/tools/go/types/objectpath"
1416
)
1517

1618
func SetUsesCgo(conf *types.Config) bool {
@@ -50,3 +52,10 @@ func ReadGo116ErrorData(err types.Error) (code ErrorCode, start, end token.Pos,
5052
}
5153

5254
var SetGoVersion = func(conf *types.Config, version string) bool { return false }
55+
56+
// NewObjectpathEncoder returns a function closure equivalent to
57+
// objectpath.For but amortized for multiple (sequential) calls.
58+
// It is a temporary workaround, pending the approval of proposal 58668.
59+
//
60+
//go:linkname NewObjectpathFunc golang.org/x/tools/go/types/objectpath.newEncoderFor
61+
func NewObjectpathFunc() func(types.Object) (objectpath.Path, error)

0 commit comments

Comments
 (0)