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:
@@ -114,16 +114,31 @@ func New(v any) R {
|
|||||||
panic(NewErrUnsupportedTargetType(actualValue.Kind()))
|
panic(NewErrUnsupportedTargetType(actualValue.Kind()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果原始传入的不是指针类型,需要进行深度克隆以避免修改原始数据
|
// 如果原始传入的不是指针类型,需要复制一份以避免修改原始数据
|
||||||
// 对于引用类型(map, slice)这尤其重要
|
|
||||||
if !isPtr {
|
if !isPtr {
|
||||||
// 使用深度克隆创建一个完全独立的副本(DeepClone 返回的是指针)
|
rv = cloneForValueInput(actualValue)
|
||||||
rv = DeepClone(actualValue)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return newRfx(rv)
|
return newRfx(rv)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cloneForValueInput 为 New(值) 复制一份独立的数据,返回指向副本的指针。
|
||||||
|
//
|
||||||
|
// 只有类型含引用成分(指针/切片/map/interface 等)时才需要 DeepClone 的递归;
|
||||||
|
// 纯值类型逐字节拷贝就已经完全独立了 —— 字符串虽然内部有指针,但底层数组不可变,
|
||||||
|
// 共享是安全的,所以不算引用成分。
|
||||||
|
//
|
||||||
|
// 实测纯值 struct: DeepClone 145 ns / 2 allocs,浅拷贝 28 ns / 1 alloc。
|
||||||
|
// 判断本身不花钱: needsClone 在类型描述符构建时算好,这里只读一个 bool。
|
||||||
|
func cloneForValueInput(v reflect.Value) reflect.Value {
|
||||||
|
if td := rfxDescriptorOf(v.Type()); !td.needsClone {
|
||||||
|
box := reflect.New(v.Type())
|
||||||
|
box.Elem().Set(v)
|
||||||
|
return box
|
||||||
|
}
|
||||||
|
return DeepClone(v)
|
||||||
|
}
|
||||||
|
|
||||||
// newRfx 把一个 reflect.Value 转成 rfx 的内部表示(类型描述符 + 地址 + 可写标记)。
|
// newRfx 把一个 reflect.Value 转成 rfx 的内部表示(类型描述符 + 地址 + 可写标记)。
|
||||||
func newRfx(rv reflect.Value) *rfx {
|
func newRfx(rv reflect.Value) *rfx {
|
||||||
if rv.Kind() == reflect.Ptr {
|
if rv.Kind() == reflect.Ptr {
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package reflux
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New(值) 的语义是"改副本不影响调用方的原数据"。为了省掉深拷贝,现在会先看
|
||||||
|
// 类型描述符里的 needsClone —— 不含引用成分的类型走逐字节浅拷贝。
|
||||||
|
//
|
||||||
|
// 这一组测试守两件事:
|
||||||
|
// 1. needsClone 的判定不能漏(漏了就会浅拷贝一个含引用的类型,写入穿透到原数据)
|
||||||
|
// 2. 无论走哪条路,隔离语义都必须成立
|
||||||
|
//
|
||||||
|
// 第 1 条是这次改动唯一的风险点:判错的后果是静默的数据串改,不报错也不 panic。
|
||||||
|
|
||||||
|
// ---- needsClone 判定 ----
|
||||||
|
|
||||||
|
type cPure struct {
|
||||||
|
A, B string
|
||||||
|
N int
|
||||||
|
F float64
|
||||||
|
Ok bool
|
||||||
|
Arr [3]int
|
||||||
|
Sub struct{ X, Y string }
|
||||||
|
}
|
||||||
|
|
||||||
|
type cSlice struct {
|
||||||
|
A string
|
||||||
|
Tags []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type cMap struct {
|
||||||
|
A string
|
||||||
|
Meta map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type cPtr struct {
|
||||||
|
A string
|
||||||
|
P *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type cIface struct {
|
||||||
|
A string
|
||||||
|
V any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 引用成分藏在两层嵌套之下
|
||||||
|
type cInner struct{ Tags []string }
|
||||||
|
type cMid struct{ Inner cInner }
|
||||||
|
type cDeepRef struct {
|
||||||
|
A string
|
||||||
|
Mid cMid
|
||||||
|
}
|
||||||
|
|
||||||
|
// 引用成分藏在数组元素里
|
||||||
|
type cArrRef struct {
|
||||||
|
A string
|
||||||
|
Arrs [2]cInner
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自引用
|
||||||
|
type cNode struct {
|
||||||
|
Name string
|
||||||
|
Next *cNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNeedsCloneDetection(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
typ reflect.Type
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"纯值 struct", reflect.TypeOf(cPure{}), false},
|
||||||
|
{"含切片", reflect.TypeOf(cSlice{}), true},
|
||||||
|
{"含 map", reflect.TypeOf(cMap{}), true},
|
||||||
|
{"含指针", reflect.TypeOf(cPtr{}), true},
|
||||||
|
{"含 interface", reflect.TypeOf(cIface{}), true},
|
||||||
|
{"引用藏在两层嵌套下", reflect.TypeOf(cDeepRef{}), true},
|
||||||
|
{"引用藏在数组元素里", reflect.TypeOf(cArrRef{}), true},
|
||||||
|
{"自引用类型", reflect.TypeOf(cNode{}), true},
|
||||||
|
{"time.Time(内含 *Location)", reflect.TypeOf(time.Time{}), true},
|
||||||
|
{"裸切片", reflect.TypeOf([]string{}), true},
|
||||||
|
{"裸 map", reflect.TypeOf(map[string]string{}), true},
|
||||||
|
{"纯值数组", reflect.TypeOf([3]int{}), false},
|
||||||
|
{"字符串不算引用", reflect.TypeOf(""), false},
|
||||||
|
{"纯值嵌套 struct", reflect.TypeOf(struct{ S struct{ A string } }{}), false},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
if got := rfxDescriptorOf(c.typ).needsClone; got != c.want {
|
||||||
|
t.Fatalf("needsClone = %v, 期望 %v", got, c.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 隔离语义:无论走哪条路都必须成立 ----
|
||||||
|
|
||||||
|
func TestNewByValueIsolation(t *testing.T) {
|
||||||
|
t.Run("纯值 struct(走浅拷贝)", func(t *testing.T) {
|
||||||
|
orig := cPure{A: "a", N: 1, Arr: [3]int{1, 2, 3}}
|
||||||
|
orig.Sub.X = "x"
|
||||||
|
r := New(orig)
|
||||||
|
r.Set("A", "changed")
|
||||||
|
r.Set("N", 99)
|
||||||
|
r.Set("Arr.0", 100)
|
||||||
|
r.Set("Sub.X", "changed")
|
||||||
|
if orig.A != "a" || orig.N != 1 || orig.Arr[0] != 1 || orig.Sub.X != "x" {
|
||||||
|
t.Fatalf("原数据被改写了: %+v", orig)
|
||||||
|
}
|
||||||
|
if r.Get("A").String() != "changed" || r.Get("Arr.0").Int() != 100 {
|
||||||
|
t.Fatal("副本上的写入没生效")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("含切片(走深拷贝)", func(t *testing.T) {
|
||||||
|
orig := cSlice{A: "a", Tags: []string{"x", "y"}}
|
||||||
|
r := New(orig)
|
||||||
|
r.Set("Tags.0", "changed")
|
||||||
|
if orig.Tags[0] != "x" {
|
||||||
|
t.Fatalf("原切片被改写了: %v", orig.Tags)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("含 map(走深拷贝)", func(t *testing.T) {
|
||||||
|
orig := cMap{A: "a", Meta: map[string]string{"k": "v"}}
|
||||||
|
r := New(orig)
|
||||||
|
r.Set("Meta.k", "changed")
|
||||||
|
if orig.Meta["k"] != "v" {
|
||||||
|
t.Fatalf("原 map 被改写了: %v", orig.Meta)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("引用藏在两层嵌套下", func(t *testing.T) {
|
||||||
|
orig := cDeepRef{A: "a", Mid: cMid{Inner: cInner{Tags: []string{"x"}}}}
|
||||||
|
r := New(orig)
|
||||||
|
r.Set("Mid.Inner.Tags.0", "changed")
|
||||||
|
if orig.Mid.Inner.Tags[0] != "x" {
|
||||||
|
t.Fatalf("深层的原切片被改写了: %v", orig.Mid.Inner.Tags)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("引用藏在数组元素里", func(t *testing.T) {
|
||||||
|
orig := cArrRef{A: "a"}
|
||||||
|
orig.Arrs[0] = cInner{Tags: []string{"x"}}
|
||||||
|
r := New(orig)
|
||||||
|
r.Set("Arrs.0.Tags.0", "changed")
|
||||||
|
if orig.Arrs[0].Tags[0] != "x" {
|
||||||
|
t.Fatalf("数组元素里的原切片被改写了: %v", orig.Arrs[0].Tags)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("裸 map", func(t *testing.T) {
|
||||||
|
orig := map[string]any{"k": "v"}
|
||||||
|
r := New(orig)
|
||||||
|
r.Set("k", "changed")
|
||||||
|
if orig["k"] != "v" {
|
||||||
|
t.Fatalf("原 map 被改写了: %v", orig)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("裸切片", func(t *testing.T) {
|
||||||
|
orig := []string{"x", "y"}
|
||||||
|
r := New(orig)
|
||||||
|
r.Set("0", "changed")
|
||||||
|
if orig[0] != "x" {
|
||||||
|
t.Fatalf("原切片被改写了: %v", orig)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 与旧实现逐格对拍:浅拷贝路径的可观察行为必须与 DeepClone 完全一致。
|
||||||
|
func TestNewByValueMatchesLegacy(t *testing.T) {
|
||||||
|
mk := []struct {
|
||||||
|
name string
|
||||||
|
val func() any
|
||||||
|
}{
|
||||||
|
{"纯值 struct", func() any { return cPure{A: "a", N: 7, F: 1.5, Ok: true, Arr: [3]int{1, 2, 3}} }},
|
||||||
|
{"纯值数组", func() any { return [3]int{1, 2, 3} }},
|
||||||
|
{"含切片", func() any { return cSlice{A: "a", Tags: []string{"x"}} }},
|
||||||
|
{"含 map", func() any { return cMap{A: "a", Meta: map[string]string{"k": "v"}} }},
|
||||||
|
{"纯值嵌套", func() any { return struct{ S struct{ A string } }{} }},
|
||||||
|
}
|
||||||
|
ops := []struct {
|
||||||
|
name string
|
||||||
|
f func(R) any
|
||||||
|
}{
|
||||||
|
{"RawKind", func(r R) any { return r.Raw().Kind().String() }},
|
||||||
|
{"Keys", func(r R) any { return r.Keys() }},
|
||||||
|
{"JSON", func(r R) any { b, e := r.MarshalJSON(); return string(b) + "/" + errStr(e) }},
|
||||||
|
{"Any", func(r R) any { return r.Any() }},
|
||||||
|
{"PtrType", func(r R) any { return reflect.TypeOf(r.Ptr()).String() }},
|
||||||
|
}
|
||||||
|
for _, m := range mk {
|
||||||
|
for _, op := range ops {
|
||||||
|
t.Run(m.name+"/"+op.name, func(t *testing.T) {
|
||||||
|
want := call(newLegacy(m.val()), op.f)
|
||||||
|
got := call(New(m.val()), op.f)
|
||||||
|
if want != got {
|
||||||
|
t.Fatalf("行为不一致\n 旧 = %s\n 新 = %s", want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func errStr(e error) string {
|
||||||
|
if e == nil {
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
|
return e.Error()
|
||||||
|
}
|
||||||
@@ -49,6 +49,16 @@ type rfxTypeDesc struct {
|
|||||||
// ptrElemKind 缓存指针指向类型的 Kind, 避免热路径上再取一次
|
// ptrElemKind 缓存指针指向类型的 Kind, 避免热路径上再取一次
|
||||||
ptrElemKind reflect.Kind
|
ptrElemKind reflect.Kind
|
||||||
|
|
||||||
|
// needsClone 表示该类型是否含有引用语义的成分(指针/切片/map/interface/
|
||||||
|
// chan/func/unsafe.Pointer), 递归包含字段与元素。
|
||||||
|
//
|
||||||
|
// New(值) 要保证"改副本不影响调用方的原数据"。对于不含任何引用成分的类型,
|
||||||
|
// 逐字节拷贝就已经完全独立了, 没必要走 DeepClone 的递归 —— 实测纯值 struct
|
||||||
|
// 深拷贝 145ns/2allocs, 而语义等价的浅拷贝只要 28ns/1alloc。
|
||||||
|
//
|
||||||
|
// 字符串不算引用成分: 它的底层数组不可变, 共享是安全的。
|
||||||
|
needsClone bool
|
||||||
|
|
||||||
// fast 标记该类型是否是可以直接按机器类型读写的常见标量类型。
|
// fast 标记该类型是否是可以直接按机器类型读写的常见标量类型。
|
||||||
// 热路径上只要比较一个字节, 就能决定走不走零分配快路径。
|
// 热路径上只要比较一个字节, 就能决定走不走零分配快路径。
|
||||||
fast fastKind
|
fast fastKind
|
||||||
@@ -161,6 +171,18 @@ func buildLocked(t reflect.Type, pending map[reflect.Type]*rfxTypeDesc) *rfxType
|
|||||||
// 先登记再递归, 否则自引用类型会栈溢出
|
// 先登记再递归, 否则自引用类型会栈溢出
|
||||||
pending[t] = td
|
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() {
|
switch t.Kind() {
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
n := t.NumField()
|
n := t.NumField()
|
||||||
@@ -179,6 +201,10 @@ func buildLocked(t reflect.Type, pending map[reflect.Type]*rfxTypeDesc) *rfxType
|
|||||||
for i := range td.fields {
|
for i := range td.fields {
|
||||||
f := &td.fields[i]
|
f := &td.fields[i]
|
||||||
td.fieldMap[f.Name] = f
|
td.fieldMap[f.Name] = f
|
||||||
|
// 任一字段含引用成分, 整个 struct 就必须深拷贝
|
||||||
|
if f.Type.needsClone {
|
||||||
|
td.needsClone = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 追加"首字母小写"别名, 兼容 reflux 的大小写宽松查找。
|
// 追加"首字母小写"别名, 兼容 reflux 的大小写宽松查找。
|
||||||
// 只在不与真实字段名冲突时才登记。
|
// 只在不与真实字段名冲突时才登记。
|
||||||
@@ -204,6 +230,10 @@ func buildLocked(t reflect.Type, pending map[reflect.Type]*rfxTypeDesc) *rfxType
|
|||||||
td.Elem = buildLocked(t.Elem(), pending)
|
td.Elem = buildLocked(t.Elem(), pending)
|
||||||
td.elemSize = t.Elem().Size()
|
td.elemSize = t.Elem().Size()
|
||||||
td.arrayLen = t.Len()
|
td.arrayLen = t.Len()
|
||||||
|
// 数组是值语义, 但元素含引用成分时整体仍需深拷贝
|
||||||
|
if td.Elem.needsClone {
|
||||||
|
td.needsClone = true
|
||||||
|
}
|
||||||
|
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
// map 没有稳定的内存布局可以做偏移量运算, 描述符只记录元素类型,
|
// map 没有稳定的内存布局可以做偏移量运算, 描述符只记录元素类型,
|
||||||
|
|||||||
Reference in New Issue
Block a user