Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 56 additions & 4 deletions go/types/objectpath/objectpath.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
188 changes: 188 additions & 0 deletions go/types/objectpath/objectpath_bench_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}