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:
@@ -1184,6 +1184,10 @@ type Reflux interface {
|
||||
|
||||
## 性能
|
||||
|
||||
> **想看内部是怎么工作的?** [docs/flow.md](docs/flow.md) 用两张流程图讲清了
|
||||
> `Get`/`Set` 如何把路径逐段分派到 unsafe 快路径或 reflect 回退,
|
||||
> 以及每条分支各自的代价。下面只讲结论。
|
||||
|
||||
### 实现方式
|
||||
|
||||
热路径不再逐次走 `reflect` 的按名字段查找,而是:
|
||||
@@ -1231,11 +1235,25 @@ type Reflux interface {
|
||||
|
||||
| 场景 | 旧版本 | 新版本 | 变化 |
|
||||
|---|---|---|---:|
|
||||
| `Get("Meta","k")` map 键 | 175.6 ns / 10 allocs | 140.9 ns / 7 allocs | 1.25x |
|
||||
| `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** |
|
||||
| `New(指针)` 构造 | 16.9 ns / 1 alloc | 25.4 ns / 1 alloc | **0.67x** |
|
||||
|
||||
- **map 只快 1.25 倍**: map 没有稳定的内存布局可以做偏移量运算,这条路径完全走
|
||||
reflect,而且取出来的值必须拷一份(map 元素不可寻址)。这是设计上的取舍。
|
||||
- **map 路径不但没提速,纯 map 场景反而更慢**。map 没有稳定的内存布局可以做
|
||||
偏移量运算,这条路径完全走 reflect;而且新实现的内部表示是"地址 + 类型描述符",
|
||||
而 map 元素**不可寻址**,所以每经过一跳 map 都要 `reflect.New` 拷贝一份到堆上
|
||||
才能拿到地址 —— 旧实现直接持有 `reflect.Value`,不需要这次拷贝。
|
||||
层级越深,多出来的拷贝越多。
|
||||
|
||||
上面第一行的 1.25x 之所以是正的,是因为第一跳 `Meta` 是 **struct 字段**,
|
||||
走了快路径,把第二跳 map 的损失盖过去了。**根是 map、或路径深处全是 map 时,
|
||||
收益是负的。**
|
||||
|
||||
**选型建议**: 如果你的数据以 `map[string]any` 为主(比如把数据库查询结果直接
|
||||
存成 map),那么这次重写对你没有收益,反而略有退化 —— 收益全部集中在
|
||||
**struct 字段访问**上。
|
||||
|
||||
- **`New` 慢了约 8 ns**: 构造时要查一次类型描述符缓存。这是一次性成本,
|
||||
换来之后每次 `Get`/`Set` 省下 50-100 ns —— 只要构造后至少访问一次就是净赚。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user