mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
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)
````
This commit is contained in:
@@ -158,10 +158,31 @@ type methodKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
var methodCache sync.Map
|
||||
var (
|
||||
methodIndexCache sync.Map
|
||||
methodCache sync.Map
|
||||
)
|
||||
|
||||
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the
|
||||
// type lookup.
|
||||
// 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 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 {
|
||||
|
||||
@@ -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"}
|
||||
|
||||
+3
-4
@@ -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() {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user