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:
@@ -116,11 +116,11 @@ func fromReflect(rv reflect.Value, writable bool) (rfx, bool) {
|
||||
}
|
||||
|
||||
// step 沿路径前进一段。
|
||||
//
|
||||
// 要求 v 已经 normalize 过, 且 Kind 不是 Map —— 两件事都由 walk 保证:
|
||||
// map 会被 walk 拦下来改走 walkReflect, 不会到这里。
|
||||
func (v rfx) step(seg string) (rfx, bool) {
|
||||
cur, ok := v.normalize()
|
||||
if !ok {
|
||||
return rfx{}, false
|
||||
}
|
||||
cur := v
|
||||
|
||||
switch cur.td.Kind {
|
||||
case reflect.Struct:
|
||||
@@ -163,19 +163,8 @@ func (v rfx) step(seg string) (rfx, bool) {
|
||||
}
|
||||
return rfx{td: cur.td.Elem, ptr: p, writable: cur.writable}, true
|
||||
|
||||
case reflect.Map:
|
||||
// map 没有稳定布局, 退回 reflect
|
||||
m := valueAt(cur.td, cur.ptr)
|
||||
if m.IsNil() {
|
||||
return rfx{}, false
|
||||
}
|
||||
mv := tryMapFieldValue(m, seg)
|
||||
if !mv.IsValid() {
|
||||
return rfx{}, false
|
||||
}
|
||||
return fromReflect(mv, false)
|
||||
|
||||
default:
|
||||
// 含 Map —— walk 已经把它拦到 walkReflect 去了, 走不到这里
|
||||
return rfx{}, false
|
||||
}
|
||||
}
|
||||
@@ -188,8 +177,35 @@ func (v rfx) walk(it pathIter) (rfx, bool) {
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
var ok bool
|
||||
cur, ok = cur.step(seg)
|
||||
norm, ok := cur.normalize()
|
||||
if !ok {
|
||||
return rfx{}, false
|
||||
}
|
||||
|
||||
// 命中 map: 用 reflect 连续走完**这一段 map**, 只在离开时装箱一次。
|
||||
//
|
||||
// 两头都要顾: 逐跳走 step 的话每跳都要 boxCopy(map 元素不可寻址,
|
||||
// 而 rfx 的表示需要一个地址), 连续多层 map 会累积多次堆拷贝; 但如果
|
||||
// 一路 reflect 走到底, map 后面的 struct 跳就丢了偏移量快路径,
|
||||
// 退回 FieldByName。所以只在 map 段内用 reflect, 一离开就切回去。
|
||||
if norm.td.Kind == reflect.Map {
|
||||
rv, pending, exhausted := walkMapRun(valueAt(norm.td, norm.ptr), seg, &it)
|
||||
if exhausted {
|
||||
return fromReflect(rv, false)
|
||||
}
|
||||
// 路径还没走完但已经不是 map 了: 装箱一次, 回到快路径继续。
|
||||
// pending 是离开 map 段之后的第一跳, walkMapRun 没有消费它。
|
||||
cur, ok = fromReflect(rv, false)
|
||||
if !ok {
|
||||
return rfx{}, false
|
||||
}
|
||||
if cur, ok = cur.step(pending); !ok {
|
||||
return rfx{}, false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
cur, ok = norm.step(seg)
|
||||
if !ok {
|
||||
return rfx{}, false
|
||||
}
|
||||
@@ -197,6 +213,38 @@ func (v rfx) walk(it pathIter) (rfx, bool) {
|
||||
return cur.normalize()
|
||||
}
|
||||
|
||||
// walkMapRun 用 reflect 连续走完一段 map: 从 seg 这一跳开始, 只要落点仍是
|
||||
// map 就继续吃下一段, 一旦不是 map(或路径走完)就返回。
|
||||
//
|
||||
// exhausted 为 false 时表示路径还没走完但已经离开 map 段, 此时 pending 是
|
||||
// 尚未消费的下一跳; 调用方应当把返回值装箱一次, 切回基于偏移量的快路径 ——
|
||||
// map 后面如果跟着 struct, 那些跳仍然值得走快路径。
|
||||
//
|
||||
// 注意 pending 是**返回**给调用方的, 而不是通过 it 指针回退。
|
||||
// 写回(*it = save)会让逃逸分析认为 it 指向的内容可能逃逸, 进而把 Get 的
|
||||
// 可变参数切片也判定为逃逸 —— 那会波及所有路径, 连纯 struct 的零分配快路径
|
||||
// 都跟着退化成 1 次分配。
|
||||
func walkMapRun(v reflect.Value, seg string, it *pathIter) (_ reflect.Value, pending string, exhausted bool) {
|
||||
for {
|
||||
if !v.IsValid() || v.Kind() != reflect.Map {
|
||||
return reflect.Value{}, "", true
|
||||
}
|
||||
v = derefWithAccessor(tryMapFieldValue(v, seg))
|
||||
if !v.IsValid() {
|
||||
return reflect.Value{}, "", true
|
||||
}
|
||||
|
||||
next, more := it.next()
|
||||
if !more {
|
||||
return v, "", true
|
||||
}
|
||||
if v.Kind() != reflect.Map {
|
||||
return v, next, false
|
||||
}
|
||||
seg = next
|
||||
}
|
||||
}
|
||||
|
||||
// boxed 把栈上的遍历结果装箱成返回给调用方的 *rfx(唯一一次分配)。
|
||||
func boxed(v rfx, ok bool) R {
|
||||
if !ok {
|
||||
|
||||
Reference in New Issue
Block a user