reflect.StructOf 即使命中 runtime 自己的类型缓存, 仍然要重新构造 []StructField、算签名、加锁查表。实测 12 字段的 struct: reflect.StructOf(相同字段, 已缓存) 2349 ns / 2549 B / 31 allocs reflect.TypeOf(已知类型) 2.3 ns / 0 B / 0 allocs 相差三个数量级。而 fields 在资源构造之后不再变化, 相同的 extends 必然得到 同一个类型, 完全可以缓存复用。 GetStruct/GetSliceStruct 抽出共用的 structType(), 结果按 extends 缓存: GetStruct() 1399 ns / 2352 B / 28 allocs -> 22.2 ns / 48 B / 1 alloc 63x GetStruct(extends) 1673 ns / 3592 B / 31 allocs -> 47.1 ns / 176 B / 2 allocs 36x GetSliceStruct() 1457 ns / 2376 B / 29 allocs -> 31.5 ns / 24 B / 1 alloc 46x 缓存用切片线性扫描而不是 map: 键是 []reflect.StructField, 要做 map key 得先 拼字符串, 那笔开销反而可能盖过 StructOf 本身; 而 extends 的取值组合来自关联 配置, 数量很少, 逐项比对最省。条目数设了上限, 超出后退回每次现算, 保证内存有界。 一个容易踩的坑: 缓存键会复制一份再存。调用方常把 extends 当临时缓冲区复用 (append 到同一个底层数组上), 直接持有会让键在之后被改写, 变成命中错误类型的 隐蔽 bug。TestGetStructExtendsAliasing 专门守这个。 行为不变: GetStruct 仍然每次返回新实例, 只是复用类型。 新增 4 个测试(类型稳定性/extends 隔离/别名安全/并发)与 3 个基准。
88 lines
2.6 KiB
Go
88 lines
2.6 KiB
Go
package resx
|
|
|
|
import (
|
|
"reflect"
|
|
"sync"
|
|
)
|
|
|
|
// 本文件为 GetStruct/GetSliceStruct 提供结果类型缓存。
|
|
//
|
|
// 动机: reflect.StructOf 即使命中 runtime 自己的类型缓存, 仍然要重新构造
|
|
// []StructField、算签名、加锁查表。实测 12 字段的 struct 约
|
|
//
|
|
// reflect.StructOf(相同字段, 已缓存) 2349 ns / 2549 B / 31 allocs
|
|
// reflect.TypeOf(已知类型) 2.3 ns / 0 B / 0 allocs
|
|
//
|
|
// 相差三个数量级。而资源的 fields 在构造之后不再变化, 相同的 extends 必然
|
|
// 得到同一个类型, 所以完全可以缓存下来复用。
|
|
|
|
// resStructCache 按 extends 缓存 GetStruct 的结果类型。
|
|
//
|
|
// 用切片线性扫描而不是 map: 键是 []reflect.StructField, 要做 map key 得先拼
|
|
// 字符串, 那笔开销反而可能盖过 StructOf 本身。而 extends 的取值组合来自关联
|
|
// 配置, 数量很少(通常个位数), 逐项比对最省 —— reflect.StructField 的
|
|
// Name/Type/Tag 三者都可比较。
|
|
type resStructCache struct {
|
|
mu sync.RWMutex
|
|
base reflect.Type // extends 为空时的类型(最常见的调用形态)
|
|
entries []resStructCacheEntry
|
|
}
|
|
|
|
type resStructCacheEntry struct {
|
|
extends []reflect.StructField
|
|
typ reflect.Type
|
|
}
|
|
|
|
// resStructCacheLimit 限制缓存条目数。正常配置下远远用不满; 万一调用方拿运行时
|
|
// 生成的字段做 extends, 超过上限后退回每次现算, 保证内存有界。
|
|
const resStructCacheLimit = 64
|
|
|
|
// sameExtends 判断两组 extends 是否等价。
|
|
func sameExtends(a, b []reflect.StructField) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i].Name != b[i].Name || a[i].Type != b[i].Type || a[i].Tag != b[i].Tag {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// load 查缓存, 未命中返回 nil。
|
|
func (c *resStructCache) load(extends []reflect.StructField) reflect.Type {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
if len(extends) == 0 {
|
|
return c.base
|
|
}
|
|
for i := range c.entries {
|
|
if sameExtends(c.entries[i].extends, extends) {
|
|
return c.entries[i].typ
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// store 写入缓存。
|
|
//
|
|
// extends 会复制一份再存: 调用方常把它当临时缓冲区复用(append 到同一个底层
|
|
// 数组上), 直接持有会让缓存键在之后被改写, 变成命中错误类型的隐蔽 bug。
|
|
func (c *resStructCache) store(extends []reflect.StructField, t reflect.Type) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if len(extends) == 0 {
|
|
c.base = t
|
|
return
|
|
}
|
|
if len(c.entries) >= resStructCacheLimit {
|
|
return
|
|
}
|
|
key := make([]reflect.StructField, len(extends))
|
|
copy(key, extends)
|
|
c.entries = append(c.entries, resStructCacheEntry{extends: key, typ: t})
|
|
}
|