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:
+125
@@ -0,0 +1,125 @@
|
||||
package reflux
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// 本文件是整个 reflux 包里唯一使用 unsafe 的地方, 评审内存安全性时只需要看这里
|
||||
// 加上 value.go 里对这些函数的调用点。
|
||||
//
|
||||
// 本包 unsafe 路径的核心不变式(invariant), 下面每个函数都依赖它:
|
||||
//
|
||||
// I1. value.ptr 永远是一个"指向某个 value.td.rtype 类型值的合法地址",
|
||||
// 要么来自 reflect.Value.UnsafePointer()/reflect.New(), 要么由
|
||||
// fieldAt/elemAt 从一个满足 I1 的地址加上该类型真实的字段偏移量算出。
|
||||
// I2. ptr 是 unsafe.Pointer 类型而不是 uintptr, GC 能正常追踪它,
|
||||
// 被指向的对象不会在 value 存活期间被回收或移动。
|
||||
// 所有指针运算都用 unsafe.Add 在单个表达式内完成, 不把地址存成 uintptr。
|
||||
// I3. 只有 Exported == true 的字段才允许构造出 value, 见 rfxFieldDesc.Exported
|
||||
// 的注释: reflect.NewAt 不带只读标记, 语言层面的未导出字段保护在这里失效,
|
||||
// 必须由调用方自己拦住。
|
||||
// I4. 只有 writable == true 的 value 才允许写入, writable 的传播规则见
|
||||
// value.go 的 canWrite 说明。
|
||||
|
||||
// fieldAt 返回 base 所指 struct 里偏移量为 off 的字段地址。
|
||||
// 要求: base 满足 I1, off 来自 reflect.StructField.Offset。
|
||||
func fieldAt(base unsafe.Pointer, off uintptr) unsafe.Pointer {
|
||||
return unsafe.Add(base, off)
|
||||
}
|
||||
|
||||
// loadPtr 把 p 当作 *T 读出来, 返回它指向的地址。
|
||||
// 要求: p 指向一个指针类型的字段。返回 nil 表示空指针。
|
||||
func loadPtr(p unsafe.Pointer) unsafe.Pointer {
|
||||
return *(*unsafe.Pointer)(p)
|
||||
}
|
||||
|
||||
// sliceHeader 与运行时 slice 的内存布局一致, 用于零拷贝读取切片头。
|
||||
type sliceHeader struct {
|
||||
Data unsafe.Pointer
|
||||
Len int
|
||||
Cap int
|
||||
}
|
||||
|
||||
// sliceLen 返回 p 所指切片的长度。要求 p 指向一个 slice 类型的值。
|
||||
func sliceLen(p unsafe.Pointer) int {
|
||||
return (*sliceHeader)(p).Len
|
||||
}
|
||||
|
||||
// sliceElemAt 返回 p 所指切片第 i 个元素的地址, 越界返回 nil。
|
||||
// 要求: elemSize 是该切片元素类型的真实 Size()。
|
||||
func sliceElemAt(p unsafe.Pointer, i int, elemSize uintptr) unsafe.Pointer {
|
||||
h := (*sliceHeader)(p)
|
||||
if i < 0 || i >= h.Len {
|
||||
return nil
|
||||
}
|
||||
return unsafe.Add(h.Data, uintptr(i)*elemSize)
|
||||
}
|
||||
|
||||
// arrayElemAt 返回 p 所指数组第 i 个元素的地址, 越界返回 nil。
|
||||
func arrayElemAt(p unsafe.Pointer, i int, n int, elemSize uintptr) unsafe.Pointer {
|
||||
if i < 0 || i >= n {
|
||||
return nil
|
||||
}
|
||||
return unsafe.Add(p, uintptr(i)*elemSize)
|
||||
}
|
||||
|
||||
// valueAt 把 (地址, 类型) 还原成一个可寻址的 reflect.Value, 用于回退到
|
||||
// 标准反射路径(类型转换、JSON、map 操作等)。
|
||||
//
|
||||
// 安全性: 返回的 Value 是可寻址且可写的, 即使它对应的是未导出字段 ——
|
||||
// 这正是 I3 存在的原因, 调用方必须已经确认过 Exported。
|
||||
func valueAt(td *rfxTypeDesc, p unsafe.Pointer) reflect.Value {
|
||||
return reflect.NewAt(td.rtype, p).Elem()
|
||||
}
|
||||
|
||||
// ptrValueAt 把 (地址, 类型) 还原成一个 *T 的 reflect.Value。
|
||||
func ptrValueAt(td *rfxTypeDesc, p unsafe.Pointer) reflect.Value {
|
||||
return reflect.NewAt(td.rtype, p)
|
||||
}
|
||||
|
||||
// addrOf 取一个可寻址 reflect.Value 的地址。要求 v.CanAddr()。
|
||||
func addrOf(v reflect.Value) unsafe.Pointer {
|
||||
return v.Addr().UnsafePointer()
|
||||
}
|
||||
|
||||
// boxCopy 把一个不可寻址的值(典型来源: map 取值、interface 拆包)拷贝到
|
||||
// 新分配的堆内存上, 返回该副本的地址。副本与原值不共享内存, 因此对它的写入
|
||||
// 不会影响原容器 —— 这与 reflect 里 "map 元素不可寻址" 的语义一致。
|
||||
func boxCopy(v reflect.Value) unsafe.Pointer {
|
||||
p := reflect.New(v.Type())
|
||||
p.Elem().Set(v)
|
||||
return p.UnsafePointer()
|
||||
}
|
||||
|
||||
// ---- 基础类型的直接读写 ----
|
||||
// 这些函数通过 *T 做**有类型**的读写, 编译器会正常插入 GC 写屏障,
|
||||
// 不存在漏掉写屏障导致对象被误回收的问题。unsafe 只体现在"地址是算出来的"。
|
||||
|
||||
func loadString(p unsafe.Pointer) string { return *(*string)(p) }
|
||||
func storeString(p unsafe.Pointer, v string) { *(*string)(p) = v }
|
||||
|
||||
func loadInt(p unsafe.Pointer) int { return *(*int)(p) }
|
||||
func storeInt(p unsafe.Pointer, v int) { *(*int)(p) = v }
|
||||
|
||||
// 各宽度整数的直读。读取只依赖字段的 Kind(内存宽度), 不要求具名类型完全一致 ——
|
||||
// type Age int 和 int 的内存表示相同, 按 *int 读出来的值是一样的。
|
||||
// (写入则不同, 见 storeFast: 写要求类型完全一致, 否则可能绕过 cast 的转换语义。)
|
||||
func loadInt8(p unsafe.Pointer) int8 { return *(*int8)(p) }
|
||||
func loadInt16(p unsafe.Pointer) int16 { return *(*int16)(p) }
|
||||
func loadInt32(p unsafe.Pointer) int32 { return *(*int32)(p) }
|
||||
func loadInt64(p unsafe.Pointer) int64 { return *(*int64)(p) }
|
||||
|
||||
func loadUint(p unsafe.Pointer) uint { return *(*uint)(p) }
|
||||
func loadUint8(p unsafe.Pointer) uint8 { return *(*uint8)(p) }
|
||||
func loadUint16(p unsafe.Pointer) uint16 { return *(*uint16)(p) }
|
||||
func loadUint32(p unsafe.Pointer) uint32 { return *(*uint32)(p) }
|
||||
func loadUint64(p unsafe.Pointer) uint64 { return *(*uint64)(p) }
|
||||
|
||||
func loadFloat32(p unsafe.Pointer) float32 { return *(*float32)(p) }
|
||||
|
||||
func loadBool(p unsafe.Pointer) bool { return *(*bool)(p) }
|
||||
func storeBool(p unsafe.Pointer, v bool) { *(*bool)(p) = v }
|
||||
|
||||
func loadFloat64(p unsafe.Pointer) float64 { return *(*float64)(p) }
|
||||
func storeFloat64(p unsafe.Pointer, v float64) { *(*float64)(p) = v }
|
||||
Reference in New Issue
Block a user