自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

?TypeScript 源碼啟示:驚人的52000行代碼文件

開發(fā) 開源
我們?cè)?review 各大開源 js 倉庫性能實(shí)踐的時(shí)候注意到:ts 源碼的 checker.ts 這個(gè)文件相當(dāng)暴力,它將 TS 完整類型系統(tǒng)全部邏輯 5.2 萬行全部寫在一個(gè) ts 文件里,而文件大小則達(dá)到了驚人的 2.92 MB , 這相當(dāng)為什么?

作者 | ecznlai

重型 JS 項(xiàng)目的性能問題一向很難,我們?cè)?review 各大開源 js 倉庫性能實(shí)踐的時(shí)候注意到:ts 源碼的 checker.ts 這個(gè)文件相當(dāng)暴力,它將 TS 完整類型系統(tǒng)全部邏輯 5.2 萬行全部寫在一個(gè) ts 文件里,而文件大小則達(dá)到了驚人的 2.92 MB —— 這相當(dāng)有趣,為什么?

大名鼎鼎的 checker.ts 這個(gè)文件我很久以前就知道了, 在 Github 上直接打不開:Github - microsfot/Github: ./src/compiler/checker.ts

好,VSCode,啟動(dòng):

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

這個(gè)文件很暴力,類型系統(tǒng)全部邏輯 5 萬行 all-in-one file ,是 ts 源碼維護(hù)者不會(huì)寫代碼嗎?顯然并不是,我翻了一些資料和讀了下其中的實(shí)現(xiàn),稍微震撼了一下,將相關(guān)思考細(xì)節(jié)記錄在本文。

低配版 named parameters

眾所周知,js 各種規(guī)范都推薦你用一個(gè)對(duì)象來傳遞多個(gè)參數(shù),然后在函數(shù)里解構(gòu) —— 多數(shù)時(shí)候這沒什么,但是在 ts compiler 里,任何浪費(fèi)都會(huì)被極限放大,因此他們用了這種低配版用注釋的方式來表示 named parameters (這行還是 anders 老爺子寫的,C# 之父 編程領(lǐng)域的傳奇!C#、TypeScript之父!全世界最頂尖的程序員之一。-騰訊云開發(fā)者社區(qū)-騰訊云 )

何為 named parameter 呢?其實(shí)就是帶名字標(biāo)簽的函數(shù),調(diào)用的時(shí)候可以指定標(biāo)簽來傳參數(shù),這個(gè)在其他語言里是基操,比如 moonbit or swift 里的標(biāo)簽函數(shù):

fn add(~left: Int, ~right: Int) -> Int {
  return left + right;
}

add(left: 1, right: 44); // ?? 
add(right: 44, left: 1); // ?? 
add(1, 2);               // ??此時(shí)會(huì)自動(dòng)匹配到 left 和 right

為什么 ts 需要 named parameter 特性: 在 ts 這種高頻調(diào)用場(chǎng)景里通過解構(gòu) options 對(duì)象的方式傳參會(huì)導(dǎo)致大量無謂的內(nèi)存開銷 —— 這通常會(huì)導(dǎo)致 type checking 過程中的內(nèi)存峰值而造成頻繁 gc & mem_copy 更重要的是字面量 key 的順序還會(huì)影響 v8 的 inline caches 優(yōu)化,寫的不好可能會(huì)對(duì)函數(shù)調(diào)用 feedback 造成嚴(yán)重負(fù)面反饋進(jìn)而影響 TurboFan 的進(jìn)一步優(yōu)化最后造成非常大的性能損失 ...

當(dāng) V8 函數(shù)調(diào)用的 feedback slot 從 SMI 變成 Any 時(shí),TurboFan codegen 的匯編將會(huì)慢三倍,關(guān)于這個(gè)問題的細(xì)節(jié),我們?cè)谶@里有深度討論&實(shí)踐。

能用 number 盡量 number

比如 switch、比如 const enum、比如各種 enum bitmap flags 等等設(shè)計(jì),原因是 object 和 string 的開銷太大了,而小一點(diǎn)的整數(shù)在 v8 里甚至是無開銷的(如果 SMI tagged pointer 指針自身數(shù)值不算開銷的話)。

無限制使用 const enum

const enum 有個(gè)特性可以直接 inline 枚舉值到函數(shù)里變成立即數(shù),能享受極致優(yōu)化:

但目前社區(qū)對(duì)于 const enum 的主流意見是 不推薦使用,而且 ts 的部分維護(hù)者也認(rèn)為這個(gè)是 mistake:

但是這說法其實(shí)相當(dāng)尷尬:是的雖然這是 mistake 我們不推薦使用,但我們 ts 源碼里全都是 const enum 到處飛 ... (800+ 個(gè) const enum,沒這個(gè)特性估計(jì) tsc 要慢不少)

ESM/CJS 的性能問題:尤其是 export 導(dǎo)出特別多的時(shí)候

當(dāng) export 導(dǎo)出太多成員的情況下,V8 內(nèi)部處理這類對(duì)象會(huì)將其變成 Slow Properties 字典模式,在多數(shù)時(shí)候這沒啥,但如果遇到某高頻模塊內(nèi)的常量被引用大幾百萬次的情況下,此時(shí) export.xxxxx 的點(diǎn)讀查詢開銷就不能忽視了,尤其是當(dāng) export 上有幾百個(gè)導(dǎo)出的時(shí)候,此時(shí)點(diǎn)讀開銷不可忽視,比如:

const constant = require(`./constant`);

module.export = function getXXConfig() {
  return constant.xxx + constant.bbb;
}
// 由于 constant 上有幾百個(gè)常量,
// 即使是 constant.xxx 這樣簡單的語句
// 在百萬次調(diào)用的時(shí)候,其耗時(shí)將不可忽略 ( 幾百 ms 以上 )

而 checker.ts 則是將所有東西 all in one,就沒這問題了,全都在函數(shù)作用域內(nèi),查詢時(shí)間是 O(1)。

ESM 沒有 private 導(dǎo)出

有種 export 是只想在項(xiàng)目內(nèi)無限制使用,但是又不期望導(dǎo)出能被外部的 npm 看到 —— 也就是 esm 沒有提供 private export 這種特性:

import D from '@tencent/xxx/a/b/c/d';
// ?? 我不期望別人能這樣 import 我內(nèi)部的東西

而 ts 又恰恰要這種特性,那么它們?cè)趺磳?shí)現(xiàn)的呢?通過 /** @internal */ 注解,比如:

標(biāo)記為 @internal 的東西在生成 d.ts 的時(shí)候會(huì)被抹去,變相實(shí)現(xiàn)外部無法 import 而 ts 倉庫內(nèi)隨便 import 。

ts 甚至大量使用 var,而不是用 let 和 const

又比如,有部分函數(shù)為了性能全用 var,愣是沒用 const / let 這些,你看 ts 怎么寫的:

具體見: github.com/microsoft...

大意是 ts 的場(chǎng)景下,v8 這類 js runtime 的 TDZ 檢查甚至?xí)喈?dāng)影響運(yùn)行性能。。。畢竟五萬行呢。。。(production build 會(huì)比 dev build 要快不少的原因之一)

往 String.prototype.xxx 上注入東西

這類操作在普通 js/ts 項(xiàng)目里是一定會(huì)被鄙視的,但一個(gè)靜態(tài)類型語言怎么沒辦法自己拓展基礎(chǔ)類型來使用呢?(這在 swift / go 之類的語言里基于 string / int 來搞出一個(gè)新的類型出來是基操。)

無類編程,推崇組合編程

checker.ts 幾萬行核心邏輯幾乎沒有 class 和繼承,完全通過函數(shù)組合的方式來架構(gòu)代碼,整體看著像是有 rust impl 關(guān)鍵字的 ts 那樣:

代碼里大部分函數(shù)都是上面這種風(fēng)格,第一個(gè)參數(shù)是「核心接口」其他參數(shù)則是對(duì)應(yīng)的參數(shù),當(dāng)然,組合優(yōu)于繼承也算是近年來業(yè)界達(dá)成的共識(shí)了。

當(dāng)然比起架構(gòu),我更愿意相信 ts 是考慮到 class 繼承可能存在潛在的性能問題導(dǎo)致的:

比如 V8 引擎下的 A extends B 場(chǎng)景,B 上面有個(gè)方法 fn,當(dāng) A.fn(); B.fn(); 都調(diào)用了之后,如果 A 和 B 的 shapes 不一樣,此時(shí) fn 調(diào)用 feedback slot 會(huì)從 monomorphic 的變成 polymorphic 的,當(dāng)繼承三個(gè)以上的時(shí)候就會(huì)變成 megamorphic 了,這會(huì)影響引擎 ICs 的優(yōu)化效果,導(dǎo)致性能下降。

怎么沒有用「表驅(qū)動(dòng)」這種所謂的常用「前端設(shè)計(jì)模式」?

圖片

源碼里很多這種根據(jù) ast node kind 去走不同邏輯,然后這些邏輯都寫成 if else if else 或者 switch 語句 —— 為何不使用一個(gè) Record<Kind, Fn> 的方式去表驅(qū)動(dòng)呢?

原因很簡單:表驅(qū)動(dòng)無法被 v8 這類 runtime 靜態(tài)分析優(yōu)化,而且表驅(qū)動(dòng)這類寫法會(huì)慢個(gè)幾十倍對(duì)于基礎(chǔ)設(shè)施來說這是不可接受的。(無貶義,js 的表驅(qū)動(dòng)寫法看場(chǎng)景,高頻調(diào)用還是別了吧,寫 event selector 之類的倒是一類比較合適的場(chǎng)景)。

從語言特性的層面來說,ts 真的缺一個(gè)滿血版模式匹配 + enum adt 了,但目前 ts 原則上是不會(huì)再合入新的 runtime 特性了 —— 這就很難受了,又不能表驅(qū)動(dòng),又不能模式匹配,最后代碼很 C style 了,而且要寫非常多的 x is X 謂語 。

基本沒有 try-catch

與 go 有類似的想法,checker.ts 里通過返回值 + 往 context.xxx 上寫東西的方式來指示異常,一方面是為了性能,另外一方面我甚至可以合理懷疑為是沒有 checked exception 導(dǎo)致只能這樣才能 type checked ... (當(dāng)然 anders 老爺子應(yīng)該是 uncheck 黨,參考 C# 的設(shè)計(jì))

文件多才是大問題 —— 可惜了半成品的 ts namespace

如果有接觸過大型 js/ts 項(xiàng)目的同學(xué)肯定知道,文件一多就不知道東西在哪了,找個(gè) import 你甚至要垮十幾個(gè)文件 。

—— 從這也可看到,東西為什么要 import 才讓用呢?能否有 moonbit、rust 那樣好用的模塊系統(tǒng)呢??? 但這依然涉及 runtime 改造,現(xiàn)階段 ts 就別想了,當(dāng)然 tc39 也不會(huì)再考慮這類特性就是了,等一個(gè) TypeScript Pro Max 吧。

關(guān)于 namespace:有接觸過 go Rust C++ 的同學(xué)應(yīng)該都有了解了,是用來管理包及語言符號(hào)的特性,是業(yè)內(nèi)比較通用的解決方案。

在 ESM 落地之前,ts 有嘗試去做滿血版的 namespace 特性,但是由于重新確定了不做運(yùn)行時(shí)的想法,因此這個(gè)特性在成熟之前就放棄迭代而全面轉(zhuǎn)向 ESM 了,至今 ts 源碼里還大量使用 namespace 或者用 ESM 模擬出 namesapce 特性:

最后來個(gè)暴論:JS 已經(jīng)嚴(yán)重影響 TS 的演進(jìn)了

不得不說,ts 如果繼續(xù)死磕 js/tc39 而放棄做 runtime feature,恐怕現(xiàn)在已經(jīng)是最終形態(tài)了 ... 以后不會(huì)有更進(jìn)一步的演進(jìn)了,因?yàn)槟壳?ts 類型系統(tǒng)已經(jīng)相當(dāng)完善了,甚至部分能力其他語言都沒有,比如 Union Types 以及領(lǐng)先各大友商的控制流分析技術(shù)(然而,2024 了 ts 還沒有滿血版 ADT + 模式匹配,因?yàn)檫@屬于 runtime 特性,不是簡單擦掉類型就能搞定的)。

當(dāng)然,近期 tc39 雖然也提了不少新東西但是沒有靜態(tài)類型系統(tǒng)就顯得這些特性相當(dāng)雞肋以至于它們看起來就像是 ts39 一樣,比如備受關(guān)注的 Record & Tuple 已經(jīng)到 Stage 2 了,但懂得都懂這特性一看就知道明顯就是給 ts 設(shè)計(jì)的,給 js 用這個(gè)特性跟到處傳 void* 一樣沒什么區(qū)別,因?yàn)檫@東西是運(yùn)行時(shí)強(qiáng)類型的,也就是訪問 one_record.x 如果真的沒有定義 x 那么會(huì)直接拋出 error 的而不是返回 undefined。

此外這東西太猛了,幾乎就是一個(gè) C 語言版的 匿名 struct 定義對(duì)象+內(nèi)存結(jié)構(gòu)的方案了,我估計(jì)各大瀏覽器估計(jì)都不太想搞這個(gè) —— 這個(gè)要大改引擎的 js 對(duì)象模型了,如果真能實(shí)裝我很期待它的性能表現(xiàn)。

總之,就目前 ts 源碼倉庫來看,js 自身的語言特性已經(jīng)極其限制 ts 對(duì)其自身的實(shí)現(xiàn)了,但是 ts 又承諾不再做新的 runtime 特性,只做類型系統(tǒng),這就相當(dāng)擰巴了,尤其是體現(xiàn)在 ts 源碼里,這要是放在公司,晉級(jí) CR 估計(jì)要涼透了(悲)

EOF

checker.ts 已經(jīng)搞出幾萬行文件以及大量 if-else 超高復(fù)雜度的控制流了,還自己手寫 named para 注釋、甚至不用 const / let / class 。而且從代碼里處處可見 ts 相當(dāng)鄙視 esm 和 cjs 這些 module 方案,覺得性能不行,然后搞出來一個(gè)半成品的 namespace 模塊方案。

總之由于 js 特性太少了,導(dǎo)致源碼實(shí)現(xiàn)相當(dāng)擰巴,雖然如此但 ts 整體的 compiler pipeline 架構(gòu)設(shè)計(jì)卻相當(dāng)漂亮和簡潔,尤其是 transfomers 和 anders 老爺子主推的 LSP 所帶來的 IDE 革命,有機(jī)會(huì)我單開一篇談?wù)勥@個(gè)。

責(zé)任編輯:趙寧寧 來源: 騰訊技術(shù)工程
相關(guān)推薦

2015-02-06 09:37:13

程序員

2018-10-07 15:10:49

2022-08-04 09:01:45

TypeScriptMicrosoft

2021-01-06 14:42:09

前端Typescript代碼

2021-02-18 18:47:02

比特幣加密貨幣區(qū)塊鏈

2020-05-06 22:01:52

Excel代碼Python

2009-05-20 13:28:28

蘋果Iphone移動(dòng)OS

2025-01-14 00:01:01

2021-02-20 23:33:34

TypeScript代碼開發(fā)

2020-06-10 07:40:36

CPU內(nèi)核態(tài)多線程

2022-11-03 18:28:05

C語言Vely

2022-07-14 16:27:11

邊緣計(jì)算智慧城市應(yīng)用

2024-05-07 16:38:25

2022-11-07 07:04:25

2017-04-05 11:10:23

Javascript代碼前端

2017-03-06 15:01:38

Python代碼詞云

2017-04-10 09:59:08

python代碼愛心線

2010-03-22 12:40:48

Python代碼加密

2025-02-26 10:11:01

2012-12-24 09:45:21

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)