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 个隔离用例全部失败, 包括
藏在两层嵌套和数组元素里的引用。
246 lines
8.0 KiB
Go
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
|
|
}
|