Files
reflux/unsafeptr.go
T
what 5c9f8bd6e7 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 处行为偏差。
2026-08-28 16:37:04 +08:00

126 lines
5.4 KiB
Go

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 }