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
+158
View File
@@ -0,0 +1,158 @@
package reflux
import (
"fmt"
"testing"
)
// 标量直读快路径必须与参照实现 refx 逐格等价 —— 包括溢出截断、负数转无符号 panic
// 这些边角行为。这里对"字段类型 × 访问器方法"做全矩阵对拍。
type myStr string
type myInt int
type myBool bool
type myFloat float64
type convBox struct {
Str string
NamedS myStr
I int
NamedI myInt
I8 int8
I16 int16
I32 int32
I64 int64
U uint
U8 uint8
U16 uint16
U32 uint32
U64 uint64
F32 float32
F64 float64
B bool
NamedB myBool
NamedF myFloat
Neg int // 负数: 转无符号应该 panic
Over int // 300: 读成 Int8 应该截断成 44
FracF float64 // 3.9: 读成整数应该截断成 3
NumStr string // "123": 字符串转数字
BadStr string // "abc": 转数字应该 panic
PtrStr *string
AnyVal any
}
func newConvBox() *convBox {
s := "viaptr"
return &convBox{
Str: "hello", NamedS: "named",
I: 42, NamedI: 7,
I8: -8, I16: -16, I32: -32, I64: -64,
U: 1, U8: 2, U16: 3, U32: 4, U64: 5,
F32: 1.5, F64: 2.5,
B: true, NamedB: true, NamedF: 3.5,
Neg: -1, Over: 300, FracF: 3.9,
NumStr: "123", BadStr: "abc",
PtrStr: &s, AnyVal: 99,
}
}
// accessors 是所有会做类型转换的访问器方法
var accessors = []struct {
name string
call func(R) any
}{
{"String", func(r R) any { return r.String() }},
{"Bool", func(r R) any { return r.Bool() }},
{"Int", func(r R) any { return r.Int() }},
{"Int8", func(r R) any { return r.Int8() }},
{"Int16", func(r R) any { return r.Int16() }},
{"Int32", func(r R) any { return r.Int32() }},
{"Int64", func(r R) any { return r.Int64() }},
{"Uint", func(r R) any { return r.Uint() }},
{"Uint8", func(r R) any { return r.Uint8() }},
{"Uint16", func(r R) any { return r.Uint16() }},
{"Uint32", func(r R) any { return r.Uint32() }},
{"Uint64", func(r R) any { return r.Uint64() }},
{"Float32", func(r R) any { return r.Float32() }},
{"Float64", func(r R) any { return r.Float64() }},
{"Any", func(r R) any { return r.Any() }},
}
var convFields = []string{
"Str", "NamedS", "I", "NamedI", "I8", "I16", "I32", "I64",
"U", "U8", "U16", "U32", "U64", "F32", "F64",
"B", "NamedB", "NamedF", "Neg", "Over", "FracF",
"NumStr", "BadStr", "PtrStr", "AnyVal",
}
// callSafe 调用访问器, 把 panic 也当成一种"结果"记录下来。
func callSafe(r R, f func(R) any) (result any, panicked bool) {
defer func() {
if rec := recover(); rec != nil {
panicked = true
}
}()
return f(r), false
}
func TestScalarFastPathMatchesReflux(t *testing.T) {
box := newConvBox()
rx := newLegacy(box)
fx := New(box)
for _, field := range convFields {
for _, acc := range accessors {
t.Run(field+"/"+acc.name, func(t *testing.T) {
wantVal, wantPanic := callSafe(rx.Get(field), acc.call)
gotVal, gotPanic := callSafe(fx.Get(field), acc.call)
if wantPanic != gotPanic {
t.Fatalf("panic 行为不一致: reflux panic=%v, fastx panic=%v (值 reflux=%#v fastx=%#v)",
wantPanic, gotPanic, wantVal, gotVal)
}
if wantPanic {
return // 两边都 panic, 一致
}
if fmt.Sprintf("%v", wantVal) != fmt.Sprintf("%v", gotVal) {
t.Fatalf("结果不一致: reflux=%#v, fastx=%#v", wantVal, gotVal)
}
})
}
}
}
// 单独把几个最容易出错的边角行为钉死
func TestScalarEdgeCases(t *testing.T) {
box := newConvBox()
fx := New(box)
rx := newLegacy(box)
// 整数溢出静默截断: 300 -> int8 = 44
if got, want := fx.Get("Over").Int8(), rx.Get("Over").Int8(); got != want || got != 44 {
t.Fatalf("溢出截断: fastx=%d reflux=%d, 期望 44", got, want)
}
// 负数转无符号: 两边都必须 panic
if _, p1 := callSafe(fx.Get("Neg"), func(r R) any { return r.Uint8() }); !p1 {
t.Fatal("负数转 Uint8 应该 panic")
}
// 浮点截断: 3.9 -> int = 3
if got, want := fx.Get("FracF").Int(), rx.Get("FracF").Int(); got != want || got != 3 {
t.Fatalf("浮点截断: fastx=%d reflux=%d, 期望 3", got, want)
}
// 字符串转数字仍然可用(走 cast 回退)
if got := fx.Get("NumStr").Int(); got != 123 {
t.Fatalf("字符串转数字: %d", got)
}
// 非数字字符串转数字: 两边都 panic
if _, p := callSafe(fx.Get("BadStr"), func(r R) any { return r.Int() }); !p {
t.Fatal("\"abc\" 转 Int 应该 panic")
}
// 具名类型走快路径, 结果与 reflux 一致
if got, want := fx.Get("NamedS").String(), rx.Get("NamedS").String(); got != want {
t.Fatalf("具名 string: fastx=%q reflux=%q", got, want)
}
// 指针字段自动解引用后仍能直读
if got := fx.Get("PtrStr").String(); got != "viaptr" {
t.Fatalf("指针字段: %q", got)
}
}