docs: 说明类型描述符缓存的生命周期与 reflect.StructOf 的注意事项
补充两个评审/接手时大概率会问到的问题: 1. 缓存什么时候清除 —— 永不清除, 正常情况下也不需要。key 是 reflect.Type, 程序里声明的类型是编译期确定的有限集合, 缓存大小 warmup 之后收敛。 附实测占用(1000 个类型约 0.6 MB), 并说明只增不删是有意设计: 描述符指针永久有效, 递归引用不需要生命周期管理。 2. 唯一的例外是调用方用 reflect.StructOf 在运行时动态造类型(典型是 ORM 按 查询字段拼扫描目标 struct)。实测 12 字段的动态类型每个约 3315 字节, 其中 reflux 占 59%、Go runtime 自己的类型元数据占 41% —— 后者同样永不回收, 所以这不是 reflux 特有的问题。 同时给出两条实践建议: 优先在上游收敛字段组合并缓存类型(reflect.StructOf 即使命中 runtime 缓存也要 2μs / 30 次分配); 以及注意字段顺序 —— 字段集合相同但顺序不同会产生不同的类型, 拼字段时遍历 map 会因为迭代顺序 随机而每次造出新类型, 那才是真正会导致无界增长的 bug。
This commit is contained in:
@@ -1251,6 +1251,62 @@ type Reflux interface {
|
||||
`Get` 剩下的 2 次分配是 API 形状决定的下限: 一次是返回的 `R` 包装对象(24 字节),
|
||||
一次是可变参数切片(通过接口调用时逃逸分析穿不透)。想完全避免就用 `Get[T]`。
|
||||
|
||||
### 类型描述符缓存:什么时候清除
|
||||
|
||||
**永不清除,正常情况下也不需要清除。**
|
||||
|
||||
缓存的 key 是 `reflect.Type`。程序里**声明**的类型是编译期确定的有限集合,
|
||||
runtime 为它们创建的 `reflect.Type` 本身就是永久对象,所以缓存大小收敛于
|
||||
"程序实际用到的类型数",warmup 之后不再增长。
|
||||
|
||||
实测占用(描述符 + 字段切片 + 字段名索引):
|
||||
|
||||
| 类型数 | 缓存占用 |
|
||||
|---|---|
|
||||
| 100 | 约 0.06 MB |
|
||||
| 1,000 | 约 0.6 MB |
|
||||
| 10,000 | 约 6 MB |
|
||||
|
||||
一个中等项目撑死几百个类型,占用在百 KB 级别。
|
||||
|
||||
设计上有意做成**只增不删**:描述符指针一旦发布就永久有效,
|
||||
`Elem`/`Type` 这些递归引用不需要任何生命周期管理,热路径上也不必做
|
||||
引用计数或有效性校验。
|
||||
|
||||
#### 唯一的例外:`reflect.StructOf` 动态造类型
|
||||
|
||||
如果调用方用 `reflect.StructOf` / `reflect.MapOf` 等在**运行时动态生成
|
||||
形状各异的类型**,再把它们交给 `reflux.New`,缓存就会持续增长。
|
||||
典型场景是 ORM 按查询字段动态拼 struct 作为扫描目标。
|
||||
|
||||
实测(12 字段的动态 struct,1000 个):
|
||||
|
||||
| | 每类型占用 | 占比 |
|
||||
|---|---:|---:|
|
||||
| Go runtime 的类型元数据 | 1369 B | 41% |
|
||||
| reflux 的类型描述符 | **1945 B** | **59%** |
|
||||
| 合计 | 3315 B | |
|
||||
|
||||
两点需要说明:
|
||||
|
||||
1. **这不是 reflux 特有的问题**。`reflect.StructOf` 创建的类型
|
||||
**Go runtime 自己也永不回收**,泄漏在更底层就已经发生了;
|
||||
reflux 是在此基础上多加了约 59%。
|
||||
2. **类型数量通常仍然有界**。`reflect.StructOf` 对**相同字段集合**(名称、
|
||||
类型、tag、**顺序**都相同)返回同一个 `reflect.Type`,所以重复调用不会造新类型。
|
||||
只有当字段组合本身是无界的(比如允许客户端任意指定 `?fields=a,b,c`),
|
||||
才会真正持续增长。
|
||||
|
||||
如果你的场景确实会动态生成大量不同的 struct 类型,建议:
|
||||
|
||||
- **优先在上游收敛**:把字段组合固定成有限的几种,或者在构造类型的那一层
|
||||
加缓存(既省掉 `reflect.StructOf` 的开销,也自然限住了类型数量)。
|
||||
顺带一提,`reflect.StructOf` 即使命中 runtime 自己的类型缓存也要
|
||||
**2μs 上下、30 次左右的分配**,重复调用本身就值得避免。
|
||||
- **注意字段顺序**:字段集合相同但顺序不同会产生**不同**的类型。
|
||||
如果拼字段时遍历了 map(Go 的 map 迭代顺序随机),同一个逻辑查询每次都会
|
||||
造出新类型 —— 这是真正会导致无界增长的 bug,务必用切片保证顺序稳定。
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **指针 vs 值传递**:
|
||||
|
||||
Reference in New Issue
Block a user