Files
reflux/rfx_typedesc.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

216 lines
6.6 KiB
Go

// 类型布局描述符: 把 struct 的字段偏移量、元素类型大小等信息构建一次并全局缓存,
// 热路径上不再走 reflect 的按名字段查找。
package reflux
import (
"reflect"
"sync"
)
// rfxFieldDesc 描述一个字段在其所属 struct 里的位置和类型。
type rfxFieldDesc struct {
// Name 字段名(原始声明名)
Name string
// Offset 字段相对于 struct 起始地址的字节偏移量
Offset uintptr
// Type 字段类型的描述符, 递归引用以支持嵌套 struct
Type *rfxTypeDesc
// Exported 字段是否是导出字段。
//
// 这是本原型最关键的安全标记: reflect.NewAt 构造出来的 Value 是可寻址且
// **可写**的, 它不带 reflect 正常路径上的 flagRO 只读标记, 也就是说 Go 语言
// 层面"不能修改未导出字段"这条保护在 unsafe 路径上完全失效。因此必须由我们
// 自己在每一次 Get/Set 前显式拒绝未导出字段。
Exported bool
// index 在 reflect.Type 里的字段下标, 回退到 reflect 路径时使用
index int
}
// rfxTypeDesc 描述一个类型的内存布局, 构建一次之后全局复用。
type rfxTypeDesc struct {
rtype reflect.Type
Kind reflect.Kind
Size uintptr
// fields 按声明顺序保存所有字段, 用于线性扫描查找
fields []rfxFieldDesc
// fieldMap 按字段名索引, 用于 map 查找。
// 除了原始字段名, 还额外收录了"首字母小写"的别名, 以兼容 reflux 的
// tryStructFieldValue 行为(Get("address") 能命中 Address 字段)。
fieldMap map[string]*rfxFieldDesc
// Elem 是 ptr/slice/array 的元素类型描述符
Elem *rfxTypeDesc
// elemSize 是元素类型的字节大小, slice/array 下标寻址用
elemSize uintptr
// arrayLen 是数组长度(仅 Kind == Array 时有效)
arrayLen int
// ptrElemKind 缓存指针指向类型的 Kind, 避免热路径上再取一次
ptrElemKind reflect.Kind
// fast 标记该类型是否是可以直接按机器类型读写的常见标量类型。
// 热路径上只要比较一个字节, 就能决定走不走零分配快路径。
fast fastKind
}
// fastKind 标记"类型与 Go 内置标量完全一致"的情况。
// 注意必须是**完全一致**: type MyStr string 的 Kind 也是 String, 但直接按
// string 写入会绕过 reflux 原有的 cast 转换语义, 所以这里只认原生类型。
type fastKind uint8
const (
fkNone fastKind = iota
fkString
fkInt
fkBool
fkFloat64
)
var (
typeString = reflect.TypeOf("")
typeInt = reflect.TypeOf(int(0))
typeBool = reflect.TypeOf(false)
typeFloat64 = reflect.TypeOf(float64(0))
)
func fastKindOf(t reflect.Type) fastKind {
switch t {
case typeString:
return fkString
case typeInt:
return fkInt
case typeBool:
return fkBool
case typeFloat64:
return fkFloat64
}
return fkNone
}
// isScalar 报告该类型是不是可以用 cast 直接转换的标量。
// 只有标量目标才走 fastx 自己的赋值逻辑; 复合类型(指针/切片/结构体/map/interface)
// 的赋值语义由 reflux.setValue 定义, 重新实现必然产生偏差, 一律交回它处理。
func (td *rfxTypeDesc) isScalar() bool {
switch td.Kind {
case reflect.String, reflect.Bool,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Uintptr, reflect.Float32, reflect.Float64:
return true
}
return false
}
// lookupField 用 map 查找字段, 未找到返回 nil。
func (td *rfxTypeDesc) lookupField(name string) *rfxFieldDesc {
return td.fieldMap[name]
}
var (
// descCache 是全局类型描述符缓存。reflect.Type 本身可比较, 直接做 key。
descCache sync.Map // map[reflect.Type]*rfxTypeDesc
// descMu 只在缓存未命中(冷路径)时加锁, 保证同一时刻只有一个构建者,
// 并且保证带环的类型图整体构建完成后才发布, 读者永远看不到半成品。
descMu sync.Mutex
)
// rfxDescriptorOf 返回类型 t 的布局描述符, 第一次遇到时构建, 之后命中缓存。
func rfxDescriptorOf(t reflect.Type) *rfxTypeDesc {
if v, ok := descCache.Load(t); ok {
return v.(*rfxTypeDesc)
}
descMu.Lock()
defer descMu.Unlock()
// 双重检查: 可能在抢锁期间已经被别的 goroutine 构建好了
if v, ok := descCache.Load(t); ok {
return v.(*rfxTypeDesc)
}
// pending 保存本次构建过程中新建的所有描述符。自引用类型
// (type Node struct{ Next *Node }) 靠它终止递归。
pending := make(map[reflect.Type]*rfxTypeDesc, 8)
td := buildLocked(t, pending)
// 整张类型图都填充完毕之后再统一发布。sync.Map.Store 提供的
// happens-before 保证了读者 Load 到任何一个描述符时, 它引用到的
// 其它描述符也已经写完。
for k, v := range pending {
descCache.Store(k, v)
}
return td
}
// buildLocked 在持有 descMu 的前提下递归构建描述符。
func buildLocked(t reflect.Type, pending map[reflect.Type]*rfxTypeDesc) *rfxTypeDesc {
if v, ok := descCache.Load(t); ok {
return v.(*rfxTypeDesc)
}
if td, ok := pending[t]; ok {
return td
}
td := &rfxTypeDesc{
rtype: t,
Kind: t.Kind(),
Size: t.Size(),
fast: fastKindOf(t),
}
// 先登记再递归, 否则自引用类型会栈溢出
pending[t] = td
switch t.Kind() {
case reflect.Struct:
n := t.NumField()
td.fields = make([]rfxFieldDesc, n)
td.fieldMap = make(map[string]*rfxFieldDesc, n*2)
for i := 0; i < n; i++ {
sf := t.Field(i)
td.fields[i] = rfxFieldDesc{
Name: sf.Name,
Offset: sf.Offset,
Type: buildLocked(sf.Type, pending),
Exported: sf.IsExported(),
index: i,
}
}
for i := range td.fields {
f := &td.fields[i]
td.fieldMap[f.Name] = f
}
// 追加"首字母小写"别名, 兼容 reflux 的大小写宽松查找。
// 只在不与真实字段名冲突时才登记。
for i := range td.fields {
f := &td.fields[i]
if len(f.Name) > 0 && f.Name[0] >= 'A' && f.Name[0] <= 'Z' {
alias := string(f.Name[0]+32) + f.Name[1:]
if _, exists := td.fieldMap[alias]; !exists {
td.fieldMap[alias] = f
}
}
}
case reflect.Ptr:
td.Elem = buildLocked(t.Elem(), pending)
td.ptrElemKind = t.Elem().Kind()
case reflect.Slice:
td.Elem = buildLocked(t.Elem(), pending)
td.elemSize = t.Elem().Size()
case reflect.Array:
td.Elem = buildLocked(t.Elem(), pending)
td.elemSize = t.Elem().Size()
td.arrayLen = t.Len()
case reflect.Map:
// map 没有稳定的内存布局可以做偏移量运算, 描述符只记录元素类型,
// 实际取值走 reflect 回退路径。
td.Elem = buildLocked(t.Elem(), pending)
}
return td
}