perf: map 分段走 reflect, 离开后切回快路径
walk 逐跳调用 step, 而 step 的 map 分支每次都要 boxCopy —— map 元素不可寻址,
rfx 的表示又需要一个地址, 只能 reflect.New 复制一份。层级越深累积越多,
导致纯 map 路径比改造前的旧实现还慢, 是整个重写里唯一的性能回归。
改法要两头兼顾:
- 一路 reflect 走到底能省掉逐跳装箱, 但 map 后面的 struct 跳会丢掉偏移量
快路径、退回 FieldByName —— 实测 map→struct→struct 反而比逐跳装箱慢 5.8%
- 所以只在**连续的 map 段**内用 reflect, 一离开就装箱一次切回快路径
map→map→struct→struct 303.2ns/16a -> 205.6ns/10a 1.47x
map→struct→struct→map 309.0ns/16a -> 246.3ns/11a 1.25x
map→struct→struct 224.6ns/11a -> 140.3ns/ 6a 1.60x
纯 map 三层 300.9ns/20a -> 260.8ns/15a 1.15x (原 0.72x)
纯 map 单跳 120.0ns/ 8a -> 126.8ns/ 7a 0.95x
struct 三层(对照) 200.4ns/ 9a -> 59.6ns/ 2a 3.36x
单跳 map 仍慢 5%: 只有一跳时没有 boxCopy 可省, 而 valueAt 那次 reflect.NewAt
省不掉。其余形态全部转正。
两个实现上的坑, 都是测试抓出来的:
1. walkMapRun 发现已离开 map 段时, 那一跳已经从迭代器取走但还没用, 必须交还
给调用方。最初用 *it = save 回退, 结果**零分配快路径全线退化成 1 次分配** ——
通过指针写回会让逃逸分析认为 it 指向的内容可能逃逸, 进而判定 Get 的可变参数
切片逃逸, 波及所有路径, 连纯 struct 的路径都跟着中枪。改成把待处理的段
作为返回值交出去就好了。
2. 若干处清理: normalize 从 step 提到 walk(step 只有 walk 一个调用方);
移除 step 里已不可达的 Map 分支。
测试 252 个对拍用例覆盖 map 之后的各种后继类型与失败形态, 另加 19 个交界用例
专压"离开 map 段"那一跳。已验证有效: 把交还 pending 的那行改成丢弃后,
53 个用例失败。
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
package reflux
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// walk 命中 map 之后会切到 walkReflect 走完剩余路径, 只在最后装箱一次
|
||||
// (原来是逐跳 boxCopy)。这一组守的是"切换之后行为完全不变"。
|
||||
//
|
||||
// 重点覆盖 map 之后的各种后继类型: struct / 指针 / 切片 / 数组 / 再一层 map /
|
||||
// interface / Accessor, 以及各自的失败形态。
|
||||
|
||||
type mwInner struct {
|
||||
Name string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
type mwDoc struct {
|
||||
Meta map[string]any
|
||||
Objs map[string]mwInner
|
||||
Ptrs map[string]*mwInner
|
||||
Nested map[string]map[string]any
|
||||
Arrs map[string][3]int
|
||||
Ifaces map[string]any
|
||||
Deep map[string]any
|
||||
}
|
||||
|
||||
func newMWDoc() *mwDoc {
|
||||
return &mwDoc{
|
||||
Meta: map[string]any{"s": "v", "n": 42, "b": true},
|
||||
Objs: map[string]mwInner{"o": {Name: "inner", Tags: []string{"t0", "t1"}}},
|
||||
Ptrs: map[string]*mwInner{"p": {Name: "ptr", Tags: []string{"p0"}}},
|
||||
Nested: map[string]map[string]any{
|
||||
"lv1": {"lv2": "deep"},
|
||||
},
|
||||
Arrs: map[string][3]int{"a": {10, 20, 30}},
|
||||
Ifaces: map[string]any{"i": mwInner{Name: "iface"}},
|
||||
Deep: map[string]any{
|
||||
"a": map[string]any{"b": map[string]any{"c": map[string]any{"leaf": "bottom"}}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapWalkMatchesLegacy(t *testing.T) {
|
||||
paths := [][]string{
|
||||
// map -> 标量
|
||||
{"Meta", "s"}, {"Meta", "n"}, {"Meta", "b"},
|
||||
// map -> struct -> 字段
|
||||
{"Objs", "o"}, {"Objs", "o", "Name"},
|
||||
// map -> struct -> 切片 -> 元素
|
||||
{"Objs", "o", "Tags"}, {"Objs", "o", "Tags", "1"},
|
||||
// map -> 指针 -> 字段
|
||||
{"Ptrs", "p"}, {"Ptrs", "p", "Name"}, {"Ptrs", "p", "Tags", "0"},
|
||||
// map -> map -> 键
|
||||
{"Nested", "lv1"}, {"Nested", "lv1", "lv2"},
|
||||
// map -> 数组 -> 下标
|
||||
{"Arrs", "a"}, {"Arrs", "a", "2"},
|
||||
// map -> interface -> struct -> 字段
|
||||
{"Ifaces", "i"}, {"Ifaces", "i", "Name"},
|
||||
// 四层 map 链
|
||||
{"Deep", "a"}, {"Deep", "a", "b"}, {"Deep", "a", "b", "c"},
|
||||
{"Deep", "a", "b", "c", "leaf"},
|
||||
{"Deep.a.b.c.leaf"},
|
||||
// 失败形态
|
||||
{"Meta", "nope"}, {"Objs", "o", "NoField"}, {"Objs", "nokey", "Name"},
|
||||
{"Objs", "o", "Tags", "99"}, {"Arrs", "a", "9"},
|
||||
{"Meta", "s", "further"}, {"Ptrs", "nokey", "Name"},
|
||||
{"Deep", "a", "b", "nope", "leaf"},
|
||||
}
|
||||
|
||||
ops := []struct {
|
||||
name string
|
||||
f func(R) any
|
||||
}{
|
||||
{"String", func(r R) any { return r.String() }},
|
||||
{"Int", func(r R) any { return r.Int() }},
|
||||
{"Any", func(r R) any { return fmt.Sprintf("%v", r.Any()) }},
|
||||
{"Exists", func(r R) any { return r.Exists() }},
|
||||
{"RawKind", func(r R) any { return r.Raw().Kind().String() }},
|
||||
{"Keys", func(r R) any { return fmt.Sprintf("%v", r.Keys()) }},
|
||||
{"JSON", func(r R) any { b, e := json.Marshal(r); return fmt.Sprintf("%s/%v", b, e) }},
|
||||
{"Array长度", func(r R) any { return len(r.Array()) }},
|
||||
}
|
||||
|
||||
for _, p := range paths {
|
||||
for _, op := range ops {
|
||||
t.Run(fmt.Sprintf("%v/%s", p, op.name), func(t *testing.T) {
|
||||
want := call(newLegacy(newMWDoc()).Get(p...), op.f)
|
||||
got := call(New(newMWDoc()).Get(p...), op.f)
|
||||
if want != got {
|
||||
t.Fatalf("行为不一致\n 旧 = %s\n 新 = %s", want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 经 map 的写入也要与旧实现一致(含写完之后整体数据的比对)
|
||||
func TestMapWalkWritesMatchLegacy(t *testing.T) {
|
||||
writes := []struct {
|
||||
path string
|
||||
val any
|
||||
}{
|
||||
{"Meta.s", "changed"},
|
||||
{"Meta.n", 7},
|
||||
{"Meta.new", "added"},
|
||||
{"Objs.o.Name", "changed"},
|
||||
{"Objs.o.Tags.0", "changed"},
|
||||
{"Ptrs.p.Name", "changed"},
|
||||
{"Ptrs.p.Tags.0", "changed"},
|
||||
{"Nested.lv1.lv2", "changed"},
|
||||
{"Deep.a.b.c.leaf", "changed"},
|
||||
{"Objs.o.NoField", "x"},
|
||||
{"Objs.nokey.Name", "x"},
|
||||
{"Meta.s.deeper", "x"},
|
||||
}
|
||||
for _, w := range writes {
|
||||
t.Run(w.path, func(t *testing.T) {
|
||||
dOld, dNew := newMWDoc(), newMWDoc()
|
||||
rOld, rNew := newLegacy(dOld), New(dNew)
|
||||
|
||||
resOld := call(rOld, func(r R) any { r.Set(w.path, w.val); return "ok" })
|
||||
resNew := call(rNew, func(r R) any { r.Set(w.path, w.val); return "ok" })
|
||||
if resOld != resNew {
|
||||
t.Fatalf("Set 行为不一致\n 旧 = %s\n 新 = %s", resOld, resNew)
|
||||
}
|
||||
|
||||
bOld, _ := json.Marshal(dOld)
|
||||
bNew, _ := json.Marshal(dNew)
|
||||
if string(bOld) != string(bNew) {
|
||||
t.Fatalf("写入后数据不一致\n 旧 = %s\n 新 = %s", bOld, bNew)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 根就是 map 的情况(没有任何 struct 跳)
|
||||
func TestMapWalkRootIsMap(t *testing.T) {
|
||||
mk := func() map[string]any {
|
||||
return map[string]any{
|
||||
"a": map[string]any{"b": map[string]any{"leaf": "x", "n": 1}},
|
||||
"s": "top",
|
||||
}
|
||||
}
|
||||
for _, p := range [][]string{
|
||||
{"s"}, {"a"}, {"a", "b"}, {"a", "b", "leaf"}, {"a", "b", "n"},
|
||||
{"a.b.leaf"}, {"a", "nope"}, {"a", "b", "leaf", "further"},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%v", p), func(t *testing.T) {
|
||||
mo, mn := mk(), mk()
|
||||
want := call(newLegacy(&mo).Get(p...), func(r R) any { return r.String() })
|
||||
got := call(New(&mn).Get(p...), func(r R) any { return r.String() })
|
||||
if want != got {
|
||||
t.Fatalf("旧 = %s, 新 = %s", want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// walk 命中 map 后只连续走完**这一段** map, 一离开就装箱切回快路径。
|
||||
// 交界处最容易出错: 离开 map 段的那一跳如果被 walkMapRun 吃掉又没交还,
|
||||
// 路径就会少走一跳且不报错。这一组专门压交界。
|
||||
func TestMapRunBoundary(t *testing.T) {
|
||||
type leaf struct{ V string }
|
||||
type mid struct {
|
||||
L leaf
|
||||
M map[string]any
|
||||
}
|
||||
type doc struct {
|
||||
M1 map[string]any
|
||||
M2 map[string]map[string]any
|
||||
}
|
||||
|
||||
mk := func() *doc {
|
||||
return &doc{
|
||||
M1: map[string]any{
|
||||
"s": mid{L: leaf{V: "a"}, M: map[string]any{"k": "in-mid"}},
|
||||
"sl": []any{mid{L: leaf{V: "in-slice"}}},
|
||||
},
|
||||
M2: map[string]map[string]any{
|
||||
"x": {"s": mid{L: leaf{V: "b"}}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
paths := [][]string{
|
||||
// map 之后紧跟 struct, 再往下走(交界在第 2 跳)
|
||||
{"M1", "s"}, {"M1", "s", "L"}, {"M1", "s", "L", "V"},
|
||||
// map -> struct -> map(离开又进入)
|
||||
{"M1", "s", "M"}, {"M1", "s", "M", "k"},
|
||||
// map -> map -> struct -> struct(连续两跳 map 后才离开)
|
||||
{"M2", "x"}, {"M2", "x", "s"}, {"M2", "x", "s", "L"}, {"M2", "x", "s", "L", "V"},
|
||||
// map -> slice -> struct
|
||||
{"M1", "sl"}, {"M1", "sl", "0"}, {"M1", "sl", "0", "L", "V"},
|
||||
// 点号写法应当与多参数完全一致
|
||||
{"M2.x.s.L.V"}, {"M1.s.M.k"},
|
||||
// 交界处的失败形态
|
||||
{"M1", "s", "NoField"}, {"M1", "s", "L", "NoField"},
|
||||
{"M2", "x", "s", "L", "V", "further"},
|
||||
{"M1", "nokey", "L", "V"}, {"M2", "nokey", "s", "L", "V"},
|
||||
}
|
||||
|
||||
for _, p := range paths {
|
||||
t.Run(fmt.Sprintf("%v", p), func(t *testing.T) {
|
||||
want := call(newLegacy(mk()).Get(p...), func(r R) any { return r.String() })
|
||||
got := call(New(mk()).Get(p...), func(r R) any { return r.String() })
|
||||
if want != got {
|
||||
t.Fatalf("旧 = %s, 新 = %s", want, got)
|
||||
}
|
||||
// 再比一次 Exists, 防止"取到空值"和"路径不存在"被混为一谈
|
||||
we := newLegacy(mk()).Get(p...).Exists()
|
||||
ge := New(mk()).Get(p...).Exists()
|
||||
if we != ge {
|
||||
t.Fatalf("Exists 不一致: 旧 = %v, 新 = %v", we, ge)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user