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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user