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:
+215
@@ -0,0 +1,215 @@
|
||||
// 类型布局描述符: 把 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
|
||||
}
|
||||
Reference in New Issue
Block a user