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 性能章节开头加一行指向该文档, 正文只留结论。
This commit is contained in:
2026-08-31 10:29:52 +08:00
parent d3216cb3b4
commit ac011cd5c5
4 changed files with 397 additions and 3 deletions
+139
View File
@@ -0,0 +1,139 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 726" role="img"
aria-label="Get 的流程:New 构造 rfx 后进入 walk 循环,每段路径先经 normalize 解引用,再由 step 按 Kind 分派到 struct、slice、array 三条 unsafe 快路径,或 map 的 reflect 回退分支,或直接拒绝;循环结束后 boxed 装箱一次返回">
<style>
/* 独立 SVG 通过 img 引用时是隔离渲染的: 拿不到页面的 CSS 变量, 也继承不到
currentColor。所以配色和明暗适配都必须写在文件内部。
背景刻意留透明, 让它贴合宿主页面的底色。 */
svg { --fg:#171C22; --muted:#5A6672; --faint:#8B96A2;
--line:#D8DEE5; --surface:#FFFFFF;
--accent:#0B6E75; --accent-soft:#E3F1F2;
--fallback:#9A5B08; --fallback-soft:#F7EBDA;
--stop:#9B3232; --stop-soft:#F7E4E4; }
@media (prefers-color-scheme: dark) {
svg { --fg:#DFE5EC; --muted:#93A0AE; --faint:#6B7885;
--line:#2A333E; --surface:#151B23;
--accent:#4ECDC4; --accent-soft:#12312F;
--fallback:#E3A857; --fallback-soft:#33270F;
--stop:#E07A6E; --stop-soft:#331A18; }
}
.n-label { font-family:"IBM Plex Mono",ui-monospace,"SF Mono",Menlo,monospace; font-size:13px; font-weight:500; }
.n-sub { font-family:"IBM Plex Sans",system-ui,"PingFang SC","Microsoft YaHei",sans-serif; font-size:11.5px; }
.e-label { font-family:"IBM Plex Sans",system-ui,"PingFang SC","Microsoft YaHei",sans-serif; font-size:11.5px; }
.grp { font-family:"IBM Plex Mono",ui-monospace,Menlo,monospace; font-size:11px; letter-spacing:0.08em; }
</style>
<defs>
<marker id="ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="var(--fg)"/>
</marker>
<marker id="ah-fast" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="var(--accent)"/>
</marker>
<marker id="ah-slow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="var(--fallback)"/>
</marker>
<marker id="ah-stop" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="var(--stop)"/>
</marker>
</defs>
<!-- 1. New -->
<rect x="330" y="16" width="300" height="52" rx="5" fill="var(--surface)" stroke="var(--fg)" stroke-width="1.5"/>
<text class="n-label" x="480" y="38" text-anchor="middle" fill="var(--fg)">New(&amp;v)</text>
<text class="n-sub" x="480" y="55" text-anchor="middle" fill="var(--muted)">rfx{ td, ptr, writable } · 24 字节,栈上</text>
<line x1="480" y1="68" x2="480" y2="98" stroke="var(--fg)" stroke-width="1.5" marker-end="url(#ah)"/>
<!-- 2. Get -->
<rect x="358" y="98" width="244" height="38" rx="5" fill="var(--surface)" stroke="var(--fg)" stroke-width="1.5"/>
<text class="n-label" x="480" y="122" text-anchor="middle" fill="var(--fg)">Get(path...)</text>
<line x1="480" y1="136" x2="480" y2="166" stroke="var(--fg)" stroke-width="1.5" marker-end="url(#ah)"/>
<!-- loop group -->
<rect x="150" y="166" width="660" height="330" rx="7" fill="none" stroke="var(--line)" stroke-width="1.5" stroke-dasharray="5 4"/>
<text class="grp" x="166" y="185" fill="var(--faint)">WALK 循环 · 全程栈上,零分配</text>
<!-- 3. pathIter -->
<rect x="330" y="198" width="300" height="52" rx="5" fill="var(--surface)" stroke="var(--fg)" stroke-width="1.5"/>
<text class="n-label" x="480" y="220" text-anchor="middle" fill="var(--fg)">pathIter.next()</text>
<text class="n-sub" x="480" y="237" text-anchor="middle" fill="var(--muted)">&quot;.&quot; 切子串,不新建切片</text>
<line x1="480" y1="250" x2="480" y2="280" stroke="var(--fg)" stroke-width="1.5" marker-end="url(#ah)"/>
<text class="e-label" x="492" y="269" fill="var(--muted)">每段 seg</text>
<!-- 4. normalize -->
<rect x="306" y="280" width="348" height="56" rx="5" fill="var(--surface)" stroke="var(--fg)" stroke-width="1.5"/>
<text class="n-label" x="480" y="302" text-anchor="middle" fill="var(--fg)">normalize()</text>
<text class="n-sub" x="480" y="320" text-anchor="middle" fill="var(--muted)">Ptr → loadPtr 逐层解引用 · Interface → 拆包</text>
<line x1="480" y1="336" x2="480" y2="366" stroke="var(--fg)" stroke-width="1.5" marker-end="url(#ah)"/>
<!-- 5. step -->
<rect x="358" y="366" width="244" height="38" rx="5" fill="var(--surface)" stroke="var(--fg)" stroke-width="1.5"/>
<text class="n-label" x="480" y="390" text-anchor="middle" fill="var(--fg)">step(seg) · 按 td.Kind</text>
<!-- fan-out spine -->
<line x1="480" y1="404" x2="480" y2="422" stroke="var(--fg)" stroke-width="1.5"/>
<line x1="108" y1="422" x2="852" y2="422" stroke="var(--fg)" stroke-width="1.5"/>
<!-- five drops -->
<line x1="108" y1="422" x2="108" y2="446" stroke="var(--accent)" stroke-width="1.5" marker-end="url(#ah-fast)"/>
<line x1="294" y1="422" x2="294" y2="446" stroke="var(--accent)" stroke-width="1.5" marker-end="url(#ah-fast)"/>
<line x1="480" y1="422" x2="480" y2="446" stroke="var(--accent)" stroke-width="1.5" marker-end="url(#ah-fast)"/>
<line x1="666" y1="422" x2="666" y2="446" stroke="var(--fallback)" stroke-width="1.5" marker-end="url(#ah-slow)"/>
<line x1="852" y1="422" x2="852" y2="446" stroke="var(--stop)" stroke-width="1.5" marker-end="url(#ah-stop)"/>
<!-- branch boxes -->
<rect x="28" y="446" width="160" height="64" rx="5" fill="var(--accent-soft)" stroke="var(--accent)" stroke-width="1.5"/>
<text class="n-label" x="108" y="468" text-anchor="middle" fill="var(--accent)">Struct</text>
<text class="n-sub" x="108" y="485" text-anchor="middle" fill="var(--fg)">lookupField 查偏移</text>
<text class="n-sub" x="108" y="500" text-anchor="middle" fill="var(--fg)">fieldAt(ptr, off)</text>
<rect x="214" y="446" width="160" height="64" rx="5" fill="var(--accent-soft)" stroke="var(--accent)" stroke-width="1.5"/>
<text class="n-label" x="294" y="468" text-anchor="middle" fill="var(--accent)">Slice</text>
<text class="n-sub" x="294" y="485" text-anchor="middle" fill="var(--fg)">parseIndex</text>
<text class="n-sub" x="294" y="500" text-anchor="middle" fill="var(--fg)">sliceElemAt</text>
<rect x="400" y="446" width="160" height="64" rx="5" fill="var(--accent-soft)" stroke="var(--accent)" stroke-width="1.5"/>
<text class="n-label" x="480" y="468" text-anchor="middle" fill="var(--accent)">Array</text>
<text class="n-sub" x="480" y="485" text-anchor="middle" fill="var(--fg)">parseIndex</text>
<text class="n-sub" x="480" y="500" text-anchor="middle" fill="var(--fg)">arrayElemAt</text>
<rect x="586" y="446" width="160" height="64" rx="5" fill="var(--fallback-soft)" stroke="var(--fallback)" stroke-width="1.5"/>
<text class="n-label" x="666" y="468" text-anchor="middle" fill="var(--fallback)">Map</text>
<text class="n-sub" x="666" y="485" text-anchor="middle" fill="var(--fg)">tryMapFieldValue</text>
<text class="n-sub" x="666" y="500" text-anchor="middle" fill="var(--fg)">boxCopy · 堆拷贝</text>
<rect x="772" y="446" width="160" height="64" rx="5" fill="var(--stop-soft)" stroke="var(--stop)" stroke-width="1.5"/>
<text class="n-label" x="852" y="468" text-anchor="middle" fill="var(--stop)">其它 / 未导出</text>
<text class="n-sub" x="852" y="485" text-anchor="middle" fill="var(--fg)">拒绝, ok = false</text>
<text class="n-sub" x="852" y="500" text-anchor="middle" fill="var(--fg)">→ 无效值</text>
<!-- loop back: from the 4 continuing branches -->
<path d="M 108 510 L 108 546 L 666 546" fill="none" stroke="var(--fg)" stroke-width="1.5"/>
<path d="M 294 510 L 294 546" fill="none" stroke="var(--fg)" stroke-width="1.5"/>
<path d="M 480 510 L 480 546" fill="none" stroke="var(--fg)" stroke-width="1.5"/>
<path d="M 666 510 L 666 546" fill="none" stroke="var(--fg)" stroke-width="1.5"/>
<path d="M 666 546 L 892 546 L 892 224 L 634 224" fill="none" stroke="var(--fg)" stroke-width="1.5" marker-end="url(#ah)"/>
<text class="e-label" x="884" y="392" text-anchor="end" fill="var(--muted)">还有下一段 → 继续循环</text>
<!-- exit -->
<line x1="480" y1="546" x2="480" y2="500" stroke="none"/>
<path d="M 480 546 L 240 546 L 240 588" fill="none" stroke="var(--fg)" stroke-width="1.5" marker-end="url(#ah)"/>
<text class="e-label" x="252" y="574" fill="var(--muted)">路径走完</text>
<!-- 6. boxed -->
<rect x="90" y="588" width="300" height="56" rx="5" fill="var(--surface)" stroke="var(--fg)" stroke-width="1.5"/>
<text class="n-label" x="240" y="610" text-anchor="middle" fill="var(--fg)">boxed() → &amp;rfx{}</text>
<text class="n-sub" x="240" y="628" text-anchor="middle" fill="var(--muted)">唯一一次堆分配 · 24 字节</text>
<line x1="240" y1="644" x2="240" y2="672" stroke="var(--fg)" stroke-width="1.5" marker-end="url(#ah)"/>
<rect x="118" y="672" width="244" height="38" rx="5" fill="var(--surface)" stroke="var(--fg)" stroke-width="1.5"/>
<text class="n-label" x="240" y="696" text-anchor="middle" fill="var(--fg)">R 接口</text>
<!-- Get[T] shortcut -->
<path d="M 602 617 L 400 617" fill="none" stroke="var(--accent)" stroke-width="1.5" stroke-dasharray="4 3" marker-end="url(#ah-fast)"/>
<rect x="602" y="588" width="320" height="56" rx="5" fill="var(--accent-soft)" stroke="var(--accent)" stroke-width="1.5"/>
<text class="n-label" x="762" y="610" text-anchor="middle" fill="var(--accent)">Get[T] 泛型直取</text>
<text class="n-sub" x="762" y="628" text-anchor="middle" fill="var(--fg)">跳过装箱,直接写进调用方变量 · 0 分配</text>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

+106
View File
@@ -0,0 +1,106 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 566" role="img"
aria-label="Set 的流程:先把 key 拆成父路径和末段,走 walk 定位父级;父级不存在或不可写时整个交回 refx 慢路径;否则按 Kind 分派,再经过 assignField 的四级递降,从零分配的直接写内存一直退到交回 refx">
<style>
/* 独立 SVG 通过 img 引用时是隔离渲染的: 拿不到页面的 CSS 变量, 也继承不到
currentColor。所以配色和明暗适配都必须写在文件内部。
背景刻意留透明, 让它贴合宿主页面的底色。 */
svg { --fg:#171C22; --muted:#5A6672; --faint:#8B96A2;
--line:#D8DEE5; --surface:#FFFFFF;
--accent:#0B6E75; --accent-soft:#E3F1F2;
--fallback:#9A5B08; --fallback-soft:#F7EBDA;
--stop:#9B3232; --stop-soft:#F7E4E4; }
@media (prefers-color-scheme: dark) {
svg { --fg:#DFE5EC; --muted:#93A0AE; --faint:#6B7885;
--line:#2A333E; --surface:#151B23;
--accent:#4ECDC4; --accent-soft:#12312F;
--fallback:#E3A857; --fallback-soft:#33270F;
--stop:#E07A6E; --stop-soft:#331A18; }
}
.n-label { font-family:"IBM Plex Mono",ui-monospace,"SF Mono",Menlo,monospace; font-size:13px; font-weight:500; }
.n-sub { font-family:"IBM Plex Sans",system-ui,"PingFang SC","Microsoft YaHei",sans-serif; font-size:11.5px; }
.e-label { font-family:"IBM Plex Sans",system-ui,"PingFang SC","Microsoft YaHei",sans-serif; font-size:11.5px; }
.grp { font-family:"IBM Plex Mono",ui-monospace,Menlo,monospace; font-size:11px; letter-spacing:0.08em; }
</style>
<defs>
<marker id="s-ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="var(--fg)"/>
</marker>
<marker id="s-ah-slow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="var(--fallback)"/>
</marker>
<marker id="s-ah-fast" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="var(--accent)"/>
</marker>
</defs>
<!-- Set -->
<rect x="40" y="16" width="290" height="52" rx="5" fill="var(--surface)" stroke="var(--fg)" stroke-width="1.5"/>
<text class="n-label" x="185" y="38" text-anchor="middle" fill="var(--fg)">Set(key, val)</text>
<text class="n-sub" x="185" y="55" text-anchor="middle" fill="var(--muted)">splitLastStr → 父路径 + 末段</text>
<line x1="185" y1="68" x2="185" y2="98" stroke="var(--fg)" stroke-width="1.5" marker-end="url(#s-ah)"/>
<!-- walk parent -->
<rect x="40" y="98" width="290" height="38" rx="5" fill="var(--surface)" stroke="var(--fg)" stroke-width="1.5"/>
<text class="n-label" x="185" y="122" text-anchor="middle" fill="var(--fg)">walk(父路径)</text>
<line x1="185" y1="136" x2="185" y2="168" stroke="var(--fg)" stroke-width="1.5" marker-end="url(#s-ah)"/>
<!-- gate -->
<rect x="26" y="168" width="318" height="56" rx="5" fill="var(--surface)" stroke="var(--fg)" stroke-width="1.5"/>
<text class="n-label" x="185" y="190" text-anchor="middle" fill="var(--fg)">父级存在?且 writable?</text>
<text class="n-sub" x="185" y="208" text-anchor="middle" fill="var(--muted)">unsafe 路径没有 CanSet(),必须自己判</text>
<!-- gate: no -> refx -->
<path d="M 344 196 L 560 196" fill="none" stroke="var(--fallback)" stroke-width="1.5" marker-end="url(#s-ah-slow)"/>
<text class="e-label" x="452" y="187" text-anchor="middle" fill="var(--fallback)">否 · 典型是路径穿过 map</text>
<rect x="560" y="168" width="368" height="56" rx="5" fill="var(--fallback-soft)" stroke="var(--fallback)" stroke-width="1.5"/>
<text class="n-label" x="744" y="190" text-anchor="middle" fill="var(--fallback)">整个交回 refx.Set()</text>
<text class="n-sub" x="744" y="208" text-anchor="middle" fill="var(--fg)">map 的取出-修改-写回 SetMapIndex 在那边</text>
<!-- gate: yes -->
<line x1="185" y1="224" x2="185" y2="256" stroke="var(--fg)" stroke-width="1.5" marker-end="url(#s-ah)"/>
<text class="e-label" x="197" y="245" fill="var(--muted)"></text>
<rect x="40" y="256" width="290" height="38" rx="5" fill="var(--surface)" stroke="var(--fg)" stroke-width="1.5"/>
<text class="n-label" x="185" y="280" text-anchor="middle" fill="var(--fg)">setField · 按 Kind</text>
<line x1="185" y1="294" x2="185" y2="326" stroke="var(--fg)" stroke-width="1.5" marker-end="url(#s-ah)"/>
<text class="e-label" x="197" y="315" fill="var(--muted)">Struct / Slice / Array</text>
<!-- assignField ladder -->
<rect x="26" y="326" width="902" height="196" rx="7" fill="none" stroke="var(--line)" stroke-width="1.5" stroke-dasharray="5 4"/>
<text class="grp" x="42" y="345" fill="var(--faint)">ASSIGNFIELD · 四级递降,能多快就多快</text>
<rect x="46" y="358" width="418" height="60" rx="5" fill="var(--accent-soft)" stroke="var(--accent)" stroke-width="1.5"/>
<text class="n-label" x="66" y="380" fill="var(--accent)">① storeFast</text>
<text class="n-sub" x="66" y="399" fill="var(--fg)">类型完全一致的 string/int/bool/float64</text>
<text class="n-sub" x="66" y="413" fill="var(--muted)">按 *T 直接写内存 · 0 分配</text>
<rect x="490" y="358" width="418" height="60" rx="5" fill="var(--surface)" stroke="var(--fg)" stroke-width="1.5"/>
<text class="n-label" x="510" y="380" fill="var(--fg)">② 类型精确匹配</text>
<text class="n-sub" x="510" y="399" fill="var(--fg)">rv.Type() == td.rtype</text>
<text class="n-sub" x="510" y="413" fill="var(--muted)">valueAt().Set(rv),不绕道</text>
<rect x="46" y="440" width="418" height="60" rx="5" fill="var(--surface)" stroke="var(--fg)" stroke-width="1.5"/>
<text class="n-label" x="66" y="462" fill="var(--fg)">③ 标量目标</text>
<text class="n-sub" x="66" y="481" fill="var(--fg)">assignReflect + spf13/cast 转换</text>
<text class="n-sub" x="66" y="495" fill="var(--muted)">溢出截断等边角行为与旧实现对齐</text>
<rect x="490" y="440" width="418" height="60" rx="5" fill="var(--fallback-soft)" stroke="var(--fallback)" stroke-width="1.5"/>
<text class="n-label" x="510" y="462" fill="var(--fallback)">④ 复合目标 → refx</text>
<text class="n-sub" x="510" y="481" fill="var(--fg)">指针 / 切片 / 结构体 / map / interface</text>
<text class="n-sub" x="510" y="495" fill="var(--muted)">语义琐碎,重写必然出偏差</text>
<!-- ladder arrows -->
<path d="M 464 388 L 490 388" fill="none" stroke="var(--fg)" stroke-width="1.5" marker-end="url(#s-ah)"/>
<path d="M 908 418 L 928 418 L 928 430 L 26 430 L 26 470 L 46 470" fill="none" stroke="var(--fg)" stroke-width="1.5" marker-end="url(#s-ah)"/>
<path d="M 464 470 L 490 470" fill="none" stroke="var(--fg)" stroke-width="1.5" marker-end="url(#s-ah)"/>
<text class="e-label" x="477" y="382" text-anchor="middle" fill="var(--muted)">miss</text>
<text class="e-label" x="477" y="464" text-anchor="middle" fill="var(--muted)">miss</text>
<!-- ok exit -->
<path d="M 255 418 L 255 530" fill="none" stroke="var(--accent)" stroke-width="1.5" stroke-dasharray="4 3" marker-end="url(#s-ah-fast)"/>
<text class="e-label" x="267" y="543" fill="var(--accent)">命中即返回</text>
</svg>

After

Width:  |  Height:  |  Size: 7.7 KiB

+131
View File
@@ -0,0 +1,131 @@
# 内部流程:路径是怎么被分派的
`Get``Set` 都不直接操作数据,而是把路径拆成段,逐段按当前值的 `Kind` 分派。
**分派到哪个分支,决定了这一跳走 unsafe 偏移量寻址还是退回 reflect,
也决定了它要不要分配内存。**
图例:
| 标记 | 含义 |
|---|---|
| 青色 | unsafe 快路径 —— 偏移量寻址,零分配 |
| 琥珀色 | reflect 回退 —— 有堆拷贝 |
| 红色 | 拒绝 —— 返回无效值或 panic |
---
## 一、取值:`Get(path...)`
![Get 的分派流程](flow-get.svg)
主干是 `walk` 的循环:每取出一段路径,先 `normalize` 把指针和 interface 剥掉,
再由 `step``Kind` 分派。整个循环在栈上进行,只有最后返回给调用方时才装箱一次。
### 各步骤
| 步骤 | 函数 | 说明 |
|---|---|---|
| 构造 | `New(&v)` | 得到 `rfx{td, ptr, writable}`,24 字节,栈上传递 |
| 取段 | `pathIter.next()` | 按 `.` 切子串,**不新建切片** —— 旧实现的 `expandPath` 每次要分配 4 次 |
| 解引用 | `normalize()` | `Ptr``loadPtr` 逐层解;`Interface` 退回 reflect 拆包 |
| 分派 | `step(seg)` | 按 `td.Kind` 进入下面五个分支之一 |
| 装箱 | `boxed()` | 唯一一次堆分配,24 字节 |
### 五个分支
| 分支 | 做法 | 代价 |
|---|---|---|
| **Struct** | `lookupField` 查缓存好的偏移量 → `fieldAt(ptr, off)` | 零分配 |
| **Slice** | `parseIndex``sliceElemAt` | 零分配 |
| **Array** | `parseIndex``arrayElemAt` | 零分配 |
| **Map** | `tryMapFieldValue``boxCopy` | **每跳一次堆拷贝** |
| 其它 / 未导出字段 | 拒绝,`ok = false` | 返回无效值 |
**map 是唯一在循环内部就要堆拷贝的分支。** map 元素不可寻址,而 `rfx` 的内部表示
需要一个地址,所以每经过一跳 map 都要 `reflect.New` 复制一份。层级越深,拷贝越多 ——
这是纯 map 路径反而比旧实现慢的直接原因(见下面的实测)。
未导出字段必须显式拒绝:`reflect.NewAt` 造出来的 Value **不带只读标记**,
放行会让调用方绕过 Go 的导出规则直接读写私有字段。这是 unsafe 路径上最关键的一条防线。
---
## 二、写入:`Set(key, val)`
![Set 的分派流程](flow-set.svg)
写入比取值多两道关:**目标必须可写**,**类型必须对得上**。
两者任何一个不满足,就把整个操作交回基于 reflect 的旧实现 `refx`
### 为什么不自己实现复合类型的赋值
`assignField` 的四级递降是有意的,越往下越通用、也越慢:
| 级别 | 条件 | 做法 |
|---|---|---|
| ① | 类型完全一致的 `string`/`int`/`bool`/`float64` | 按 `*T` 直接写内存,**0 分配** |
| ② | `rv.Type() == td.rtype` | `valueAt().Set(rv)`,不绕道 |
| ③ | 标量目标 | `assignReflect` + `spf13/cast` 转换 |
| ④ | 复合目标(指针/切片/结构体/map/interface) | **整个交回 `refx`** |
第 ④ 级不能自己实现。`refx.setValue` 对这些类型有一整套规则:
- 指针字段是"设置指针**指向的值**"而不是替换指针
- `[]any` 会逐元素转成目标切片的元素类型
- `map` 可以按字段名填充进 `struct`
重新实现一遍必然出偏差 —— 这不是假设,是实际踩过的坑:
第一版自己实现时,`Set("Email", "x")`(`Email``*string`)会 panic,
而旧实现是正常的。
### 父级不可写的典型情况
`walk` 定位到父级后如果 `!parent.writable`,整个 `Set` 交回 `refx`
最常见的触发是**路径穿过了 map**:map 元素不可寻址,取到的是副本,
直接写副本不会反映到原 map 上。`refx` 有完整的"取出-修改-写回 `SetMapIndex`"逻辑,
复用它而不是重写。
---
## 三、每条分支的实际代价
同样一次 `Get`,走哪条分支决定了它是 63 ns 还是 440 ns。
| 分支 | 路径 | 旧实现 | 当前 | 变化 |
|---|---|---|---|---|
| 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×** |
三点说明:
1. **纯 map 路径是负收益。** 根就是 map、或路径深处全是 map 时,当前实现比旧版慢约 28%。
原因在第一张图里直接看得到:每跳一次 map 都要 `boxCopy`
**收益全部集中在 struct 字段访问上** —— 如果你的数据以 `map[string]any` 为主,
这次重写对你没有收益。
2. **那个 1.25× 容易看错。** 它的第一跳 `Meta`**struct 字段**,走了快路径,
把第二跳 map 的损失盖了过去 —— 不能当成"map 变快了"来读。
3. **剩下的 2 次分配是 API 形状的下限。** 一次是返回的 `R` 包装对象(24 字节),
一次是可变参数切片经接口调用时的逃逸。想彻底避开,用 `Get[T]`
---
## 对应源码
| 图中节点 | 位置 |
|---|---|
| `walk` / `step` / `normalize` / `fromReflect` | `rfx.go` |
| `Set` / `setField` / `assignField` / `storeFast` | `rfx.go` |
| `pathIter` | `path.go` |
| 类型布局缓存 `rfxDescriptorOf` | `rfx_typedesc.go` |
| `fieldAt` / `sliceElemAt` / `boxCopy` 等全部 unsafe 操作 | `unsafeptr.go` |
| `refx` 慢路径实现 | `rfx_reflect.go` |
改动这几个函数时记得同步这两张图。SVG 源文件就在本目录下,
是手写的(没有用绘图工具),直接编辑坐标即可。
实测环境:Apple M4 Pro / darwin-arm64 / go1.25.5,数字取多轮中位数。