]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
hreflect: Cache reflect method lookups used in collections.Where and others
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 26 Oct 2025 16:45:57 +0000 (17:45 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 27 Oct 2025 14:45:50 +0000 (15:45 +0100)
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)
````

common/hreflect/helpers.go
common/hreflect/helpers_test.go
langs/i18n/i18n.go
tpl/collections/where.go

index a0a5c0382a89ca555dc8fe0dd4771669ed13ba35..4b70167d53352549acbd96410fda1e4fe259acc3 100644 (file)
@@ -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 {
index cd1826ec32fd960e8efddfecbdeb928d74583fab..6a6e7b26b378f576b30c35cb4563690a7801d382 100644 (file)
@@ -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"}
index e97ec8b8d64f18e3e6d99c2a5d46188c56461053..db27fb997e788b23f0aaa9b543b66bc81fc23af1 100644 (file)
@@ -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() {
index 5726cc8ce88f141bd0e2b1dcf1cd3f617631241d..9a45ba12f22ca47632c93a79b27e7eaa0bd20184 100644 (file)
@@ -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))
                }