diff --git a/go/types/objectpath/objectpath.go b/go/types/objectpath/objectpath.go index 6646bf55089..a3e1b3a32a3 100644 --- a/go/types/objectpath/objectpath.go +++ b/go/types/objectpath/objectpath.go @@ -28,11 +28,30 @@ import ( "go/types" "strconv" "strings" + "sync" "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typesinternal" ) +// pathBufPool reduces allocations by reusing path buffers. +var pathBufPool = sync.Pool{ + New: func() interface{} { + buf := make([]byte, 0, 128) + return &buf + }, +} + +// smallInts contains pre-computed string representations of integers 0-99 +// to avoid strconv.AppendInt allocations for common method/field indices. +var smallInts [100]string + +func init() { + for i := range smallInts { + smallInts[i] = strconv.Itoa(i) + } +} + // TODO(adonovan): think about generic aliases. // A Path is an opaque name that identifies a types.Object @@ -268,7 +287,12 @@ func (enc *Encoder) For(obj types.Object) (Path, error) { // In the presence of path aliases, these give // the best paths because non-types may // refer to types, but not the reverse. - empty := make([]byte, 0, 48) // initial space + + // Get a buffer from the pool to reduce allocations. + bufPtr := pathBufPool.Get().(*[]byte) + empty := (*bufPtr)[:0] + defer pathBufPool.Put(bufPtr) + objs := enc.scopeObjects(scope) for _, o := range objs { tname, ok := o.(*types.TypeName) @@ -344,7 +368,12 @@ func (enc *Encoder) For(obj types.Object) (Path, error) { func appendOpArg(path []byte, op byte, arg int) []byte { path = append(path, op) - path = strconv.AppendInt(path, int64(arg), 10) + // Use pre-computed strings for small integers to avoid allocations. + if arg >= 0 && arg < len(smallInts) { + path = append(path, smallInts[arg]...) + } else { + path = strconv.AppendInt(path, int64(arg), 10) + } return path } @@ -443,6 +472,16 @@ func (enc *Encoder) concreteMethod(meth *types.Func) (Path, bool) { // panic(fmt.Sprintf("couldn't find method %s on type %s; methods: %#v", meth, named, enc.namedMethods(named))) } +// finderPool reduces allocations by reusing finder structs. +var finderPool = sync.Pool{ + New: func() interface{} { + return &finder{ + seenTParamNames: make(map[*types.TypeName]bool), + seenMethods: make(map[*types.Func]bool), + } + }, +} + // find finds obj within type T, returning the path to it, or nil if not found. // // The seen map is used to short circuit cycles through type parameters. If @@ -455,7 +494,14 @@ func (enc *Encoder) concreteMethod(meth *types.Func) (Path, bool) { // // See golang/go#68046 for details. func find(obj types.Object, T types.Type, path []byte) []byte { - return (&finder{obj: obj}).find(T, path) + f := finderPool.Get().(*finder) + f.obj = obj + // Clear maps but keep allocated backing storage + clear(f.seenTParamNames) + clear(f.seenMethods) + result := f.find(T, path) + finderPool.Put(f) + return result } // finder closes over search state for a call to find. @@ -561,7 +607,13 @@ func (f *finder) find(T types.Type, path []byte) []byte { } func findTypeParam(obj types.Object, list *types.TypeParamList, path []byte, op byte) []byte { - return (&finder{obj: obj}).findTypeParam(list, path, op) + f := finderPool.Get().(*finder) + f.obj = obj + clear(f.seenTParamNames) + clear(f.seenMethods) + result := f.findTypeParam(list, path, op) + finderPool.Put(f) + return result } func (f *finder) findTypeParam(list *types.TypeParamList, path []byte, op byte) []byte { diff --git a/go/types/objectpath/objectpath_bench_test.go b/go/types/objectpath/objectpath_bench_test.go new file mode 100644 index 00000000000..6491430b6e1 --- /dev/null +++ b/go/types/objectpath/objectpath_bench_test.go @@ -0,0 +1,188 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package objectpath_test + +import ( + "go/ast" + "go/build" + "go/importer" + "go/parser" + "go/token" + "go/types" + "path/filepath" + "testing" + + "golang.org/x/tools/go/types/objectpath" +) + +// testData holds pre-loaded type information for benchmarks. +var testData struct { + pkg *types.Package + objects []types.Object + methods []*types.Func + fields []*types.Var + paths []objectpath.Path +} + +func init() { + // Load net/http for realistic benchmarks - it has interfaces, + // structs with many fields, and methods. + pkg, err := build.Default.Import("net/http", "", 0) + if err != nil { + panic("failed to import net/http: " + err.Error()) + } + + fset := token.NewFileSet() + var files []*ast.File + for _, filename := range pkg.GoFiles { + f, err := parser.ParseFile(fset, filepath.Join(pkg.Dir, filename), nil, 0) + if err != nil { + panic("failed to parse: " + err.Error()) + } + files = append(files, f) + } + + conf := types.Config{Importer: importer.Default()} + tpkg, err := conf.Check("net/http", fset, files, nil) + if err != nil { + panic("failed to type-check: " + err.Error()) + } + + testData.pkg = tpkg + scope := tpkg.Scope() + + // Collect diverse objects for comprehensive benchmarking + for _, name := range scope.Names() { + obj := scope.Lookup(name) + testData.objects = append(testData.objects, obj) + + // Collect methods from named types + if named, ok := obj.Type().(*types.Named); ok { + for i := 0; i < named.NumMethods(); i++ { + m := named.Method(i) + testData.methods = append(testData.methods, m) + testData.objects = append(testData.objects, m) + } + + // Collect fields from struct types + if st, ok := named.Underlying().(*types.Struct); ok { + for i := 0; i < st.NumFields(); i++ { + f := st.Field(i) + testData.fields = append(testData.fields, f) + testData.objects = append(testData.objects, f) + } + } + } + } + + // Pre-encode paths for decode benchmarks + enc := new(objectpath.Encoder) + for _, obj := range testData.objects { + if path, err := enc.For(obj); err == nil { + testData.paths = append(testData.paths, path) + } + } +} + +// BenchmarkEncoderFor measures the cost of encoding object paths. +func BenchmarkEncoderFor(b *testing.B) { + if len(testData.objects) == 0 { + b.Skip("no test objects available") + } + for b.Loop() { + enc := new(objectpath.Encoder) + for _, obj := range testData.objects { + _, _ = enc.For(obj) + } + } +} + +// BenchmarkEncoderFor_SingleEncoder measures encoding with encoder reuse. +func BenchmarkEncoderFor_SingleEncoder(b *testing.B) { + if len(testData.objects) == 0 { + b.Skip("no test objects available") + } + enc := new(objectpath.Encoder) + for b.Loop() { + for _, obj := range testData.objects { + _, _ = enc.For(obj) + } + } +} + +// BenchmarkEncoderFor_Methods focuses on method path encoding. +func BenchmarkEncoderFor_Methods(b *testing.B) { + if len(testData.methods) == 0 { + b.Skip("no methods available") + } + for b.Loop() { + enc := new(objectpath.Encoder) + for _, m := range testData.methods { + _, _ = enc.For(m) + } + } +} + +// BenchmarkEncoderFor_Fields focuses on struct field path encoding. +func BenchmarkEncoderFor_Fields(b *testing.B) { + if len(testData.fields) == 0 { + b.Skip("no fields available") + } + for b.Loop() { + enc := new(objectpath.Encoder) + for _, f := range testData.fields { + _, _ = enc.For(f) + } + } +} + +// BenchmarkObject measures decoding paths back to objects. +func BenchmarkObject(b *testing.B) { + if len(testData.paths) == 0 { + b.Skip("no paths available") + } + for b.Loop() { + for _, path := range testData.paths { + _, _ = objectpath.Object(testData.pkg, path) + } + } +} + +// BenchmarkRoundTrip measures encode + decode cycles. +func BenchmarkRoundTrip(b *testing.B) { + if len(testData.objects) == 0 { + b.Skip("no test objects available") + } + for b.Loop() { + enc := new(objectpath.Encoder) + for _, obj := range testData.objects { + path, err := enc.For(obj) + if err != nil { + continue + } + _, _ = objectpath.Object(testData.pkg, path) + } + } +} + +// BenchmarkEncoderFor_Repeated measures many sequential encoding calls. +func BenchmarkEncoderFor_Repeated(b *testing.B) { + if len(testData.objects) == 0 { + b.Skip("no test objects available") + } + // Use a subset to get more iterations + objects := testData.objects + if len(objects) > 100 { + objects = objects[:100] + } + for b.Loop() { + enc := new(objectpath.Encoder) + for j := 0; j < 10; j++ { + for _, obj := range objects { + _, _ = enc.For(obj) + } + } + } +}