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 |
|
what
|
cb84176381
|
docs: 补上 New 的解析流程,并用测试钉住 rfx 的大小
原来的流程文档只画了 Get 和 Set, New 在图里就是一个方框。但 New 的分支其实不少,
而且"指针模式 vs 值模式"是对外承诺的语义, 值得单独说清楚。
新增 docs/flow-new.svg 与对应章节, 覆盖四步: 快速分支 -> 解包 -> 解引用与校验
-> 按 isPtr 分岔。其中三处是使用者真正需要知道的:
1. 快速分支里 []R 不深拷贝也不包指针。这样 Raw() 才是 Slice kind、
Array() 才能取到里面的 R。
2. 传值比传指针贵约 60 倍(约 1518ns/44allocs vs 约 25ns), 因为要 DeepClone
递归复制整个对象图, 代价随对象规模增长。不需要副本语义就一律传指针。
顺带记下 DeepClone 的已知限制: 循环引用会栈溢出(CloneValue 没有已访问
集合), 这是旧实现就有的行为, 当前实现复用了它所以两边一致。
3. 为什么两条路最后都标 ptrRoot。漏掉它会连锁出问题: Raw() 返回值类型而不是
指针时, normalizeAccessorSlice 会把 []R 解成 []map 而不是 []*map ——
这个 bug 只在 []R 相关用例上才暴露得出来。
另外补 rfx_size_test.go: 文档里"24 字节"这个数字此前没有测试守着, 而 rfx 在
walk 循环里全程按值传递, 加字段会直接拖慢热路径。加了 ptrRoot 之后仍是 24 字节
(落在原有的对齐填充里), 用测试钉住, 以后加字段时会先失败。
同时修正: 源码对照表补上 New/newRfx 与 normalizeInputValue/DeepClone 的位置,
Get 章节的 rfx 字段列表补上 ptrRoot, 开头改成三个入口。
|
2026-08-31 11:02:00 +08:00 |
|
what
|
ac011cd5c5
|
docs: 更正 map 性能数据, 并增加内部流程文档
两部分改动。
一、更正 README 里关于 map 的性能结论(这是修正一个错误的说法)
原来写的是 "map 只快 1.25 倍", 但那个数字来自 Get("Meta","k") ——
第一跳 Meta 是 struct 字段, 走了快路径, 把第二跳 map 的损失盖了过去。
标成 "map 键" 会误导人。
补测纯 map 路径(根就是 map, 全程没有 struct 跳), 复测 10 轮结果稳定:
1 层 旧 120.7ns/8allocs -> 新 161.7ns/8allocs 0.75x
3 层 旧 314.4ns/20allocs -> 新 439.5ns/20allocs 0.72x
也就是说纯 map 场景当前实现比旧版慢约 28%, 不是变快。原因是内部表示为
"地址 + 类型描述符", 而 map 元素不可寻址, 每跳一次 map 都要 reflect.New
拷贝一份才能拿到地址; 旧实现直接持有 reflect.Value, 没这次拷贝。
表格改成分行列出两种场景, 并加了选型建议: 数据以 map[string]any 为主的
调用方, 这次重写没有收益, 反而略有退化 —— 收益全部集中在 struct 字段访问。
二、新增 docs/flow.md 与两张手写 SVG 流程图
说明 Get/Set 如何把路径逐段分派: 每段先 normalize 剥掉指针和 interface,
再按 Kind 进入 struct/slice/array 三条 unsafe 快路径、map 的 reflect 回退,
或直接拒绝。三色区分快路径 / 回退 / 拒绝, 把两件容易被忽略的事画了出来:
1. map 是唯一在 walk 循环内部就要堆拷贝的分支 —— 正是上面那个负收益的
直接原因, 图里一眼能看到。
2. Set 的 assignField 是四级递降, 第 ④ 级把复合类型整个交回 refx。
原因写在文档里: 指针字段要设置指向的值而不是替换指针、[]any 要逐元素
转换、map 可填充进 struct —— 重新实现必然出偏差, 是实际踩过的坑。
SVG 通过 img 引用时是隔离渲染(拿不到宿主页面的 CSS 变量和 currentColor),
所以配色和 prefers-color-scheme 明暗适配都烘在文件内部, 背景留透明。
README 性能章节开头加一行指向该文档, 正文只留结论。
|
2026-08-31 10:29:52 +08:00 |
|