Loment
中文

系统 · 进行中

Loment VM

它是运行程序的第二种方式:一份产物可以交给一个执行器,由它按需接纳, 而它决定要不要接纳,靠的是读这份产物已经携带的东西。它不是一条更快的路,也不是一种把 程序写得更省事的办法。仓库里现在有一个能用的解释器 —— 包里还不带它,而这个 设置也不改变编译器发出的东西。

有什么,没有什么

仓库里现在有一层能用的解释器,T0 到 T3,读的是链接器读的同一份中间形式。 T0 直接执行那份形式;T1 记下一份反馈剖面,却不改动一个字节的输出;T2 在进入执行循环之前 就把指令逐条解码,在语料上跑到 T0 的 6.1×,输出逐字节相同;紧凑的 序列化形式是同一单元文本形式的 28.5%;T3 加上带守卫的特化,以及一条回头路。

没有的,是这一切在包里。解释器是仓库里的 tools/lvm.py 一族, 用 Python 写成。它不在一次发布所带的那些二进制里,所以一套已安装的工具链不能用这种方式跑 程序。这个设置也不改变产物:今天 choose vm 与 no_vm 的产出逐字节 相同。

项目以前就背过的一笔债。runtime 设置曾在一段时间里被声明在 产物里,却什么也不做,后来才被补齐到它声称的样子。这又是同一种形状:引擎在了,发布没有。 这一页把哪一半是哪一半说出来,而不是留给读的人去发现。

它不是什么

有三种解读是错的,而每一种都是读者带着来的。它们排在设计之前,因为把这三种解读放过去, 就会让这东西变成什么都想做、却什么都做不好的那种。

不是那条快路

说反了。Loment 的原生路径本来就在用速度换独立性,而且它一开始就不快:在 docs/212 的数组基准上,对着 C -O2,它落后 10.7× —— 这是 一次记录下来的测量,不是一条长期承诺。VM 是叠在它上面的一层。这条线的轴不是 性能,而逃生舱属于完全不同的另一条线。

不是一次编写、到处运行

那件事早就交给了编译期:一份 IR,三种输出 —— Linux ELF、Windows PE,或一个 freestanding 目标文件。一个字节码格式并不比这更接近可移植性,而且这个说法过不了它 自己的检验:如果可移植性是重点,判据就该是"增加一个目标不需要新的后端",而现在有 两个目标。

不是一种把程序写得更快的办法

那是另一组东西,而这一个一样都不碰:另外六门表层语法、格式化器、语言服务器、编译期 语法定义、为编码 agent 写的指南。LVM 是在程序写完之后才到来的。

设计,分三部分

字节码不是一项新发明

它是对编译器已经产出的那份中间形式的一次序列化。随包的后端是一台 栈机 —— 每个值占一个栈槽,参数放在栈上,聚合体通过一个隐藏的结果指针传递 —— 所以 针对那个子集的一套完整执行模型已经存在、也已经在被演练。缺的是执行它的 那一步。LVM 会是同一份形式的第四个读者:没有新的前端,没有新的 中间形式,没有新的类型系统。

第五个设置

choose 已有四个 —— 程序可以用库的哪一层、它如何回收内存、产物能否 携带一个 runtime、以及它能否向自身之外伸手。vm 是第五个,回答的是 这个程序是为哪种执行模型而写的?不写就是 no_vm, 也就是这个设置存在之前一切东西的状态。自 0.1.4.6 起,发布构建也接受它;今天它能买到的是一条声明,而不是一份不同的产物。

这个值说的是是否,从不是哪一个。这正是 runtime 设置 已经遵循的规则:里面装的东西会随年份变化,而一个把今天这一级编码进名字的写法,明天 要加新的一级时就得改名。

这道闸,以及它为什么开了

直到 0.1.4.6,发布构建在编译期拒绝 choose vm;只有开发 通道接受它。规则很硬 —— 在第一个可用的引擎出现之前,对它的任何调用在发布里都非法 —— 这样一个已安装的工具链就永远不会接受一个它兑现不了的承诺。一个落空的承诺比 一次拒绝更糟。

0.1.4.6 把它打开,因为那条拒绝的前提不再成立:第一个可用的引擎到了。这个设置在两个 通道里现在都是一条声明,而这次更正被记录下来,而不是删掉 —— 曾经要求"拒"的那条校验, 现在要求"收",于是"这道闸存在过、以及它是什么时候开的"仍然看得见。

这道闸门需要一个仓库此前没有的概念:这个编译器是发布构建,还是开发构建?设计把它收进 一个常量,每一边一个值,在打包时翻转。只用一行是有意为之 —— 打包好 的语言指南未经修改就被拷进发行版,这样"包里的副本就是仓库里的副本"才保持可核对,而 一行是弯曲这条规则的最小办法。代价随它一起被说出来:随包发出的编译器种子就不再逐字节 等于仓库里的,于是判据从完全相同降为可复现地相等,且差异恰好是那一个 常量。

阶梯

把 VM 当作一级一级来读,而不是当作一件要建的事。每一级都必须可达成,也必须可证伪 —— "写个 JIT"不是一级,因为没有什么能证伪它。零件清单取自两个在规模上做过这件事的系统; 实现会是项目自己的,而它是否保持如此,是一个悬而未决的决定。

T0 — 字节码和一个直接解释器

地板。仓库里已经建好。判据:一个以这种方式运行的 程序,产出与原生产物逐字节相同的输出和相同的退出码。

T1 — 反馈

针对类型、分支与调用点的剖析数据。仓库里已经建好。判据: 剖析数据落在产物里,第三方能读取它 —— 而把它关掉,产物逐字节不变。

T2 — 一个基线编译器

去掉解释的开销,而不做优化。仓库里已经建好 —— 语料上是 T0 的 6.1×。判据:可测量地比 T0 快,语义上完全相同。

T3 — 一个优化编译器,且有回头路

一个被证明是错的猜测,必须能被放弃。仓库里已经建好,针对那唯一一条背着猜测的 优化。这是这一级唯一一条硬判据:在猜测被放弃之后,程序的结果 与原生产物一致。

T4 — 分代与并发回收

唯一一级不是其它几级的延伸。回收设计曾经说这件事在这里根本做不了:后端只把指针存在 局部变量和形参槽里,而这排除了通常的写屏障。那条理由不再成立 —— 把指针 变成整数的办法出现了,而分代那一半落到了回收那一侧,带着一张卡表 和一道写屏障。仍然在前面的,是在VM 内部做这件事,而这与提前做是两回事。

仓库里有 T0 到 T3;包里一级都没有。建解释器与把它装进发行版是两步, 而只有第一步做完了。上面那几级是计划的形状,不是时间表。

它是干什么用的

先泼盆冷水:大型项目最常想要的两件事 —— 构建更快、运行更快 —— 都在范围之外。构建时间 是编译器内部的问题,而运行时间对这条线来说是错的轴。

但"大"已经暴露出一个结构性的、而非缺陷的问题,而这是唯一能治它的东西。产出的 符号是扁平的:一百个模块意味着函数名必须靠手工做成全局唯一,而两个各自定义了 同一个短名的模块会被拒。这是"一次静态链接"作为一种模型带来的后果 —— 大型项目那道经典 的可扩展性墙,被语言自身的结构直接推到用户脸上。仓库已经把一个超大型项目语料作为判据 在跑,所以这件事是测量出来的,不是猜的。

在运行期接纳一个模块

给一个原生产物加一个模块,意味着重新链接整个程序。在这里,它意味着加载一个单元。

不重新链接就替换一个模块

改一个模块,重新加载它,程序其余部分留在原地。

运行不是你写的代码

一个第三方扩展,或者一个机器写出来的扩展。这是项目自己的那些想法第一次有活 要干的地方。在链接期,端口表、一个单元声明自己可达的范围、关于它没有什么的 记录,以及它的回收数字,都是审计 —— 事后拿来读的东西。到了加载期,这些同样 的记录变成一道闸门 —— 在任何东西运行之前,拿来作决定的东西。

这就是这一页愿意为之背书的那一句话,而且它可证伪:添加一个扩展模块,不意味着 重新链接整个程序 —— 而在它被加载之前,你就能判断出它能不能触达它不该触达的东西。

它不声称什么

这份清单存在设计记录里,而它应该在这页上,而不是留在记录里。

它新

这里头没有哪一块砖是新的。把执行证据携带在产物里、在加载期加以判断,是 20 世纪 90 年代就有的;拒绝加载无法被证明的东西,是沙箱化 runtime 的做法;一份输入配多种执行 模型,是最大的移动端 runtime 在做的事。可能被摆在不同位置的,是那道接缝,不是那些 零件。

那两件事没人做过

全篇的措辞是没有在某次检索里被找到,这与没人做过不是一回事。两项 能推翻这个说法的检索被写下来当作未决判据,并诚实地注明它们没有被穷尽地跑过。

它有用

这一格今天是空的。而且上面第三个角色建立在一个没人测量过的数字上 —— 那个超大型语料 里,究竟有多少是真正应该被延迟加载的代码。如果这个数字接近零,那个角色就只是个漂亮 故事。