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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
hreflect: Cache reflect method lookups used in collections.Where and …
…others

We already cached the method index on the struct, but caching the resolved `reflect.Method` itself saves us from having to do another lookup on each call, which is escpeciall import in the hot path used by collections.Where and otherrs:

```bash
                                        │ master.bench │    fix-reflectmethodcache.bench     │
                                        │    sec/op    │    sec/op     vs base               │
WhereSliceOfStructPointersWithMethod-10   592.2µ ± ∞ ¹   390.1µ ± ∞ ¹  -34.14% (p=0.029 n=4)
¹ need >= 6 samples for confidence interval at level 0.95

                                        │  master.bench  │     fix-reflectmethodcache.bench     │
                                        │      B/op      │     B/op       vs base               │
WhereSliceOfStructPointersWithMethod-10   205.14Ki ± ∞ ¹   64.52Ki ± ∞ ¹  -68.55% (p=0.029 n=4)
¹ need >= 6 samples for confidence interval at level 0.95

                                        │ master.bench │    fix-reflectmethodcache.bench     │
                                        │  allocs/op   │  allocs/op    vs base               │
WhereSliceOfStructPointersWithMethod-10   9.003k ± ∞ ¹   4.503k ± ∞ ¹  -49.98% (p=0.029 n=4)
````
  • Loading branch information
bep committed Oct 27, 2025
commit a554c3843fcf4e7eb654a291a863a21c824e5b89
43 changes: 38 additions & 5 deletions common/hreflect/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,31 @@ type methodKey struct {
name string
}

var methodCache sync.Map
var (
methodIndexCache sync.Map
methodCache sync.Map
)

// GetMethodByNameForType returns the method with the given name for the given type,
// or a zero Method if no such method exists.
// It panics if tp is an interface type.
// It caches the lookup.
func GetMethodByNameForType(tp reflect.Type, name string) reflect.Method {
if tp.Kind() == reflect.Interface {
// Func field is nil for interface types.
panic("not supported for interface types")
}
k := methodKey{tp, name}
v, found := methodCache.Load(k)
if found {
return v.(reflect.Method)
}
m, _ := tp.MethodByName(name)
methodCache.Store(k, m)
return m
}

// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the
// type lookup.
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the lookup.
func GetMethodByName(v reflect.Value, name string) reflect.Value {
index := GetMethodIndexByName(v.Type(), name)

Expand All @@ -176,7 +197,7 @@ func GetMethodByName(v reflect.Value, name string) reflect.Value {
// -1 if no such method exists.
func GetMethodIndexByName(tp reflect.Type, name string) int {
k := methodKey{tp, name}
v, found := methodCache.Load(k)
v, found := methodIndexCache.Load(k)
if found {
return v.(int)
}
Expand All @@ -185,7 +206,7 @@ func GetMethodIndexByName(tp reflect.Type, name string) int {
if !ok {
index = -1
}
methodCache.Store(k, index)
methodIndexCache.Store(k, index)

if !ok {
return -1
Expand Down Expand Up @@ -307,6 +328,18 @@ func Indirect(v reflect.Value) (vv reflect.Value, isNil bool) {
return v, false
}

// IndirectElem is like Indirect, but if the final value is a pointer, it unwraps it.
func IndirectElem(v reflect.Value) (vv reflect.Value, isNil bool) {
vv, isNil = Indirect(v)
if isNil {
return vv, isNil
}
if vv.Kind() == reflect.Pointer {
vv = vv.Elem()
}
return vv, isNil
}

// IsNil reports whether v is nil.
// Based on reflect.Value.IsNil, but also considers invalid values as nil.
func IsNil(v reflect.Value) bool {
Expand Down
12 changes: 12 additions & 0 deletions common/hreflect/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,18 @@ func (t *testStruct) Method5() string {
return "Hugo"
}

func BenchmarkGetMethodByNameForType(b *testing.B) {
tp := reflect.TypeFor[*testStruct]()
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}

b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, method := range methods {
_ = GetMethodByNameForType(tp, method)
}
}
}

func BenchmarkGetMethodByName(b *testing.B) {
v := reflect.ValueOf(&testStruct{})
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
Expand Down
7 changes: 3 additions & 4 deletions langs/i18n/i18n.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,11 @@ func getPluralCount(v any) any {
}
}
default:
vv := reflect.Indirect(reflect.ValueOf(v))
if vv.Kind() == reflect.Interface && !vv.IsNil() {
vv = vv.Elem()
vv, isNil := hreflect.IndirectElem(reflect.ValueOf(v))
if isNil {
return nil
}
tp := vv.Type()

if tp.Kind() == reflect.Struct {
f := vv.FieldByName(countFieldName)
if f.IsValid() {
Expand Down
12 changes: 6 additions & 6 deletions tpl/collections/where.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,14 +314,14 @@ func evaluateSubElem(ctx, obj reflect.Value, elemName string) (reflect.Value, er
objPtr = objPtr.Addr()
}

index := hreflect.GetMethodIndexByName(objPtr.Type(), elemName)
if index != -1 {
var args []reflect.Value
mt := objPtr.Type().Method(index)
mt := hreflect.GetMethodByNameForType(objPtr.Type(), elemName)
if mt.Func.IsValid() {
// Receiver is the first argument.
args := []reflect.Value{objPtr}
num := mt.Type.NumIn()
maxNumIn := 1
if num > 1 && hreflect.IsContextType(mt.Type.In(1)) {
args = []reflect.Value{ctx}
args = append(args, ctx)
maxNumIn = 2
}

Expand All @@ -339,7 +339,7 @@ func evaluateSubElem(ctx, obj reflect.Value, elemName string) (reflect.Value, er
case mt.Type.NumOut() == 2 && !mt.Type.Out(1).Implements(errorType):
return zero, fmt.Errorf("%s is a method of type %s returning two values but the second value is not an error type", elemName, typ)
}
res := objPtr.Method(mt.Index).Call(args)
res := mt.Func.Call(args)
if len(res) == 2 && !res[1].IsNil() {
return zero, fmt.Errorf("error at calling a method %s of type %s: %s", elemName, typ, res[1].Interface().(error))
}
Expand Down
Loading