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:
2026-08-31 14:15:39 +08:00
parent ba0fd4b3bc
commit ca2a8f911a
4 changed files with 308 additions and 42 deletions
+9 -13
View File
@@ -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 —— 只要构造后至少访问一次就是净赚。