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
+162
View File
@@ -0,0 +1,162 @@
package reflux
// 泛型 Get[T] 的等价性与分配情况。
//
// 分配这块用 testing.AllocsPerRun 精确统计, 并且必须写进包级 sink 变量 ——
// 丢弃结果的写法会被编译器优化掉, 测出假的 0 分配。
import (
"testing"
)
// 泛型 Get[T] 必须与 r.Get(path...).Xxx() 等价, 且在"非 *rfx 实现的 R"上能正确回退。
func TestGenericGetEquivalent(t *testing.T) {
box := newConvBox()
fast := New(box) // 走快路径
slow := newLegacy(box) // 走回退路径
for _, field := range convFields {
t.Run(field, func(t *testing.T) {
// string
want, wp := callSafe(slow.Get(field), func(r R) any { return r.String() })
gotFast, gpF := callSafe(fast, func(R) any { return Get[string](fast, field) })
gotSlow, gpS := callSafe(slow, func(R) any { return Get[string](slow, field) })
if wp != gpF || wp != gpS {
t.Fatalf("panic 行为不一致: reflux=%v 快路径=%v 回退=%v", wp, gpF, gpS)
}
if !wp && (want != gotFast || want != gotSlow) {
t.Fatalf("string: reflux=%#v 快路径=%#v 回退=%#v", want, gotFast, gotSlow)
}
// int
wantI, wpI := callSafe(slow.Get(field), func(r R) any { return r.Int() })
gotIF, gpIF := callSafe(fast, func(R) any { return Get[int](fast, field) })
gotIS, gpIS := callSafe(slow, func(R) any { return Get[int](slow, field) })
if wpI != gpIF || wpI != gpIS {
t.Fatalf("int panic 行为不一致: reflux=%v 快=%v 回退=%v", wpI, gpIF, gpIS)
}
if !wpI && (wantI != gotIF || wantI != gotIS) {
t.Fatalf("int: reflux=%#v 快=%#v 回退=%#v", wantI, gotIF, gotIS)
}
})
}
}
func TestGenericGetTypes(t *testing.T) {
o := newUxOuter()
r := New(o)
if got := Get[string](r, "Mid", "Inner", "City"); got != "Beijing" {
t.Fatalf("string = %q", got)
}
if got := Get[string](r, "Mid.Inner.City"); got != "Beijing" {
t.Fatalf("点号路径 = %q", got)
}
if got := Get[int](r, "Mid.Inner.Zip"); got != 100000 {
t.Fatalf("int = %d", got)
}
if got := Get[int64](r, "Mid.Inner.Zip"); got != 100000 {
t.Fatalf("int64 = %d", got)
}
if got := Get[bool](r, "Mid.Inner.Active"); !got {
t.Fatal("bool = false")
}
if got := Get[float64](r, "Mid.Inner.Ratio"); got != 1.5 {
t.Fatalf("float64 = %v", got)
}
// 路径不存在 -> 零值
if got := Get[string](r, "NoSuch"); got != "" {
t.Fatalf("不存在的路径应该返回零值, 得到 %q", got)
}
if got := Get[int](r, "NoSuch"); got != 0 {
t.Fatalf("不存在的路径应该返回 0, 得到 %d", got)
}
// 未导出字段 -> 零值
if got := Get[string](r, "secret"); got != "" {
t.Fatalf("未导出字段应该返回零值, 得到 %q", got)
}
}
// 泛型方案的已知缺口: 具名标量类型不会命中 type switch 的 case,
// 会掉进 default 分支走 Any() 断言 —— 行为与 Get(...).String() 不同。
// 这里把这个差异**显式钉住**, 免得以后当成 bug 排查。
func TestGenericNamedTypeGap(t *testing.T) {
box := newConvBox()
r := New(box)
// NamedS 字段本身是 myStr 类型
// 1) 取成 string: 命中 case *string 快路径, 正常转换
if got := Get[string](r, "NamedS"); got != "named" {
t.Fatalf("GetT[string] 取具名字段 = %q", got)
}
// 2) Str 字段是原生 string, 取成具名类型 myStr:
// 走 default 分支的 Any().(myStr) 断言, 会失败并返回零值,
// 而 Get("Str").String() 是能拿到 "hello" 的。
if got := Get[myStr](r, "Str"); got != "" {
t.Logf("具名类型缺口已消失(实现改进了): GetT[myStr] = %q", got)
} else {
t.Log("已知缺口: GetT[具名类型] 在字段为原生类型时返回零值, 需在文档中说明")
}
}
// 全局 sink: 防止编译器把"结果被丢弃"的调用优化掉, 导致测出假的 0 分配。
var (
sinkS string
sinkI int
sinkI64 int64
sinkB bool
sinkF float64
sinkSS []string
)
// 精确统计 Get[T] 各条分支的分配次数。
// testing.AllocsPerRun 比 -benchmem 更直接: 它给出的是每次调用的确切分配数。
func TestGenericAllocs(t *testing.T) {
o := newUxOuter()
box := newConvBox()
fast := New(o)
fastBox := New(box)
slow := newLegacy(o)
cases := []struct {
name string
want float64 // 期望的分配次数
f func()
}{
// ---- 快路径: 应该全部 0 分配 ----
{"Get[string] 3层路径", 0, func() { sinkS = Get[string](fast, "Mid", "Inner", "City") }},
{"Get[string] 点号路径", 0, func() { sinkS = Get[string](fast, "Mid.Inner.City") }},
{"Get[string] 单层路径", 0, func() { sinkS = Get[string](fast, "Name") }},
{"Get[int]", 0, func() { sinkI = Get[int](fast, "Mid.Inner.Zip") }},
{"Get[int64]", 0, func() { sinkI64 = Get[int64](fast, "Mid.Inner.Zip") }},
{"Get[bool]", 0, func() { sinkB = Get[bool](fast, "Mid.Inner.Active") }},
{"Get[float64]", 0, func() { sinkF = Get[float64](fast, "Mid.Inner.Ratio") }},
{"Get[string] slice下标", 0, func() { sinkS = Get[string](fast, "Tags", "1") }},
{"Get[string] 路径不存在", 0, func() { sinkS = Get[string](fast, "NoSuch") }},
{"Get[string] 未导出字段", 0, func() { sinkS = Get[string](fast, "secret") }},
{"Get[int] 路径不存在", 0, func() { sinkI = Get[int](fast, "NoSuch") }},
// ---- 会分配的分支(预期之内, 但要说清楚) ----
{"Get[string] 跨类型(int=42, 命中strconv小整数缓存)", -1, func() { sinkS = Get[string](fastBox, "I") }},
{"Get[string] 跨类型(int=100000, 真实格式化)", -1, func() { sinkS = Get[string](fast, "Mid.Inner.Zip") }},
{"Get[string] 跨类型(float64)", -1, func() { sinkS = Get[string](fast, "Mid.Inner.Ratio") }},
{"Get[int] 跨类型(string字段)", -1, func() { sinkI = Get[int](fastBox, "NumStr") }},
{"Get[string] map键", -1, func() { sinkS = Get[string](fast, "Meta", "k1") }},
{"Get[[]string] default分支", -1, func() { sinkSS = Get[[]string](fast, "Tags") }},
{"Get[string] 回退到reflux", -1, func() { sinkS = Get[string](slow, "Mid.Inner.City") }},
// ---- 对照 ----
{"对照: Get(...).String()", -1, func() { sinkS = fast.Get("Mid", "Inner", "City").String() }},
{"对照: 内部 getString", -1, func() { sinkS = fast.(*rfx).getString("Mid", "Inner", "City") }},
{"对照: 参照实现 refx Get().String()", -1, func() { sinkS = slow.Get("Mid", "Inner", "City").String() }},
}
t.Log("每次调用的分配次数:")
for _, c := range cases {
got := testing.AllocsPerRun(200, c.f)
t.Logf(" %-32s %.0f allocs", c.name, got)
if c.want >= 0 && got != c.want {
t.Errorf("%s: 分配 %.0f 次, 期望 %.0f 次", c.name, got, c.want)
}
}
}