Skip to content

Commit e9bda21

Browse files
committed
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) ````
1 parent 3893e70 commit e9bda21

File tree

4 files changed

+59
-15
lines changed

4 files changed

+59
-15
lines changed

‎common/hreflect/helpers.go‎

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,31 @@ type methodKey struct {
158158
name string
159159
}
160160

161-
var methodCache sync.Map
161+
var (
162+
methodIndexCache sync.Map
163+
methodCache sync.Map
164+
)
165+
166+
// GetMethodByNameForType returns the method with the given name for the given type,
167+
// or a zero Method if no such method exists.
168+
// It panics if tp is an interface type.
169+
// It caches the lookup.
170+
func GetMethodByNameForType(tp reflect.Type, name string) reflect.Method {
171+
if tp.Kind() == reflect.Interface {
172+
// Func field is nil for interface types.
173+
panic("not supported for interface types")
174+
}
175+
k := methodKey{tp, name}
176+
v, found := methodCache.Load(k)
177+
if found {
178+
return v.(reflect.Method)
179+
}
180+
m, _ := tp.MethodByName(name)
181+
methodCache.Store(k, m)
182+
return m
183+
}
162184

163-
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the
164-
// type lookup.
185+
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the lookup.
165186
func GetMethodByName(v reflect.Value, name string) reflect.Value {
166187
index := GetMethodIndexByName(v.Type(), name)
167188

@@ -176,7 +197,7 @@ func GetMethodByName(v reflect.Value, name string) reflect.Value {
176197
// -1 if no such method exists.
177198
func GetMethodIndexByName(tp reflect.Type, name string) int {
178199
k := methodKey{tp, name}
179-
v, found := methodCache.Load(k)
200+
v, found := methodIndexCache.Load(k)
180201
if found {
181202
return v.(int)
182203
}
@@ -185,7 +206,7 @@ func GetMethodIndexByName(tp reflect.Type, name string) int {
185206
if !ok {
186207
index = -1
187208
}
188-
methodCache.Store(k, index)
209+
methodIndexCache.Store(k, index)
189210

190211
if !ok {
191212
return -1
@@ -307,6 +328,18 @@ func Indirect(v reflect.Value) (vv reflect.Value, isNil bool) {
307328
return v, false
308329
}
309330

331+
// IndirectElem is like Indirect, but if the final value is a pointer, it unwraps it.
332+
func IndirectElem(v reflect.Value) (vv reflect.Value, isNil bool) {
333+
vv, isNil = Indirect(v)
334+
if isNil {
335+
return vv, isNil
336+
}
337+
if vv.Kind() == reflect.Pointer {
338+
vv = vv.Elem()
339+
}
340+
return vv, isNil
341+
}
342+
310343
// IsNil reports whether v is nil.
311344
// Based on reflect.Value.IsNil, but also considers invalid values as nil.
312345
func IsNil(v reflect.Value) bool {

‎common/hreflect/helpers_test.go‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,18 @@ func (t *testStruct) Method5() string {
223223
return "Hugo"
224224
}
225225

226+
func BenchmarkGetMethodByNameForType(b *testing.B) {
227+
tp := reflect.TypeFor[*testStruct]()
228+
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
229+
230+
b.ResetTimer()
231+
for i := 0; i < b.N; i++ {
232+
for _, method := range methods {
233+
_ = GetMethodByNameForType(tp, method)
234+
}
235+
}
236+
}
237+
226238
func BenchmarkGetMethodByName(b *testing.B) {
227239
v := reflect.ValueOf(&testStruct{})
228240
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}

‎langs/i18n/i18n.go‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,11 @@ func getPluralCount(v any) any {
158158
}
159159
}
160160
default:
161-
vv := reflect.Indirect(reflect.ValueOf(v))
162-
if vv.Kind() == reflect.Interface && !vv.IsNil() {
163-
vv = vv.Elem()
161+
vv, isNil := hreflect.IndirectElem(reflect.ValueOf(v))
162+
if isNil {
163+
return nil
164164
}
165165
tp := vv.Type()
166-
167166
if tp.Kind() == reflect.Struct {
168167
f := vv.FieldByName(countFieldName)
169168
if f.IsValid() {

‎tpl/collections/where.go‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -314,14 +314,14 @@ func evaluateSubElem(ctx, obj reflect.Value, elemName string) (reflect.Value, er
314314
objPtr = objPtr.Addr()
315315
}
316316

317-
index := hreflect.GetMethodIndexByName(objPtr.Type(), elemName)
318-
if index != -1 {
319-
var args []reflect.Value
320-
mt := objPtr.Type().Method(index)
317+
mt := hreflect.GetMethodByNameForType(objPtr.Type(), elemName)
318+
if mt.Func.IsValid() {
319+
// Receiver is the first argument.
320+
args := []reflect.Value{objPtr}
321321
num := mt.Type.NumIn()
322322
maxNumIn := 1
323323
if num > 1 && hreflect.IsContextType(mt.Type.In(1)) {
324-
args = []reflect.Value{ctx}
324+
args = append(args, ctx)
325325
maxNumIn = 2
326326
}
327327

@@ -339,7 +339,7 @@ func evaluateSubElem(ctx, obj reflect.Value, elemName string) (reflect.Value, er
339339
case mt.Type.NumOut() == 2 && !mt.Type.Out(1).Implements(errorType):
340340
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)
341341
}
342-
res := objPtr.Method(mt.Index).Call(args)
342+
res := mt.Func.Call(args)
343343
if len(res) == 2 && !res[1].IsNil() {
344344
return zero, fmt.Errorf("error at calling a method %s of type %s: %s", elemName, typ, res[1].Interface().(error))
345345
}

0 commit comments

Comments
 (0)