perf: 用类型布局缓存 + 指针偏移寻址重写热路径

将实验目录 experiment/fastx 的实现合入主实现,替换原来逐次反射的做法。

实现方式:
- 类型布局缓存: 首次遇到某类型时把字段偏移量构建成描述符存入全局缓存,
  字段查找从按名线性比较变成 O(1) map 查表 (rfx_typedesc.go)
- 指针偏移寻址: 取字段时用 基址+偏移量 直接算地址, 不再构造中间 reflect.Value
  (unsafeptr.go, 全部 unsafe 代码集中在这一个文件)
- 零分配路径解析: 路径按需切片遍历, 不再为每次 Get 分配临时切片 (path.go)
- 标量直读: String/Int/Bool/Float64 在类型匹配时直接按机器类型读内存,
  绕开 interface 装箱和 cast 转换

语义复杂、调用频次低的操作(复合类型赋值、Append、Delete、容器转换)仍走原
reflect 实现, 保留为冷路径 refx (rfx_reflect.go), 避免重写引入行为偏差。

新增包级泛型函数 Get[T](r, path...), 按路径直接取出目标类型, 零内存分配。
语义与 r.Get(path...).Xxx() 严格等价, 不改动任何现有接口。

性能对比(同进程同数据, -benchmem -count=6 中位数):
- Get 嵌套 + String   150.6ns/7allocs -> 50.7ns/2allocs   2.97x
- Get 4 层深路径      234.1ns/10allocs -> 68.9ns/2allocs  3.40x
- Set 单层            65.8ns/2allocs  -> 18.1ns/0allocs   3.64x
- Set 嵌套           122.1ns/3allocs  -> 35.5ns/0allocs   3.44x
- Exists             127.4ns/5allocs  -> 37.2ns/1alloc    3.42x
- 访问器 String        15.0ns/1alloc   -> 1.3ns/0allocs   11.8x
- Get[string] 泛型直取                 -> 29.9ns/0allocs   5.04x
两处不快: map 键访问 1.25x(map 无稳定布局, 仍走 reflect);
New 构造 0.67x(多一次描述符缓存查找, 一次性成本)。

内存安全: 未导出字段在读和写两处显式拦截 —— reflect.NewAt 构造的 Value
不带只读标记, 语言层面的导出规则保护在 unsafe 路径上失效, 必须自己拦。
不变式与评审要点见 unsafeptr.go 顶部注释。

测试: 新增 703 个用例全部通过, 含 go test -race。
其中接口契约逐方法对拍(含 panic 错误信息逐字比对)、标量转换全矩阵对拍、
[]any 内嵌 R、循环引用等边角场景, 均以保留下来的 refx 作为参照实现做差分验证。
合并过程中据此发现并修复 11 处行为偏差。
This commit is contained in:
2026-08-28 16:37:04 +08:00
parent 1f48635da3
commit 5c9f8bd6e7
16 changed files with 4360 additions and 851 deletions
+299
View File
@@ -0,0 +1,299 @@
package reflux
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"reflect"
"sort"
"strings"
"testing"
"git.fsdpf.net/go/reflux/valuex"
)
// 两个之前没验证过的边角场景:
// 1. 深度嵌套的 []any 里放 R
// 2. 循环引用对象
// ---------------------------------------------------------------------------
// 1. []any / []R 里放 R
// ---------------------------------------------------------------------------
// mkR 用给定构造器建出一组嵌套的 R, 供两边分别构造同构的输入。
func mkR(newR func(any) R) (R, R) {
return newR(map[string]any{"n": "one", "deep": map[string]any{"k": "v"}}),
newR(map[string]any{"n": "two"})
}
func TestAnySliceContainingR(t *testing.T) {
cases := []struct {
name string
f func(newR func(any) R) any
}{
{"New([]any{R,R}) 长度", func(n func(any) R) any {
a, b := mkR(n)
return len(n([]any{a, b}).Array())
}},
{"New([]any{R,R}) 取元素", func(n func(any) R) any {
a, b := mkR(n)
return n([]any{a, b}).Get("0", "n").String()
}},
{"New([]R{R,R}) 取元素", func(n func(any) R) any {
a, b := mkR(n)
return n([]R{a, b}).Get("1", "n").String()
}},
{"嵌套一层 []any{[]any{R}}", func(n func(any) R) any {
a, _ := mkR(n)
return n([]any{[]any{a}}).Get("0", "0", "n").String()
}},
{"嵌套三层 + 穿透到 R 内部", func(n func(any) R) any {
a, _ := mkR(n)
return n([]any{[]any{[]any{a}}}).Get("0.0.0.deep.k").String()
}},
{"R 装在 map 里再嵌套", func(n func(any) R) any {
a, _ := mkR(n)
return n(map[string]any{"list": []any{a}}).Get("list", "0", "n").String()
}},
{"Array 元素再取值", func(n func(any) R) any {
a, b := mkR(n)
return n([]any{a, b}).Array()[1].Get("n").String()
}},
{"Slice()", func(n func(any) R) any {
a, b := mkR(n)
return fmt.Sprintf("%v", n([]any{a, b}).Slice())
}},
{"Keys 穿透到 R", func(n func(any) R) any {
a, _ := mkR(n)
// map 的 Keys 顺序在 Go 里不确定, 排序后再比
k := n([]any{a}).Get("0").Keys()
sort.Strings(k)
return k
}},
{"json.Marshal([]any{R})", func(n func(any) R) any {
a, _ := mkR(n)
b, err := json.Marshal(n([]any{a}))
return fmt.Sprintf("%s / err=%v", b, err)
}},
{"json.Marshal 嵌套两层", func(n func(any) R) any {
a, _ := mkR(n)
b, err := json.Marshal(n([]any{[]any{a}}))
return fmt.Sprintf("%s / err=%v", b, err)
}},
{"Set 字段为 []any{R,R}", func(n func(any) R) any {
a, b := mkR(n)
d := &struct{ Items []any }{}
n(d).Set("Items", []any{a, b})
return fmtDeref(d.Items)
}},
{"Set 字段为 []R", func(n func(any) R) any {
a, b := mkR(n)
d := &struct{ Items []any }{}
n(d).Set("Items", []R{a, b})
return fmtDeref(d.Items)
}},
{"Append R 到 []any", func(n func(any) R) any {
a, _ := mkR(n)
d := &struct{ Items []any }{}
n(d).Get("Items").Append(a)
return fmtDeref(d.Items)
}},
{"Scope 穿透 R", func(n func(any) R) any {
a, _ := mkR(n)
return n([]any{a}).Scope("0").Get("n").String()
}},
{"Exists 穿透 R", func(n func(any) R) any {
a, _ := mkR(n)
return n([]any{a}).Exists("0", "deep", "k")
}},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
want := callGen(newLegacy, c.f)
got := callGen(New, c.f)
if want != got {
t.Fatalf("行为不一致\n reflux = %s\n fastx = %s", want, got)
}
})
}
}
// fmtDeref 按内容格式化 []any, 把指针和 Accessor 解开。
//
// 两边存进去的都是指针, 直接 %v 比的是**地址**, 永远不会相等 ——
// 那是测试写法的问题, 不是行为差异。这里比内容。
func fmtDeref(items []any) string {
parts := make([]string, len(items))
for i, it := range items {
parts[i] = fmt.Sprintf("%v", derefAny(it))
}
return "[" + strings.Join(parts, " ") + "]"
}
func derefAny(v any) any {
if acc, ok := v.(valuex.Accessor); ok {
if raw := acc.Raw(); raw.IsValid() {
return derefReflectAny(raw)
}
return nil
}
rv := reflect.ValueOf(v)
return derefReflectAny(rv)
}
func derefReflectAny(rv reflect.Value) any {
for rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface {
if rv.IsNil() {
return nil
}
rv = rv.Elem()
}
if !rv.IsValid() {
return nil
}
return rv.Interface()
}
// callGen 与 contract_test.go 的 call 同理, 但把构造器也参数化,
// 保证两边拿到的是**各自构造**的同构输入(而不是共用 reflux 造的 R)。
func callGen(newR func(any) R, f func(func(any) R) any) string {
var out string
func() {
defer func() {
if rec := recover(); rec != nil {
out = "PANIC: " + fmt.Sprintf("%v", rec)
}
}()
out = fmt.Sprintf("%v", f(newR))
}()
return out
}
// ---------------------------------------------------------------------------
// 2. 循环引用
// ---------------------------------------------------------------------------
type cycNode struct {
Name string
Next *cycNode
Self any
}
func newCycle() *cycNode {
n := &cycNode{Name: "a"}
n.Next = n
n.Self = n
return n
}
func TestCircularReference(t *testing.T) {
cases := []struct {
name string
f func(newR func(any) R) any
}{
{"沿指针环 Get 一圈", func(n func(any) R) any {
return n(newCycle()).Get("Next", "Next", "Next", "Name").String()
}},
{"点号路径绕环", func(n func(any) R) any {
return n(newCycle()).Get("Next.Next.Next.Next.Name").String()
}},
{"经 interface 字段绕环", func(n func(any) R) any {
return n(newCycle()).Get("Self", "Self", "Name").String()
}},
{"环上 Set", func(n func(any) R) any {
c := newCycle()
n(c).Set("Next.Next.Name", "changed")
return c.Name // 环回自身, 应该改到同一个对象
}},
{"Exists 绕环", func(n func(any) R) any {
return n(newCycle()).Exists("Next", "Next", "Next", "Name")
}},
{"Keys 绕环", func(n func(any) R) any {
return n(newCycle()).Get("Next", "Next").Keys()
}},
{"json.Marshal 循环对象", func(n func(any) R) any {
b, err := json.Marshal(n(newCycle()))
return fmt.Sprintf("%s / err=%v", b, cycleErrText(err))
}},
{"json.Marshal 环上某一跳", func(n func(any) R) any {
b, err := json.Marshal(n(newCycle()).Get("Next"))
return fmt.Sprintf("%s / err=%v", b, cycleErrText(err))
}},
{"map 自引用 Get", func(n func(any) R) any {
m := map[string]any{"name": "m"}
m["self"] = m
return n(&m).Get("self", "self", "name").String()
}},
{"map 自引用 json.Marshal", func(n func(any) R) any {
m := map[string]any{"name": "m"}
m["self"] = m
b, err := json.Marshal(n(&m))
return fmt.Sprintf("%s / err=%v", b, cycleErrText(err))
}},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
want := callGen(newLegacy, c.f)
got := callGen(New, c.f)
if want != got {
t.Fatalf("行为不一致\n reflux = %s\n fastx = %s", want, got)
}
})
}
}
// cycleErrText 把错误里的具体类型名抹掉 —— reflux 报的是 *reflux.rfx,
// fastx 报的是 *fastx.Value, 这属于实现细节而非行为差异。
// 保留"是否检测到环"这个关键信息。
func cycleErrText(err error) string {
if err == nil {
return "<nil>"
}
s := err.Error()
if strings.Contains(s, "encountered a cycle") {
return "cycle-detected"
}
return s
}
// ---------------------------------------------------------------------------
// DeepClone 遇到循环引用会栈溢出(fatal, 不可 recover), 只能在子进程里验证。
// 这是 reflux 既有的限制, fastx 复用 reflux.DeepClone 所以行为一致。
// ---------------------------------------------------------------------------
func TestDeepCloneOnCycleIsFatalForBoth(t *testing.T) {
if impl := os.Getenv("FASTX_CYCLE_IMPL"); impl != "" {
// 子进程: 真的去做深拷贝, 预期栈溢出
n := newCycle()
if impl == "reflux" {
newLegacy(n).Scope()
} else {
New(n).Scope()
}
return
}
for _, impl := range []string{"reflux", "fastx"} {
t.Run(impl, func(t *testing.T) {
cmd := exec.Command(os.Args[0], "-test.run=^TestDeepCloneOnCycleIsFatalForBoth$", "-test.timeout=60s")
cmd.Env = append(os.Environ(), "FASTX_CYCLE_IMPL="+impl)
out, err := cmd.CombinedOutput()
if err == nil {
t.Fatalf("%s: 预期栈溢出, 却正常返回了", impl)
}
if !strings.Contains(string(out), "stack overflow") {
t.Fatalf("%s: 预期 stack overflow, 实际输出:\n%s", impl, truncate(string(out), 300))
}
})
}
}
func truncate(s string, n int) string {
if len(s) > n {
return s[:n] + "..."
}
return s
}