perf: 缓存 GetStruct 构造的 struct 类型
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 个基准。
This commit is contained in:
@@ -0,0 +1,87 @@
|
|||||||
|
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})
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
//go:build sqlite_vtable || vtable
|
||||||
|
|
||||||
|
package resx_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"github.com/samber/do/v2"
|
||||||
|
|
||||||
|
"git.fsdpf.net/go/db/engine"
|
||||||
|
"git.fsdpf.net/go/req"
|
||||||
|
"git.fsdpf.net/go/req/resx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newCacheTestResource(t testing.TB) req.Resource {
|
||||||
|
t.Helper()
|
||||||
|
mockDB, _, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("sqlmock: %v", err)
|
||||||
|
}
|
||||||
|
app := do.New()
|
||||||
|
do.Provide(app, func(i do.Injector) (engine.Engine, error) {
|
||||||
|
return engine.Mock(map[string]engine.MockDBConfig{
|
||||||
|
"default": {Driver: "mysql", Mock: mockDB},
|
||||||
|
}), nil
|
||||||
|
})
|
||||||
|
return resx.New(app, "User", "users",
|
||||||
|
resx.WithConn("default"),
|
||||||
|
resx.WithFields(
|
||||||
|
resx.NewResField("id", "User", resx.FieldWithName("Id"), resx.FieldWithDataType(req.ResInteger)),
|
||||||
|
resx.NewResField("name", "User", resx.FieldWithName("Name"), resx.FieldWithDataType(req.ResString)),
|
||||||
|
resx.NewResField("email", "User", resx.FieldWithName("Email"), resx.FieldWithDataType(req.ResString)),
|
||||||
|
resx.NewResField("status", "User", resx.FieldWithName("Status"), resx.FieldWithDataType(req.ResInteger)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存只应影响速度, 不影响结果: 多次调用必须拿到同一个类型, 但仍是新实例。
|
||||||
|
func TestGetStructTypeIsStable(t *testing.T) {
|
||||||
|
res := newCacheTestResource(t)
|
||||||
|
|
||||||
|
t1 := reflect.TypeOf(res.GetStruct())
|
||||||
|
t2 := reflect.TypeOf(res.GetStruct())
|
||||||
|
if t1 != t2 {
|
||||||
|
t.Fatalf("无 extends 时两次 GetStruct 类型不一致: %v vs %v", t1, t2)
|
||||||
|
}
|
||||||
|
if t1.Kind() != reflect.Ptr || t1.Elem().Kind() != reflect.Struct {
|
||||||
|
t.Fatalf("GetStruct 应返回 *struct, 得到 %v", t1)
|
||||||
|
}
|
||||||
|
if res.GetStruct() == res.GetStruct() {
|
||||||
|
t.Fatal("GetStruct 应该每次返回新实例, 只复用类型")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 带 extends
|
||||||
|
ext := reflect.StructField{Name: "Extra", Type: reflect.TypeOf("")}
|
||||||
|
e1 := reflect.TypeOf(res.GetStruct(ext))
|
||||||
|
e2 := reflect.TypeOf(res.GetStruct(ext))
|
||||||
|
if e1 != e2 {
|
||||||
|
t.Fatalf("带 extends 时两次类型不一致: %v vs %v", e1, e2)
|
||||||
|
}
|
||||||
|
if e1 == t1 {
|
||||||
|
t.Fatal("带 extends 的类型不应与不带 extends 的相同")
|
||||||
|
}
|
||||||
|
if _, ok := e1.Elem().FieldByName("Extra"); !ok {
|
||||||
|
t.Fatal("extends 字段没有出现在结果类型里")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不同的 extends 必须得到不同的类型, 且互不污染
|
||||||
|
ext2 := reflect.StructField{Name: "Other", Type: reflect.TypeOf(0)}
|
||||||
|
o1 := reflect.TypeOf(res.GetStruct(ext2))
|
||||||
|
if o1 == e1 {
|
||||||
|
t.Fatal("不同 extends 却拿到同一个类型, 缓存键有问题")
|
||||||
|
}
|
||||||
|
if _, ok := o1.Elem().FieldByName("Other"); !ok {
|
||||||
|
t.Fatal("第二组 extends 的字段没有出现")
|
||||||
|
}
|
||||||
|
if reflect.TypeOf(res.GetStruct(ext)) != e1 {
|
||||||
|
t.Fatal("缓存被后续调用污染了")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用方复用 extends 底层数组时, 缓存键不能跟着被改写。
|
||||||
|
func TestGetStructExtendsAliasing(t *testing.T) {
|
||||||
|
res := newCacheTestResource(t)
|
||||||
|
|
||||||
|
buf := make([]reflect.StructField, 0, 4)
|
||||||
|
buf = append(buf, reflect.StructField{Name: "A", Type: reflect.TypeOf("")})
|
||||||
|
tA := reflect.TypeOf(res.GetStruct(buf...))
|
||||||
|
|
||||||
|
// 复用同一个底层数组换成另一组字段
|
||||||
|
buf = buf[:0]
|
||||||
|
buf = append(buf, reflect.StructField{Name: "B", Type: reflect.TypeOf("")})
|
||||||
|
tB := reflect.TypeOf(res.GetStruct(buf...))
|
||||||
|
|
||||||
|
if tA == tB {
|
||||||
|
t.Fatal("复用底层数组导致缓存键被改写, 拿到了错误的类型")
|
||||||
|
}
|
||||||
|
if _, ok := tA.Elem().FieldByName("A"); !ok {
|
||||||
|
t.Fatal("第一次的类型被污染了")
|
||||||
|
}
|
||||||
|
if _, ok := tB.Elem().FieldByName("B"); !ok {
|
||||||
|
t.Fatal("第二次的类型不对")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSliceStructType(t *testing.T) {
|
||||||
|
res := newCacheTestResource(t)
|
||||||
|
|
||||||
|
st := reflect.TypeOf(res.GetSliceStruct())
|
||||||
|
if st.Kind() != reflect.Ptr || st.Elem().Kind() != reflect.Slice {
|
||||||
|
t.Fatalf("GetSliceStruct 应返回 *[]struct, 得到 %v", st)
|
||||||
|
}
|
||||||
|
if st.Elem().Elem() != reflect.TypeOf(res.GetStruct()).Elem() {
|
||||||
|
t.Fatal("GetSliceStruct 的元素类型应与 GetStruct 的一致")
|
||||||
|
}
|
||||||
|
if reflect.TypeOf(res.GetSliceStruct()) != st {
|
||||||
|
t.Fatal("两次 GetSliceStruct 类型不一致")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并发调用不能出现数据竞争, 且拿到的类型一致(配合 -race 跑)。
|
||||||
|
func TestGetStructConcurrent(t *testing.T) {
|
||||||
|
res := newCacheTestResource(t)
|
||||||
|
want := reflect.TypeOf(res.GetStruct())
|
||||||
|
|
||||||
|
const n = 16
|
||||||
|
done := make(chan reflect.Type, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
go func() {
|
||||||
|
var got reflect.Type
|
||||||
|
for j := 0; j < 100; j++ {
|
||||||
|
got = reflect.TypeOf(res.GetStruct())
|
||||||
|
}
|
||||||
|
done <- got
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if got := <-done; got != want {
|
||||||
|
t.Fatalf("并发下拿到了不同的类型: %v vs %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkGetStruct(b *testing.B) {
|
||||||
|
res := newCacheTestResource(b)
|
||||||
|
res.GetStruct() // 预热
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = res.GetStruct()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkGetStructWithExtends(b *testing.B) {
|
||||||
|
res := newCacheTestResource(b)
|
||||||
|
ext := reflect.StructField{Name: "Extra", Type: reflect.TypeOf("")}
|
||||||
|
res.GetStruct(ext)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = res.GetStruct(ext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkGetSliceStruct(b *testing.B) {
|
||||||
|
res := newCacheTestResource(b)
|
||||||
|
res.GetSliceStruct()
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = res.GetSliceStruct()
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
-9
@@ -102,6 +102,9 @@ type resource struct {
|
|||||||
primarykey string
|
primarykey string
|
||||||
historyRoles []string
|
historyRoles []string
|
||||||
fields []req.ResField
|
fields []req.ResField
|
||||||
|
|
||||||
|
// structCache 缓存 GetStruct 构造出来的类型,见 res_struct_cache.go
|
||||||
|
structCache resStructCache
|
||||||
}
|
}
|
||||||
|
|
||||||
// intercept 从容器里解析 DataInterceptor 并调用;容器里没有注册时返回 nil, nil, nil(不过滤、不抓取)。
|
// intercept 从容器里解析 DataInterceptor 并调用;容器里没有注册时返回 nil, nil, nil(不过滤、不抓取)。
|
||||||
@@ -337,8 +340,24 @@ func (this *resource) GetDBTable(u req.User, opts ...req.ResOption) (sd *db.Sele
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (this *resource) GetStruct(extends ...reflect.StructField) any {
|
func (this *resource) GetStruct(extends ...reflect.StructField) any {
|
||||||
fields := []reflect.StructField{}
|
return reflect.New(this.structType(extends)).Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *resource) GetSliceStruct(extends ...reflect.StructField) any {
|
||||||
|
return reflect.New(reflect.SliceOf(this.structType(extends))).Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
// structType 返回资源字段(可选叠加 extends)对应的 struct 类型。
|
||||||
|
//
|
||||||
|
// 结果按 extends 缓存: fields 在资源构造之后不再变化, 相同的 extends 必然得到
|
||||||
|
// 同一个类型, 没必要每次都走一遍 reflect.StructOf —— 它即使命中 runtime 的类型
|
||||||
|
// 缓存也要 2μs 上下并产生几十次分配, 详见 res_struct_cache.go。
|
||||||
|
func (this *resource) structType(extends []reflect.StructField) reflect.Type {
|
||||||
|
if t := this.structCache.load(extends); t != nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := make([]reflect.StructField, 0, len(this.fields)+len(extends))
|
||||||
for _, field := range this.fields {
|
for _, field := range this.fields {
|
||||||
code := field.GetCode()
|
code := field.GetCode()
|
||||||
if unicode.IsLetter(rune(code[0])) {
|
if unicode.IsLetter(rune(code[0])) {
|
||||||
@@ -351,14 +370,8 @@ func (this *resource) GetStruct(extends ...reflect.StructField) any {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t := reflect.StructOf(fields)
|
t := reflect.StructOf(fields)
|
||||||
|
this.structCache.store(extends, t)
|
||||||
return reflect.New(t).Interface()
|
return t
|
||||||
}
|
|
||||||
|
|
||||||
func (this *resource) GetSliceStruct(extends ...reflect.StructField) any {
|
|
||||||
t := reflect.TypeOf(this.GetStruct(extends...))
|
|
||||||
st := reflect.SliceOf(t.Elem())
|
|
||||||
return reflect.New(st).Interface()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// isLocalDB 判断是否为本地文件型数据库(LastInsertId 返回最后一条而非第一条)
|
// isLocalDB 判断是否为本地文件型数据库(LastInsertId 返回最后一条而非第一条)
|
||||||
|
|||||||
Reference in New Issue
Block a user