将实验目录 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 处行为偏差。
159 lines
4.6 KiB
Go
159 lines
4.6 KiB
Go
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)
|
|
}
|
|
}
|