5 Commits
Author SHA1 Message Date
what 50f5531d3b docs: 讲清"值传递的开销只在 New 这一次", 并补上浅拷贝/深拷贝的分岔
两处之前没说清楚的地方。

一、值传递的代价只发生在构造那一次, 不影响之后的操作

原来只列了 New(值) 3227ns vs New(&v) 25ns, 容易被读成"传值之后一直慢"。
实际上构造完成后两者的内部表示完全相同(都是类型描述符 + 地址), 后续
Get/Set 走同一条代码路径:

                     构造后 Get          构造后 Set
  New(v)  传值构造    49.7ns / 2 allocs   27.2ns / 0 allocs
  New(&v) 传指针构造  50.3ns / 2 allocs   27.1ns / 0 allocs

差异在噪声范围内。所以要看的是**构造频次**而不是访问频次: 构造一次访问很多次
的话那点开销会被摊薄; 每个请求都构造、对象又含 map/slice 时才值得计较。

二、流程图补上值分支内部的二次分岔

图是在 needsClone 那次优化之前画的, 一直写着"传值 = DeepClone", 已经不准:

  不含引用成分(纯值 struct/数组/字符串) -> 逐字节浅拷贝, 约 85 ns
    字符串虽然内部有指针, 但底层数组不可变, 共享是安全的
  含指针/切片/map/interface(递归包含字段与数组元素) -> DeepClone

这也是"能不能 copy 一份再取指针"的答案: 纯值类型可以(库里已经这么做了),
含引用的类型不行 —— 浅拷贝只复制 slice/map 的头部, 底层数据仍与调用方共享,
写入会穿透, 那就不再是隔离副本了。

顺带把代价数字更正为实测值(原来写的 1518ns/44allocs 是另一个更小的样本)。
2026-08-31 14:52:23 +08:00
what 6c6159f342 fix: Scope() 返回快实现, 不再把后续操作丢回逐次反射
Scope() 原来是 tv.slow().Scope(), 返回的是 *refx —— 基于 reflect 的慢实现。
功能完全正常, 只是它之后的每一次 Get/Set 都退回逐次反射, 这次重写的收益
在 Scope 之后全部消失。

  Scope("A") 之后再 Get   144.6ns / 7 allocs -> 48.7ns / 2 allocs   2.97x
  对照: 不经 Scope 直接 Get                     59.0ns / 2 allocs

改为自己走 walk 定位、克隆、再包成 *rfx。克隆复用 cloneForValueInput,
与 New(值) 同一套逻辑: 含引用成分才递归深拷贝, 纯值类型逐字节复制。

这类问题很容易在重构里悄悄失守 —— 交回慢实现不会有任何功能异常, 只是慢 3 倍,
测试不看动态类型就发现不了。所以 TestScopeReturnsFastImpl 直接断言返回的是
*rfx, 并验证过有效: 改回 &refx{} 后该用例失败。

顺带确认了其余 slow() 调用点都不会把慢实现泄漏给调用方:
Append/Delete 返回 v 自身, Keys/StringMap*/Slice* 返回的是具体 Go 类型。

另加 Scope 的语义测试(含引用子树 / 纯值子树 / map 子树 / 无参数整体克隆,
都断言原数据未被改写)与 78 个对拍用例。
2026-08-31 14:29:06 +08:00
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