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:
+13
-11
@@ -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]`。
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user