Files
reflux/rfx_typedesc.go
what a2c0c9783e perf: New(值) 对不含引用成分的类型改走浅拷贝
New(值) 的语义是"改副本不影响调用方的原数据", 原来一律走 DeepClone 递归。
但对于不含引用成分的类型, 逐字节拷贝就已经完全独立了, 没必要递归。

在类型描述符里加 needsClone(构建期算一次, 不在热路径):
含指针/切片/map/interface/chan/func/unsafe.Pointer 即为真, 递归包含字段与
数组元素。字符串不算 —— 底层数组不可变, 共享是安全的。

  New(纯值 struct)   145.0ns -> 85.5ns   1.70x
  New(含引用 struct) 304.9ns             不变(深拷贝确实必要)
  New(&ptr)           25.8ns             不变

注意 1.70x 而不是更多: New 还要做类型断言、normalizeInputValue、递归解引用、
类型校验和描述符查找, 省掉的只是深拷贝那一段(约 59ns)。

needsClone 必须在递归之前按 Kind 置位, 否则自引用类型会漏标: 类型图里的环
一定要经过某种间接(指针/切片/map), 先置位才能让递归回来的自引用类型读到
正确的值。测试里专门有一条 cNode{Next *cNode} 守这个。

判错的后果是静默的数据串改 —— 浅拷贝一个含引用的类型, 写入会穿透到调用方
的原数据, 不报错也不 panic。所以测试分两层:
  1. needsClone 判定本身: 14 个用例, 含"引用藏在两层嵌套下"、"藏在数组元素里"、
     自引用、time.Time(内含 *Location)等容易漏判的形态
  2. 隔离语义: 7 个用例, 无论走哪条路都断言原数据未被改写
  3. 与旧实现对拍: 5 种输入 x 5 个观察方法

已验证测试有效: 把 needsClone 强制为 false 后, 6 个隔离用例全部失败, 包括
藏在两层嵌套和数组元素里的引用。
2026-08-31 11:10:49 +08:00

246 lines
8.0 KiB
Go

// 类型布局描述符: 把 struct 的字段偏移量、元素类型大小等信息构建一次并全局缓存,
// 热路径上不再走 reflect 的按名字段查找。
package reflux
import (
"reflect"
"sync"
)
// rfxFieldDesc 描述一个字段在其所属 struct 里的位置和类型。
type rfxFieldDesc struct {
// Name 字段名(原始声明名)
Name string
// Offset 字段相对于 struct 起始地址的字节偏移量
Offset uintptr
// Type 字段类型的描述符, 递归引用以支持嵌套 struct
Type *rfxTypeDesc
// Exported 字段是否是导出字段。
//
// 这是本原型最关键的安全标记: reflect.NewAt 构造出来的 Value 是可寻址且
// **可写**的, 它不带 reflect 正常路径上的 flagRO 只读标记, 也就是说 Go 语言
// 层面"不能修改未导出字段"这条保护在 unsafe 路径上完全失效。因此必须由我们
// 自己在每一次 Get/Set 前显式拒绝未导出字段。
Exported bool
// index 在 reflect.Type 里的字段下标, 回退到 reflect 路径时使用
index int
}
// rfxTypeDesc 描述一个类型的内存布局, 构建一次之后全局复用。
type rfxTypeDesc struct {
rtype reflect.Type
Kind reflect.Kind
Size uintptr
// fields 按声明顺序保存所有字段, 用于线性扫描查找
fields []rfxFieldDesc
// fieldMap 按字段名索引, 用于 map 查找。
// 除了原始字段名, 还额外收录了"首字母小写"的别名, 以兼容 reflux 的
// tryStructFieldValue 行为(Get("address") 能命中 Address 字段)。
fieldMap map[string]*rfxFieldDesc
// Elem 是 ptr/slice/array 的元素类型描述符
Elem *rfxTypeDesc
// elemSize 是元素类型的字节大小, slice/array 下标寻址用
elemSize uintptr
// arrayLen 是数组长度(仅 Kind == Array 时有效)
arrayLen int
// ptrElemKind 缓存指针指向类型的 Kind, 避免热路径上再取一次
ptrElemKind reflect.Kind
// needsClone 表示该类型是否含有引用语义的成分(指针/切片/map/interface/
// chan/func/unsafe.Pointer), 递归包含字段与元素。
//
// New(值) 要保证"改副本不影响调用方的原数据"。对于不含任何引用成分的类型,
// 逐字节拷贝就已经完全独立了, 没必要走 DeepClone 的递归 —— 实测纯值 struct
// 深拷贝 145ns/2allocs, 而语义等价的浅拷贝只要 28ns/1alloc。
//
// 字符串不算引用成分: 它的底层数组不可变, 共享是安全的。
needsClone bool
// fast 标记该类型是否是可以直接按机器类型读写的常见标量类型。
// 热路径上只要比较一个字节, 就能决定走不走零分配快路径。
fast fastKind
}
// fastKind 标记"类型与 Go 内置标量完全一致"的情况。
// 注意必须是**完全一致**: type MyStr string 的 Kind 也是 String, 但直接按
// string 写入会绕过 reflux 原有的 cast 转换语义, 所以这里只认原生类型。
type fastKind uint8
const (
fkNone fastKind = iota
fkString
fkInt
fkBool
fkFloat64
)
var (
typeString = reflect.TypeOf("")
typeInt = reflect.TypeOf(int(0))
typeBool = reflect.TypeOf(false)
typeFloat64 = reflect.TypeOf(float64(0))
)
func fastKindOf(t reflect.Type) fastKind {
switch t {
case typeString:
return fkString
case typeInt:
return fkInt
case typeBool:
return fkBool
case typeFloat64:
return fkFloat64
}
return fkNone
}
// isScalar 报告该类型是不是可以用 cast 直接转换的标量。
// 只有标量目标才走 fastx 自己的赋值逻辑; 复合类型(指针/切片/结构体/map/interface)
// 的赋值语义由 reflux.setValue 定义, 重新实现必然产生偏差, 一律交回它处理。
func (td *rfxTypeDesc) isScalar() bool {
switch td.Kind {
case reflect.String, reflect.Bool,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Uintptr, reflect.Float32, reflect.Float64:
return true
}
return false
}
// lookupField 用 map 查找字段, 未找到返回 nil。
func (td *rfxTypeDesc) lookupField(name string) *rfxFieldDesc {
return td.fieldMap[name]
}
var (
// descCache 是全局类型描述符缓存。reflect.Type 本身可比较, 直接做 key。
descCache sync.Map // map[reflect.Type]*rfxTypeDesc
// descMu 只在缓存未命中(冷路径)时加锁, 保证同一时刻只有一个构建者,
// 并且保证带环的类型图整体构建完成后才发布, 读者永远看不到半成品。
descMu sync.Mutex
)
// rfxDescriptorOf 返回类型 t 的布局描述符, 第一次遇到时构建, 之后命中缓存。
func rfxDescriptorOf(t reflect.Type) *rfxTypeDesc {
if v, ok := descCache.Load(t); ok {
return v.(*rfxTypeDesc)
}
descMu.Lock()
defer descMu.Unlock()
// 双重检查: 可能在抢锁期间已经被别的 goroutine 构建好了
if v, ok := descCache.Load(t); ok {
return v.(*rfxTypeDesc)
}
// pending 保存本次构建过程中新建的所有描述符。自引用类型
// (type Node struct{ Next *Node }) 靠它终止递归。
pending := make(map[reflect.Type]*rfxTypeDesc, 8)
td := buildLocked(t, pending)
// 整张类型图都填充完毕之后再统一发布。sync.Map.Store 提供的
// happens-before 保证了读者 Load 到任何一个描述符时, 它引用到的
// 其它描述符也已经写完。
for k, v := range pending {
descCache.Store(k, v)
}
return td
}
// buildLocked 在持有 descMu 的前提下递归构建描述符。
func buildLocked(t reflect.Type, pending map[reflect.Type]*rfxTypeDesc) *rfxTypeDesc {
if v, ok := descCache.Load(t); ok {
return v.(*rfxTypeDesc)
}
if td, ok := pending[t]; ok {
return td
}
td := &rfxTypeDesc{
rtype: t,
Kind: t.Kind(),
Size: t.Size(),
fast: fastKindOf(t),
}
// 先登记再递归, 否则自引用类型会栈溢出
pending[t] = td
// needsClone 必须在递归之前按 Kind 置位。
//
// 类型图里的环一定要经过某种间接(指针/切片/map), 而这几种在这里就被标成
// true 了; 于是自引用类型递归回来时读到的是已经正确的值, 不会因为"还没算完"
// 而漏标。举例: type Node struct{ Next *Node } —— 处理 *Node 时先置 true,
// 再递归回 Node, Node 看到字段 needsClone=true, 自己也跟着为 true。
switch t.Kind() {
case reflect.Ptr, reflect.Slice, reflect.Map, reflect.Interface,
reflect.Chan, reflect.Func, reflect.UnsafePointer:
td.needsClone = true
}
switch t.Kind() {
case reflect.Struct:
n := t.NumField()
td.fields = make([]rfxFieldDesc, n)
td.fieldMap = make(map[string]*rfxFieldDesc, n*2)
for i := 0; i < n; i++ {
sf := t.Field(i)
td.fields[i] = rfxFieldDesc{
Name: sf.Name,
Offset: sf.Offset,
Type: buildLocked(sf.Type, pending),
Exported: sf.IsExported(),
index: i,
}
}
for i := range td.fields {
f := &td.fields[i]
td.fieldMap[f.Name] = f
// 任一字段含引用成分, 整个 struct 就必须深拷贝
if f.Type.needsClone {
td.needsClone = true
}
}
// 追加"首字母小写"别名, 兼容 reflux 的大小写宽松查找。
// 只在不与真实字段名冲突时才登记。
for i := range td.fields {
f := &td.fields[i]
if len(f.Name) > 0 && f.Name[0] >= 'A' && f.Name[0] <= 'Z' {
alias := string(f.Name[0]+32) + f.Name[1:]
if _, exists := td.fieldMap[alias]; !exists {
td.fieldMap[alias] = f
}
}
}
case reflect.Ptr:
td.Elem = buildLocked(t.Elem(), pending)
td.ptrElemKind = t.Elem().Kind()
case reflect.Slice:
td.Elem = buildLocked(t.Elem(), pending)
td.elemSize = t.Elem().Size()
case reflect.Array:
td.Elem = buildLocked(t.Elem(), pending)
td.elemSize = t.Elem().Size()
td.arrayLen = t.Len()
// 数组是值语义, 但元素含引用成分时整体仍需深拷贝
if td.Elem.needsClone {
td.needsClone = true
}
case reflect.Map:
// map 没有稳定的内存布局可以做偏移量运算, 描述符只记录元素类型,
// 实际取值走 reflect 回退路径。
td.Elem = buildLocked(t.Elem(), pending)
}
return td
}