SELF Customization — 给每种"调用者类型"现场打一份方法
是什么
Customization(特化)是 1989 年 Sun 实验室 SELF 团队发明的一种”让动态语言编译出来的代码尽量像静态语言”的编译技巧——编译器为每一种调用者类型,复制出一份方法的专属副本,这样在副本里 self 是哪个类型就成了已知常量,所有原本要”运行时查表”的动作都能消掉。
日常类比:餐厅有一份通用菜谱”做一份饭”,里面写着”先看顾客是不是儿童 / 老人 / 普通成年人,再分支”。每次有人点单都要重读一遍 if-else,太累。SELF 编译器的做法是——直接抄三本菜谱:儿童版只写儿童那条流程,老人版只写老人那条,成年版只写成年那条。每位顾客拿到的是已经为他量身定制的菜谱,没有 if-else,没有查表。
"通用方法 distance:算两点距离"distance = ( ((x - other x) squared + (y - other y) squared) sqrt )朴素实现:每次调 distance 都要查 self 是 IntegerPoint 还是 FloatPoint。Customization 后:编译器为 IntegerPoint 复制一份方法,里面 x 直接是整数字段访问;为 FloatPoint 再复制一份,里面 x 直接是浮点字段访问。两份代码完全分裂,各跑各的。
为什么重要
不理解 customization,下面这些事都没法解释:
- 为什么 V8 引擎能让 JavaScript 跑到接近 C 的速度——hidden class + customization 是地基
- 为什么 HotSpot JVM 的虚函数调用几乎免费——SELF 团队的 Lars Bak 把这套搬进了 HotSpot
- 为什么”动态语言注定慢”在 1989 年之后被证伪——customization 给出了第一个反例
- 为什么现代 JIT 编译器都说”先 profile 再特化”——customization 是这条路线的源头
简单说:没有 customization,今天的 JS / Python / Ruby JIT 都得重新发明一遍。
核心要点
Customization 在 SELF 里靠三件套协同工作:
-
按 receiver 特化方法:每个方法在每种调用者类型下都重新编译一份。类比抄菜谱——一菜一人份,副本之间互不打扰。这一步消除了
self上的所有动态分发。 -
类型预测(type prediction):对
+-<ifTrue:这种高频消息,编译器猜接收者是 SmallInt 或 Boolean,提前内联那条分支,再加一道 guard——猜对直接跑,猜错回退。类比熟客点单——服务员看脸就上常点的菜,错了再改。 -
Maps(隐藏类):原型对象不天然有”类”,但编译器内部偷偷给”长得一样”(slot 名字+顺序一致)的对象贴一张共享的 layout 描述符叫 map。所有 IntegerPoint 共享一张 map,访问
x就翻译成”按 map 第 0 偏移取字段”。这就是 V8 hidden class 的祖宗。
三件加起来:self 调度消失 + 高频消息内联 + 字段访问按偏移走,SELF 一下子从”100× 慢于 C”变成”4× 慢于 C”。
实践案例
案例 1:customization 怎么消除一次 self 分发
"原始:通用 length 方法"length = ( (x squared + y squared) sqrt )朴素实现编译出来的伪指令:
load self ; 取调用者dispatch x ; 查表找 x 字段位置dispatch squared ; 查表找 squared 方法... 同样的 dispatch 再来 3 遍customization 后,编译器知道这次调用者是 IntegerPoint,复制一份专属版本:
load_field self.0 ; x 是第 0 槽,直接偏移读imul ; 整数平方,一条机器指令load_field self.1 ; y 是第 1 槽imuliaddfp_sqrt四次查表全部消失,剩下纯计算指令。这就是 customization 的本钱。
案例 2:type prediction 配合 customization 把 1 + 2 内联成一条加法
SELF 里 1 + 2 是给 1 发 + 消息,参数是 2。朴素实现要一次方法查找。编译器看到 + 这个消息名时直接预测:“99% 情况下接收者是 SmallInt”,于是产出:
guard self is SmallInt ; 一条类型检查guard arg is SmallIntiadd ; 直接整数加; ----- 兜底分支 -----fallback: 走通用消息发送热路径就一条 iadd。这是 type prediction + customization 的合力——customization 把 self 类型钉死,type prediction 把高频消息提前展开。
案例 3:maps 怎么让两个原型对象共享 layout
你写:
p1 = (| x = 1. y = 2 |) "原型,slot x y"p2 = (| x = 3. y = 4 |) "另一个,slot 完全一样"p1 和 p2 没有”类”,但编译器内部给它们贴同一张 map:{x→offset 0, y→offset 1}。访问 p1 x 和 p2 x 都翻译成”取自身偏移 0”。一旦你给 p2 加一个新 slot z:
p2 z: 5 "动态加 slot"p2 的 map 立刻分裂成新的 {x→0, y→1, z→2},原先按旧 map 编译过的代码作废、需重编。这正是 V8 里 hidden class transition 的雏形。
踩过的坑
- 代码膨胀——customization 给每个 receiver 类型一份特化副本,热路径方法可能膨胀 10-100×。SELF 团队后来加了 limit + LRU 才控住。
- type prediction 猜错代价高——对真正多态的代码(一个调用点见过 5+ 类型),guard 失败的回退路径反而更慢,需要 PIC(self-pic)补救。
- map 迁移失效问题——程序运行时给对象动态加 slot 会触发 map transition,已经按旧 map 编出的机器码全部失效,要么丢弃要么 deoptimize。
- 离线全量编译撑不到 eval 这种动态场景——customization 假设方法集闭合;后来 Hölzle 1994 的自适应再编译才解开这个限制。
适用 vs 不适用场景
适用:
- 动态分发为主的 OO 语言(Smalltalk / Ruby / Python / JavaScript)
- 调用点的 receiver 类型相对集中(mono / 低多态)的代码
- 能离线或后台编译的场景(HotSpot / V8 / PyPy 都靠后台线程跑这类优化)
不适用:
- 真正高度多态的代码——一个点见过 10+ 类型,customization 副本爆炸还不如查表
- 极度动态的语言场景(频繁 eval / 改方法 / 加 slot),缓存命中率低
- 嵌入式 / 内存受限设备——10× 代码膨胀直接打爆 ROM
- 静态语言(C / Rust)——本来 self 类型就是已知,customization 没增量
历史小故事(可跳过)
- 1986 年:Ungar 和 Smith 想做”比 smalltalk-80 更纯粹”的 OO 语言,于是有了 SELF——只剩对象 + 消息 + 原型,没有类。
- 1989 年:Chambers-Ungar 在 PLDI 发表 customization 论文,第一次让 SELF 跑到 Smalltalk 的 4-5×。
- 1991 年:Hölzle 加 self-pic(polymorphic inline cache)补上多态点。
- 1994 年:Hölzle 博士论文加自适应再编译——按运行时 profile 选择性 customize,不再全量。
- 1999/2008 年:Lars Bak 把这套思想分别带进 HotSpot JVM 和 V8 引擎。SELF 实验室落幕,思想登顶工业界。
学到什么
- 特化是最朴素的 JIT 武器——原理就是”复制 + 把变量当常量内联”,partial-evaluation-jones 给了它的理论形式。
- 动态语言不一定慢——慢的不是动态本身,是朴素实现。把”运行时多态”挪到”编译期特化”,差距会被抹平大半。
- hidden class 不是 V8 发明的——是 1989 年 SELF maps 的现代马甲。读懂 SELF 论文等于免费读懂半本 V8 内核。
- 代码膨胀 vs 性能 永远是工程权衡——customization 押的是”内存便宜,CPU 周期贵”,今天依旧成立。
延伸阅读
- 视频:Craig Chambers — The Design and Implementation of SELF(作者本人讲编译器架构)
- 论文 PDF:原论文 ACM DL 链接 10.1145/74818.74831
- 后续工作:Hölzle 1994 博士论文 Adaptive Optimization for Self(自适应再编译的源头)
- 工业落地:V8 设计文档 V8 Hidden Classes & Inline Caches(直接对应 SELF maps + PIC)
- self-pic —— customization 的近邻;同一团队下一篇论文,补多态分发那块
关联
- self-pic —— PIC 接 customization 的多态点;两篇合起来才是完整的 SELF 编译器故事
- smalltalk-80 —— SELF 的”上一代”,customization 想超越的 baseline
- simula-67 —— 类的起点;SELF 是反过来的”无类原型”流派
- partial-evaluation-jones —— customization 是部分求值在 OO 语言上的具体形态
- turchin-supercompilation —— 比 customization 更激进的程序特化,思想血缘相近
- kildall-dataflow —— customization 内部的类型分析靠数据流框架推导
- ssa —— 现代编译器把 customization 后的副本进一步降到 SSA 优化
反向链接
- kildall-dataflow —— Kildall 数据流框架 — 用一套格论统一所有全局编译优化
- partial-evaluation-jones —— Jones-Gomard-Sestoft 1993 — Partial Evaluation 与自动程序生成
- self-pic —— Self / PIC — 内联缓存的诞生
- simula-67 —— SIMULA 67 — 面向对象的诞生
- smalltalk-80 —— Smalltalk-80
- ssa —— SSA — 静态单赋值形式
- strongtalk —— Strongtalk — 可以装可以卸的 Smalltalk 类型系统
- turchin-supercompilation —— Turchin Supercompilation — 让编译器把程序模拟一遍再写回去