Files
reflux/rfx_mapwalk_test.go
what ca2a8f911a 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 个用例失败。
2026-08-31 14:15:39 +08:00

221 lines
6.8 KiB
Go

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)
}
})
}
}