缓存为什么也有成本
学习入口 · 上一篇 key 与 Map · 下一篇 KV 为什么这么大
缓存通过保留结果,减少以后重复执行的工作。它同时增加了查找、维护和存储。先把这笔交换讲清楚,才能理解模型为什么用内存换计算。
保存引用和复制内容分别发生了什么
Section titled “保存引用和复制内容分别发生了什么”const message = { id: 1, text: '一段很长的回答' };const byId = new Map([[message.id, message]]);console.log(byId.get(1) === message); // true这个索引保存同一个消息对象的引用,文字没有因为这行代码而被深复制。Map 仍需保存索引结构与引用;只要它继续强引用这个对象,该对象就仍可能被保留。新增解析树、图片像素或数值数组,则会增加新的结果数据。
不要用普通 JavaScript 对象的条目数直接推算精确堆内存;运行时还要处理对象布局、字符串表示和分配。数值缓冲区可以先检查明确的有效数据大小:
const vector = new Float32Array(128);console.log(vector.byteLength); // 512Float32Array 每个元素 4 字节;128 个元素的缓冲区有效数据是 512 字节。这仍未包含视图对象等运行时开销。MDN Float32Array
先估算一次交换是否值得
Section titled “先估算一次交换是否值得”把下面这些量放进同一张账里:
缓存路径耗时 ≈ 查找与校验 + 未命中时的计算 + 写入和淘汰保留空间 ≈ 结果数据 + 索引和元数据 + 因引用而仍被保留的对象这是一种分析方法,具体数值要测量。如果计算本来很便宜,或者输入几乎每次都变化,维护缓存可能比重新计算更麻烦。若同一份昂贵结果反复使用,收益就更值得观察。
命中 失效 淘汰各回答一个问题
Section titled “命中 失效 淘汰各回答一个问题”| 问题 | 机制 | 例子 |
|---|---|---|
| 有没有保存过兼容结果 | 命中判断 | 输入和计算版本是否对应 |
| 保存的结果还正确吗 | 失效规则 | 原文、字号或模型配置改变 |
| 正确但暂时不用的结果还要留多少 | 淘汰策略 | 容量上限、最近使用顺序或到期时间 |
有缓存键仍可能遗漏依赖;有 TTL 仍可能在到期前读到旧数据。需要先明确结果对什么输入有效,再决定保留多久。
在前端常遇到的四笔成本
Section titled “在前端常遇到的四笔成本”- 重复计算。 每次增量都重新解析整段历史。
- 数据保留。 会话已经关闭,外部 Map 仍持有解析结果或大对象。
- 同步维护。 数组更新了,按 ID 建立的索引还指向旧版本。
- 数据搬运。 计算放进 Worker 后,每次却复制整份历史数据。
前两项连接到前缀增量计算,最后一项连接到Worker 与内存。
把问题带进模型缓存
Section titled “把问题带进模型缓存”模型权重决定怎样计算,某段输入的 K/V 是这些计算产生的中间状态。为了继续生成,推理引擎保留历史状态;保存的对象从前端的一棵解析树,变成按层与位置组织的数值向量。
接下来用同样的方法问:一个位置有多少数据,保存了多少位置,多少条序列同时存在?沿为什么 KV 会这么大把每个乘数算出来。