diff --git a/README.md b/README.md index f6d3575..f84d52c 100644 --- a/README.md +++ b/README.md @@ -1236,23 +1236,19 @@ type Reflux interface { | 场景 | 旧版本 | 新版本 | 变化 | |---|---|---|---:| | `Get("Meta","k")` struct 字段 → map 键 | 175.6 ns / 10 allocs | 140.9 ns / 7 allocs | 1.25x | -| **纯 map 路径** `Get("leaf")`(根就是 map) | 120.7 ns / 8 allocs | 161.7 ns / 8 allocs | **0.75x** | -| **纯 map 路径** `Get("b","c","leaf")` 三层 | 314.4 ns / 20 allocs | 439.5 ns / 20 allocs | **0.72x** | +| **纯 map 路径** `Get("leaf")`(根就是 map) | 120.0 ns / 8 allocs | 126.8 ns / 7 allocs | **0.95x** | +| 纯 map 三层 | 300.9 ns / 20 allocs | 260.8 ns / 15 allocs | 1.15x | +| map → struct → struct | 224.6 ns / 11 allocs | 140.3 ns / 6 allocs | 1.60x | | `New(指针)` 构造 | 16.9 ns / 1 alloc | 25.4 ns / 1 alloc | **0.67x** | -- **map 路径不但没提速,纯 map 场景反而更慢**。map 没有稳定的内存布局可以做 - 偏移量运算,这条路径完全走 reflect;而且新实现的内部表示是"地址 + 类型描述符", - 而 map 元素**不可寻址**,所以每经过一跳 map 都要 `reflect.New` 拷贝一份到堆上 - 才能拿到地址 —— 旧实现直接持有 `reflect.Value`,不需要这次拷贝。 - 层级越深,多出来的拷贝越多。 - - 上面第一行的 1.25x 之所以是正的,是因为第一跳 `Meta` 是 **struct 字段**, - 走了快路径,把第二跳 map 的损失盖过去了。**根是 map、或路径深处全是 map 时, - 收益是负的。** +- **map 路径基本打平,提速有限**。map 没有稳定的内存布局可以做偏移量运算, + 这条路径完全走 reflect。命中 map 之后会一次性用 reflect 走完剩余路径、 + 只在最后装箱一次(逐跳装箱的话每跳都要 `reflect.New` 拷贝一份,因为 map + 元素不可寻址),所以层级越深收益越明显;但单跳 map 省不掉那次 + `reflect.NewAt`,仍比旧实现慢约 5%。 **选型建议**: 如果你的数据以 `map[string]any` 为主(比如把数据库查询结果直接 - 存成 map),那么这次重写对你没有收益,反而略有退化 —— 收益全部集中在 - **struct 字段访问**上。 + 存成 map),这次重写对你收益很小 —— 提速几乎全部集中在 **struct 字段访问**上。 - **`New` 慢了约 8 ns**: 构造时要查一次类型描述符缓存。这是一次性成本, 换来之后每次 `Get`/`Set` 省下 50-100 ns —— 只要构造后至少访问一次就是净赚。 diff --git a/docs/flow.md b/docs/flow.md index 174fdc8..3456106 100644 --- a/docs/flow.md +++ b/docs/flow.md @@ -150,20 +150,22 @@ | Struct | 三层嵌套字段 | 199.1 ns / 9 allocs | 63.4 ns / 2 allocs | **3.14×** | | Slice | `Get("Tags","1")` | 132.6 ns / 7 allocs | 47.5 ns / 2 allocs | **2.79×** | | `Get[T]` | 泛型直取,跳过装箱 | — | 29.8 ns / **0 allocs** | **5.08×** | -| Map | struct 字段 → map 键 | 175.6 ns / 10 allocs | 140.9 ns / 7 allocs | 1.25× | -| Map | **纯 map,三层嵌套** | 314.4 ns / 20 allocs | 439.5 ns / 20 allocs | **0.72×** | +| Map | struct 字段 → map 键 | 270.3 ns / 16 allocs | 210.2 ns / 11 allocs | 1.29× | +| Map | 纯 map,三层嵌套 | 300.9 ns / 20 allocs | 260.8 ns / 15 allocs | 1.15× | +| Map | 纯 map,单跳 | 120.0 ns / 8 allocs | 126.8 ns / 7 allocs | **0.95×** | +| Map | map → struct → struct | 224.6 ns / 11 allocs | 140.3 ns / 6 allocs | 1.60× | +| Map | map → map → struct → struct | 303.2 ns / 16 allocs | 205.6 ns / 10 allocs | 1.47× | -三点说明: +两点说明: -1. **纯 map 路径是负收益。** 根就是 map、或路径深处全是 map 时,当前实现比旧版慢约 28%。 - 原因在第一张图里直接看得到:每跳一次 map 都要 `boxCopy`。 - **收益全部集中在 struct 字段访问上** —— 如果你的数据以 `map[string]any` 为主, - 这次重写对你没有收益。 +1. **map 路径分段处理,只有单跳 map 略负。** 命中 map 后用 reflect 连续走完 + **这一段** map,离开时装箱一次再切回偏移量快路径。两头都要顾:逐跳装箱的话 + 每跳 map 都要 `boxCopy`(元素不可寻址),连续多层会累积;但一路 reflect 走到底 + 又会让 map 后面的 struct 跳丢掉快路径、退回 `FieldByName`。分段之后三种混合 + 形态都是最优。只有"单跳 map"仍慢约 5% —— 没有 `boxCopy` 可省,而 + `reflect.NewAt` 那一次省不掉。 -2. **那个 1.25× 容易看错。** 它的第一跳 `Meta` 是 **struct 字段**,走了快路径, - 把第二跳 map 的损失盖了过去 —— 不能当成"map 变快了"来读。 - -3. **剩下的 2 次分配是 API 形状的下限。** 一次是返回的 `R` 包装对象(24 字节), +2. **剩下的 2 次分配是 API 形状的下限。** 一次是返回的 `R` 包装对象(24 字节), 一次是可变参数切片经接口调用时的逃逸。想彻底避开,用 `Get[T]`。 --- diff --git a/rfx.go b/rfx.go index 0bf4a5f..cc634b7 100644 --- a/rfx.go +++ b/rfx.go @@ -116,11 +116,11 @@ func fromReflect(rv reflect.Value, writable bool) (rfx, bool) { } // step 沿路径前进一段。 +// +// 要求 v 已经 normalize 过, 且 Kind 不是 Map —— 两件事都由 walk 保证: +// map 会被 walk 拦下来改走 walkReflect, 不会到这里。 func (v rfx) step(seg string) (rfx, bool) { - cur, ok := v.normalize() - if !ok { - return rfx{}, false - } + cur := v switch cur.td.Kind { case reflect.Struct: @@ -163,19 +163,8 @@ func (v rfx) step(seg string) (rfx, bool) { } return rfx{td: cur.td.Elem, ptr: p, writable: cur.writable}, true - case reflect.Map: - // map 没有稳定布局, 退回 reflect - m := valueAt(cur.td, cur.ptr) - if m.IsNil() { - return rfx{}, false - } - mv := tryMapFieldValue(m, seg) - if !mv.IsValid() { - return rfx{}, false - } - return fromReflect(mv, false) - default: + // 含 Map —— walk 已经把它拦到 walkReflect 去了, 走不到这里 return rfx{}, false } } @@ -188,8 +177,35 @@ func (v rfx) walk(it pathIter) (rfx, bool) { if !more { break } - var ok bool - cur, ok = cur.step(seg) + norm, ok := cur.normalize() + if !ok { + return rfx{}, false + } + + // 命中 map: 用 reflect 连续走完**这一段 map**, 只在离开时装箱一次。 + // + // 两头都要顾: 逐跳走 step 的话每跳都要 boxCopy(map 元素不可寻址, + // 而 rfx 的表示需要一个地址), 连续多层 map 会累积多次堆拷贝; 但如果 + // 一路 reflect 走到底, map 后面的 struct 跳就丢了偏移量快路径, + // 退回 FieldByName。所以只在 map 段内用 reflect, 一离开就切回去。 + if norm.td.Kind == reflect.Map { + rv, pending, exhausted := walkMapRun(valueAt(norm.td, norm.ptr), seg, &it) + if exhausted { + return fromReflect(rv, false) + } + // 路径还没走完但已经不是 map 了: 装箱一次, 回到快路径继续。 + // pending 是离开 map 段之后的第一跳, walkMapRun 没有消费它。 + cur, ok = fromReflect(rv, false) + if !ok { + return rfx{}, false + } + if cur, ok = cur.step(pending); !ok { + return rfx{}, false + } + continue + } + + cur, ok = norm.step(seg) if !ok { return rfx{}, false } @@ -197,6 +213,38 @@ func (v rfx) walk(it pathIter) (rfx, bool) { return cur.normalize() } +// walkMapRun 用 reflect 连续走完一段 map: 从 seg 这一跳开始, 只要落点仍是 +// map 就继续吃下一段, 一旦不是 map(或路径走完)就返回。 +// +// exhausted 为 false 时表示路径还没走完但已经离开 map 段, 此时 pending 是 +// 尚未消费的下一跳; 调用方应当把返回值装箱一次, 切回基于偏移量的快路径 —— +// map 后面如果跟着 struct, 那些跳仍然值得走快路径。 +// +// 注意 pending 是**返回**给调用方的, 而不是通过 it 指针回退。 +// 写回(*it = save)会让逃逸分析认为 it 指向的内容可能逃逸, 进而把 Get 的 +// 可变参数切片也判定为逃逸 —— 那会波及所有路径, 连纯 struct 的零分配快路径 +// 都跟着退化成 1 次分配。 +func walkMapRun(v reflect.Value, seg string, it *pathIter) (_ reflect.Value, pending string, exhausted bool) { + for { + if !v.IsValid() || v.Kind() != reflect.Map { + return reflect.Value{}, "", true + } + v = derefWithAccessor(tryMapFieldValue(v, seg)) + if !v.IsValid() { + return reflect.Value{}, "", true + } + + next, more := it.next() + if !more { + return v, "", true + } + if v.Kind() != reflect.Map { + return v, next, false + } + seg = next + } +} + // boxed 把栈上的遍历结果装箱成返回给调用方的 *rfx(唯一一次分配)。 func boxed(v rfx, ok bool) R { if !ok { diff --git a/rfx_mapwalk_test.go b/rfx_mapwalk_test.go new file mode 100644 index 0000000..e6c35cd --- /dev/null +++ b/rfx_mapwalk_test.go @@ -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) + } + }) + } +}