源码启发 惊人的52000行代码文件 ​TypeScript

作者 | ecznlai

重型 JS 名目标性能疑问一贯很难,咱们在 review 各大开源 js 仓库性能通常的时刻留意到:ts 源码的 checker.ts 这个文件相当暴力,它将 TS 完整类型系统所有逻辑 5.2 万行所有写在一个 ts 文件里,而文件大小则到达了惊人的 2.92 MB —— 这相当幽默,为什么?

小名鼎鼎的checker.ts这个文件我很久以前就知道了, 在 Github 上间接打不开:Github - microsfot/Github: ./src/compiler/checker.ts

好,VSCode,启动:

五万行 all-in-one 的 checker.ts

这个文件很暴力,类型系统所有逻辑 5 万行 all-in-one file ,是 ts 源码保养者不会写代码吗?显然并不是,我翻了一些资料和读了下其中的成功,稍微震撼了一下,将关系思索细节记载在本文。

低配版 named parameters

妇孺皆知,js 各种规范都介绍你用一个对象来传递多个参数,而后在函数里解构 —— 少数时刻这没什么,但是在 ts compiler 里,任何糜费都会被极限加大,因此他们用了这种低配版用注释的形式来示意 named parameters (这行还是 anders 老爷子写的,C# 之父编程畛域的传奇!C#、TypeScript之父!全环球最顶尖的程序员之一。-腾讯云开发者社区-腾讯云)

何为 named parameter 呢?其实就是带名字标签的函数,调用的时刻可以指定标签来传参数,这个在其余言语里是基操,比如 moonbit or swift 里的标签函数:

fn left Int right Int  Int  left  rightleft    right     

为什么 ts 须要 named parameter 个性:在 ts 这种高频调用场景里经过解构 options 对象的形式传参会造成少量无谓的内存开支 —— 这通常会造成 type checking 环节中的内存峰值而形成频繁 gc & mem_copy 更关键的是字面量 key 的顺序还会影响 v8 的 inline caches 优化,写的不好或许会对函数调用 feedback 形成重大负面反应进而影响 TurboFan 的进一步优化最后形成十分大的性能损失 ...

当 V8 函数调用的 feedback slot 从 SMI 变成 Any 时,TurboFan codegen 的汇编将会慢三倍,关于这个疑问的细节,咱们在这里有深度探讨&通常。

能用 number 尽量 number

比如 switch、比如 const enum、比如各种 enum bitmap flags 等等设计,要素是 object 和 string 的开支太大了,而小一点的整数在 v8 里甚至是无开支的(假设 SMI tagged pointer 指针自身数值不算开支的话)。

有限度经常使用 const enum

const enum 有个个性可以间接 inline 枚举值到函数里变成立刻数,能享用极致优化:

但目前社区关于 const enum 的干流意见是不介绍经常使用,而且 ts 的局部保养者也以为这个是 mistake:

但是这说法其实相当难堪:是的虽然这是 mistake 咱们不介绍经常使用,但咱们 ts 源码里全都是 const enum 四处飞 ... (800+ 个 const enum,没这个个性预计 tsc 要慢不少)

ESM/CJS 的性能疑问:尤其是 export 导出特意多的时刻

当export导出太多成员的状况下,V8 外部处置这类对象会将其变成Slow Properties字典形式,在少数时刻这没啥,但假设遇到某高频模块内的常量被援用大几百万次的状况下,此时export.xxxxx的点读查问开支就不能漠视了,尤其是当export上有几百个导出的时刻,此时点读开支无法漠视,比如:

 constant  module     constantxxx  constantbbb// 即使是 constant.xxx 这样便捷的语句// 在百万次调用的时刻,其耗时将无法疏忽 ( 几百 ms 以上 )

而 checker.ts 则是将一切物品 all in one,就没这疑问了,全都在函数作用域内,查问期间是 O(1)。

ESM 没有 private 导出

有种export是只想在名目内有限度经常使用,但是又不希冀导出能被外部的 npm 看到 —— 也就是 esm 没有提供 private export 这种个性:

   // ⬆️ 我不希冀他人能这样 import 我外部的物品

而 ts 又恰好要这种个性,那么它们怎样成功的呢?经过/** @internal */注解,比如:

标志为@internal的物品在生成 d.ts 的时刻会被抹去,变相成功外部无法 import 而 ts 仓库内随意 import 。

ts 甚至少量经常使用 var,而不是用 let 和 const

又比如,有局部函数为了性能全用 var,愣是没用 const / let 这些,你看 ts 怎样写的:

详细见:github.com/microsoft...

大意是 ts 的场景下,v8 这类 js runtime 的 TDZ 审核甚至会相当影响运转性能。。。毕竟五万行呢。。。(production build 会比 dev build 要快不少的要素之一)

往 String.prototype.xxx 上注入物品

这类操作在个别 js/ts 名目里是必定会被轻视的,但一个静态类型言语怎样没方法自己拓展基础类型来经常使用呢?(这在 swift / go 之类的言语里基于 string / int 来搞出一个新的类型进去是基操。)

无类编程,推崇组合编程

checker.ts几万行外围逻辑简直没有 class 和承袭,齐全经过函数组合的形式来架构代码,全体看着像是有rust impl关键字的 ts 那样:

代码里大局部函数都是上方这种格调,第一个参数是「外围接口」其余参数则是对应的参数,当然,组合优于承袭也算是近年来业界达成的共识了。

当然比起架构,我更情愿置信 ts 是思索到 class 承袭或许存在潜在的性能疑问造成的:

比如 V8 引擎下的A extends B场景,B 上方有个方法 fn,当A.fn();B.fn();都调用了之后,假设 A 和 B 的shapes不一样,此时 fn 调用 feedback slot 会从 monomorphic 的变成 polymorphic 的,当承袭三个以上的时刻就会变成 megamorphic 了,这会影响引擎 ICs 的优化成果,造成性能降低。

怎样没有用「表驱动」这种所谓的罕用「前端设计形式」?

源码里很多这种依据 ast node kind 去走不同逻辑,而后这些逻辑都写成 if else if else 或许 switch 语句 —— 为何不经常使用一个Record<Kind, Fn>的形式去表驱动呢?

要素很便捷:表驱动无法被 v8 这类 runtime 静态剖析优化,而且表驱动这类写法会慢个几十倍关于基础设备来说这是无法接受的。(无褒义,js 的表驱动写法看场景,高频调用还是别了吧,写 event selector 之类的倒是一类比拟适宜的场景)。

从言语个性的层面来说,ts 真的缺一个满血版形式婚配 + enum adt 了,但目前 ts 准则上是不会再合入新的 runtime 个性了 —— 这就很舒服了,又不能表驱动,又不能形式婚配,最后辈码很 C style 了,而且要写十分多的 x is X 谓语 。

基本没有 try-catch

与 go 有相似的想法,checker.ts 里经过前往值 + 往 context.xxx 上写物品的形式来批示意外,一方面是为了性能,另外一方面我甚至可以正当疑心为是没有 checked exception 造成只能这样才干 type checked ... (当然 anders 老爷子应该是 uncheck 党,参考 C# 的设计)

文件多才是大疑问 —— 惋惜了半成品的 ts namespace

假设有接触过大型 js/ts 名目标同窗必需知道,文件一多就不知道物品在哪了,找个 import 你甚至要垮十几个文件 。

—— 从这也可看到,物品为什么要 import 才让用呢?是否有 moonbit、rust 那样好用的模块系统呢?⬅️ 但这依然触及 runtime 变革,现阶段 ts 就别想了,当然 tc39 也不会再思索这类个性就是了,等一个 TypeScript Pro Max 吧。

关于 namespace:有接触过 go Rust C++ 的同窗应该都有了解了,是用来治理包及言语符号的个性,是业内比拟通用的处置打算。

在 ESM 落地之前,ts 有尝试去做满血版的 namespace 个性,但是由于从新确定了不做运转时的想法,因此这个个性在成熟之前就丢弃迭代而片面转向 ESM 了,至今 ts 源码里还少量经常使用 namespace 或许用 ESM 模拟出 namesapce 个性:

最起初个暴论:JS 曾经重大影响 TS 的演进了

不得不说,ts 假设继续死磕 js/tc39 而丢弃做 runtime feature,恐怕如今曾经是最终外形了 ... 不会有更进一步的演进了,由于目前 ts 类型系统曾经相当完善了,甚至局部才干其余言语都没有,比如 Union Types 以及上游各大友商的控制流剖析技术(但是,2024 了 ts 还没有满血版 ADT + 形式婚配,由于这属于 runtime 个性,不是便捷擦掉类型就能搞定的)。

当然,近期 tc39 虽然也提了不少新物品但是没有静态类型系统就显得这些个性相当鸡肋以致于它们看起来就像是ts39一样,比如备受关注的Record & Tuple曾经到Stage 2了,但懂得都懂这个性一看就知道显著就是给 ts 设计的,给 js 用这个个性跟四处传void*一样没什么区别,由于这物品是运转时强类型的,也就是访问one_record.x假设真的没有定义x那么会间接抛出 error 的而不是前往undefined。

此外这物品太猛了,简直就是一个 C 言语版的匿名 struct定义对象+内存结构的打算了,我预计各大阅读器预计都不太想搞这个 —— 这个要大改引擎的 js 对象模型了,假设真能实装我很等候它的性能体现。

总之,就目前 ts 源码仓库来看,js 自身的言语个性曾经极端限度 ts 对其自身的成功了,但是 ts 又承诺不再做新的 runtime 个性,只做类型系统,这就相当拧巴了,尤其是体如今 ts 源码里,这要是放在公司,晋级 CR 预计要凉透了(悲)

checker.ts 曾经搞出几万行文件以及少量 if-else 超高复杂度的控制流了,还自己手写 named para 注释、甚至不用 const / let / class 。而且从代码里处处可见 ts 相当轻视 esm 和 cjs 这些 module 打算,感觉性能不行,而后搞进去一个半成品的 namespace 模块打算。

总之由于 js 个性太少了,造成源码成功相当拧巴,虽然如此但 ts 全体的 compiler pipeline 架构设计却相当美丽和繁复,尤其是 transfomers 和 anders 老爷子主推的 LSP 所带来的 IDE 反派,无时机我单开一篇谈谈这个。

您可能还会对下面的文章感兴趣: