From: Bjørn Erik Pedersen Date: Sun, 26 Oct 2025 16:45:57 +0000 (+0100) Subject: hreflect: Cache reflect method lookups used in collections.Where and others X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=e9bda21ce9d1ab80377044d8de1d7884142bfa14;p=brevno-suite%2Fhugo 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) ```` --- diff --git a/common/hreflect/helpers.go b/common/hreflect/helpers.go index a0a5c0382..4b70167d5 100644 --- a/common/hreflect/helpers.go +++ b/common/hreflect/helpers.go @@ -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) @@ -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) } @@ -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 @@ -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 { diff --git a/common/hreflect/helpers_test.go b/common/hreflect/helpers_test.go index cd1826ec3..6a6e7b26b 100644 --- a/common/hreflect/helpers_test.go +++ b/common/hreflect/helpers_test.go @@ -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"} diff --git a/langs/i18n/i18n.go b/langs/i18n/i18n.go index e97ec8b8d..db27fb997 100644 --- a/langs/i18n/i18n.go +++ b/langs/i18n/i18n.go @@ -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() { diff --git a/tpl/collections/where.go b/tpl/collections/where.go index 5726cc8ce..9a45ba12f 100644 --- a/tpl/collections/where.go +++ b/tpl/collections/where.go @@ -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 } @@ -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)) }