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 个隔离用例全部失败, 包括
藏在两层嵌套和数组元素里的引用。
This commit is contained in:
2026-08-31 11:10:49 +08:00
parent cb84176381
commit a2c0c9783e
3 changed files with 263 additions and 4 deletions
+30
View File
@@ -49,6 +49,16 @@ type rfxTypeDesc struct {
// ptrElemKind 缓存指针指向类型的 Kind, 避免热路径上再取一次
ptrElemKind reflect.Kind
// needsClone 表示该类型是否含有引用语义的成分(指针/切片/map/interface/
// chan/func/unsafe.Pointer), 递归包含字段与元素。
//
// New(值) 要保证"改副本不影响调用方的原数据"。对于不含任何引用成分的类型,
// 逐字节拷贝就已经完全独立了, 没必要走 DeepClone 的递归 —— 实测纯值 struct
// 深拷贝 145ns/2allocs, 而语义等价的浅拷贝只要 28ns/1alloc。
//
// 字符串不算引用成分: 它的底层数组不可变, 共享是安全的。
needsClone bool
// fast 标记该类型是否是可以直接按机器类型读写的常见标量类型。
// 热路径上只要比较一个字节, 就能决定走不走零分配快路径。
fast fastKind
@@ -161,6 +171,18 @@ func buildLocked(t reflect.Type, pending map[reflect.Type]*rfxTypeDesc) *rfxType
// 先登记再递归, 否则自引用类型会栈溢出
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()
@@ -179,6 +201,10 @@ func buildLocked(t reflect.Type, pending map[reflect.Type]*rfxTypeDesc) *rfxType
for i := range td.fields {
f := &td.fields[i]
td.fieldMap[f.Name] = f
// 任一字段含引用成分, 整个 struct 就必须深拷贝
if f.Type.needsClone {
td.needsClone = true
}
}
// 追加"首字母小写"别名, 兼容 reflux 的大小写宽松查找。
// 只在不与真实字段名冲突时才登记。
@@ -204,6 +230,10 @@ func buildLocked(t reflect.Type, pending map[reflect.Type]*rfxTypeDesc) *rfxType
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 没有稳定的内存布局可以做偏移量运算, 描述符只记录元素类型,