transient:临时放弃持久性的可变窗口
不可变结构的写操作总要新建一条路径。这在「改一处、留一档」的场景里就是想要的效果,但在「从零建一个 10 万元素的 vector」里全是浪费:中间那 99999 个版本没有任何人看,造出来就为了下一步把它扔掉。
实测这笔浪费的规模:10 万次 push 逐次持久化共新建 11441 个节点,最终活着的只有 3227 个,72% 当场成垃圾。批量 set 更极端——10 万次单点修改新建 399872 个节点,活着的仍是 3227 个。
Clojure 给出的解法叫 transient,Immutable.js 里同一个东西叫 withMutations。
1 · 所有权标记
思路是给每个节点记「它属于哪一次批量构建」。
定义 1.1(transient) 一个带 owner 标记的可变视图。asTransient(v) 生成一个全局唯一的 id 并复制根节点、给它打上这个 id。此后的写操作下降到某节点时:若该节点的 owner 等于当前 id,说明它是本次构建期间造出来的、外界没有任何引用,可直接原地改;否则先复制一份、打上
id,再改副本。persistentVec(t) 把 id 置空,结构随即冻回持久化。
判据「owner 等于当前 id」等价于「这个节点自本次构建开始后被造出来,因而不可能被本次构建之外的任何版本引用」。这一条成立时原地改是安全的:没有人能看见它变。第一次下降会沿路把旧节点全部复制一遍(它们属于构建之前的版本),从第二次起沿途就都是自己的了。
id 全局递增而不复用,是为了让「上一次构建留下的标记」不会被下一次误判为自己的。引擎里的实现是一个模块级计数器,冻结时只置空句柄上的 id、不去遍历清理节点上的标记——节点上的旧 id 此后再也匹配不上任何活着的句柄。
2 · 批量构建的账
省下的正是「每次复制一整条路径」。第一次写复制 个节点( 为树深),此后每次写 0 个,直到需要新挂叶子或长出新层。
几组实测(元素值与顺序完全相同,产物逐项相等、树形也相同):
| 操作 | 逐次持久化 | transient | 倍数 |
|---|---|---|---|
1024 次 push |
63 个节点 | 33 个节点 | 1.9 |
100000 次 push |
11441 个节点 | 3228 个节点 | 3.54 |
100000 次 set |
399872 个节点 | 3227 个节点 | 123.9 |
push 的倍数远小于 set,因为 tail 已经吸收掉 96.88% 的 push,剩下能省的只有挂叶子那部分。set 没有 tail 这层缓冲,每一次都要走完整条路径,所以 transient 在 set 上的收益接近「树深倍」乘以「操作次数」。
3 · 冻结与句柄作废
persistentVec 之后原句柄必须失效。这不是洁癖:冻结版本与句柄共用同一批节点,句柄若还能写,它改的就是冻结版本正在用的那些节点,已经交出去的不可变值会在别人手里变形。Clojure 的做法是把根上的 AtomicReference<Thread> 置空,此后任何 ensureEditable 抛
IllegalAccessError;引擎里同样把句柄的 owner 置空,写操作抛错。
警示 · transient 的安全性不由类型系统保证,只由「不要保留旧句柄」这条纪律保证。persistentVec(t) 返回新值而不是原地把 t 变成持久化的,用意是让「继续用 t」这个写法在运行期立刻炸掉,而不是悄悄产生别名。Clojure 文档里同一条写作「transients
不是值,不要把它当值传递」。
4 · 节点数与时间的背离
节点数省下 123.9 倍,时间却只省 1.56 倍——这是本页最该记住的一组数。
10 万次 set 的实测:逐次持久化 27.8 ms,transient 17.8 ms。分配确实少了两个数量级,但每次 set 仍然要从根走 4 层找到目标叶子,这段指针追逐两种写法一模一样。省掉的只是「走到之后再复制一遍」。JS 引擎的分代 GC
对这种朝生夕死的小对象本来就很便宜,于是分配量的下降没有按比例反映到墙钟时间上。
push 那一侧倒是接近同比:4.8 ms 降到 1.5 ms,3.23 倍,与节点数的 3.54 倍相当。差别在于 push 的树遍历本来就短(只走右脊,且大部分时候连树都不碰),复制在总开销里占比更高。
所以 transient 的收益取决于「复制在总开销里占多大比重」,而不是「省了多少次分配」。批量 push、concat、从数组建表这类操作里它值得用;随机 set 密集的场景收益有限,真要提速得先想办法减少遍历次数。
5 · 参考文献
- Hickey, R. (2009). Transient data structures. clojure.org/reference/transients.
- Bagwell, P., & Rompf, T. (2011). RRB-Trees: Efficient immutable vectors. Technical Report, EPFL. §6 讨论批量构建。
- L'orange, J. N. (2014). Understanding Clojure's transients.
hypirion.com - Immutable.js 的
withMutations/asMutable/asImmutable。