Dart VM 介紹 譯
前言
Dart VM 是一個(gè)執(zhí)行 Dart 語言的組件集合读拆,包括但不限于以下組件:
-
運(yùn)行系統(tǒng)
- 對(duì)象模型
- 垃圾收集
- 快照
native 代碼核心庫
-
開發(fā)體驗(yàn)優(yōu)化組件
- debug調(diào)試
- 性能分析
- 熱重載
JIT(Just-in-Time)和 AOT (Ahead of time) 編譯管道痢虹。
解釋器
ARM指令模擬器
"Dart VM" 的命名是有歷史原因的。從某種程度上來說梯皿,Dart VM 作為虛擬機(jī)為高級(jí)的編程語言提供了執(zhí)行環(huán)境仇箱,但這并不表示 Dart VM 總是在解釋或 JIT 的方式執(zhí)行 Dart 。舉個(gè)例子东羹,Dart 的代碼能夠被 Dart VM 的 AOT 管道(AOT pipeline) 編譯成機(jī)器碼剂桥,然后通過一個(gè)叫做 precompiled runtime 的精簡版本的 Dart VM 執(zhí)行,這個(gè)版本不包括任何的編譯組件属提,也不能動(dòng)態(tài)加載 Dart 源碼权逗。
1 Dart VM 怎么執(zhí)行你的代碼?
Dart VM 有很多種方式來執(zhí)行你的代碼冤议,比如說:
- 從源碼執(zhí)行或者通過 JIT 執(zhí)行內(nèi)核二進(jìn)制文件(Kernel binary)斟薇。
- 從快照中恢復(fù):
- 從 AOT 快照;
- 從 AppJIT 快照恕酸。
任何的 Dart 在 VM 上執(zhí)行的 Dart 代碼,都依賴 isolate义矛。isolate 可以理解為 Dart 獨(dú)占的一部分堆內(nèi)存发笔,并且通常擁有自己的控制線程(mutator thread)。在并發(fā)執(zhí)行 Dart 代碼的時(shí)候凉翻,通常會(huì)有很多的 isolate筐咧,但是他們之間是無法直接共享內(nèi)存,共享狀態(tài)的噪矛,僅能通過消息傳遞端口(不要和網(wǎng)絡(luò)端口混淆了A咳铩)。
系統(tǒng)線程和 isolates 的概念有一點(diǎn)模糊艇挨,這高度依賴應(yīng)用是怎么集成的 VM残炮。只有如下的事情可以保證:
- 同一時(shí)刻一個(gè)線程只能進(jìn)入一個(gè) isolate 。如果想要進(jìn)入其他的 isolate 缩滨,就必須先離開當(dāng)前的 isolate势就;
- 同一時(shí)刻泉瞻,只能有一個(gè)突變線程(mutator thread)關(guān)聯(lián)到一個(gè) isolate 上面。突變線程的主要功能是執(zhí)行 Dart 代碼苞冯,并使用 VM 的公開C接口袖牙。
然而相同的系統(tǒng)線程可以首先進(jìn)入一個(gè) isolate,執(zhí)行 Dart 代碼舅锄,然后退出這個(gè) isolate 在進(jìn)入另外一個(gè) isolate鞭达。同樣,也可以有很多的系統(tǒng)線程進(jìn)入一個(gè) isolate 中皇忿,在里面執(zhí)行 Dart 代碼畴蹭,只不過不同時(shí)而已。
isolate 除了會(huì)關(guān)聯(lián)一個(gè)突變線程鳍烁,同時(shí)也會(huì)被關(guān)聯(lián)到許多輔助線程叨襟,比如:
- 一個(gè)后臺(tái)的 JIT 編譯線程;
- 多個(gè)垃圾清理線程幔荒;
- 多個(gè)并發(fā)的垃圾標(biāo)記線程糊闽。
VM 內(nèi)部使用一個(gè)線程池(ThreadPool)來處理系統(tǒng)線程,整個(gè)代碼邏輯圍繞著任務(wù)(ThreadPool::Task)而不是系統(tǒng)線程的觀念來展開的爹梁。舉個(gè)例子右犹,我們并不會(huì)專門為了一個(gè)后臺(tái)垃圾回收的事情開一個(gè)單獨(dú)的線程,而是發(fā)送一個(gè)叫做 SweeperTask 的任務(wù)到全局 VM 線程池當(dāng)中卫键,而線程池會(huì)要么選擇一個(gè)空閑的線程或者當(dāng)沒有可用線程時(shí)等待創(chuàng)建一個(gè)新的線程來執(zhí)行這個(gè)任務(wù)傀履。類似的虱朵,isolate 的事件循環(huán)處理的實(shí)現(xiàn)莉炉,也沒有一個(gè)專門叫做事件循環(huán)的線程,而是在接收了一個(gè)新消息之后碴犬,發(fā)送一個(gè) MessageHandlerTask 任務(wù)到線程池來處理的絮宁。
源碼導(dǎo)讀:類 Isolate 表示一個(gè) isolate,類 Heap - 表示 isolate的堆服协。類 Thread 描述了關(guān)聯(lián)到一個(gè) isoloate 的線程狀態(tài)绍昂。注意 Thread 這個(gè)名字可能會(huì)產(chǎn)生一些困惑,因?yàn)樗凶鳛?mutator 關(guān)聯(lián)到一個(gè) isolate 上系統(tǒng)線程都會(huì)被相同的 Thread 實(shí)例復(fù)用偿荷。通過查看 Dart_RunLoop 和 MessageHandler 來了解 isolate 默認(rèn)的消息處理實(shí)現(xiàn)原理窘游。
1.1 通過 JIT 機(jī)制從源碼運(yùn)行
這一節(jié)解釋一下當(dāng)你從命令行執(zhí)行 Dart 的過程:
// hello.dart
main() => print('Hello, World!');
$ dart hello.dart
Hello, World!
自從 Dart 2 版本之后,VM 已經(jīng)沒有了直接從源代碼執(zhí)行 Dart 的功能跳纳,取而代之的是忍饰,VM 只能執(zhí)行那些由內(nèi)核抽象語法樹(Kernel ASTs)序列化成的內(nèi)核二進(jìn)制文件(Kernel binaries)(又被稱作 dill files)。而將 Dart 源碼翻譯成內(nèi)核抽象語法樹的任務(wù)則交給了由 Dart 編寫的通用前端(common front-end(CFE)),這個(gè)工具被不同的 Dart 模塊所使用(舉個(gè)例子:虛擬機(jī)(VM)寺庄,dart2js艾蓝,Dart Dev Compiler)力崇。
為了保留直接從獨(dú)立源碼直接執(zhí)行 Dart 的便利性,專門還提供了一個(gè)輔助 isolate 赢织,叫做 kernel service 亮靴,專門用來處理 Dart 源碼編譯成內(nèi)核可執(zhí)行文件的過程。之后 VM 就能直接執(zhí)行生成的內(nèi)核二進(jìn)制文件了于置。
然而這并不是 VM 和 CFE 唯一運(yùn)行 Dart 代碼的方式茧吊。舉個(gè)例子,F(xiàn)lutter 完全分離了編譯和執(zhí)行的過程俱两,編譯過程發(fā)生在開發(fā)過程中饱狂,而用戶則獲取這些編譯好的文件直接在裝有 flutter 工具的移動(dòng)設(shè)備上運(yùn)行。
注意 flutter 工具不會(huì)直接去解析 Dart 宪彩,而是生成林一個(gè)持久的前端服務(wù)(frontend_server)休讳,這個(gè)前端服務(wù)實(shí)際上就是對(duì) CFE 和一些 Flutter 特殊的內(nèi)核到內(nèi)核的轉(zhuǎn)換器的一個(gè)封裝。frontend_server 將 Dart 源碼編譯成內(nèi)核文件尿孔,然后通過 flutter 工具發(fā)送給設(shè)備俊柔。當(dāng)開發(fā)者請求熱重載的時(shí)候,這個(gè)持久化的前端服務(wù)就開始發(fā)揮作用活合,在這種情況下雏婶,前端服務(wù)可以重用之前一次的編譯狀態(tài),只編譯那些變化的文件白指。
一旦內(nèi)核二進(jìn)制被加載到了 VM 里面留晚,他會(huì)被解析,然后創(chuàng)建多個(gè)代表了不同程序?qū)嶓w的對(duì)象告嘲。然而這一切都是懶加載的:首先只有庫和類的基礎(chǔ)信息會(huì)被加載错维。每一個(gè)程序?qū)嶓w都會(huì)保留一個(gè)指向內(nèi)核二進(jìn)制文件的指針,所以在需要更多信息的時(shí)候橄唬,可以按需加載赋焕。
那些類的信息只有當(dāng)運(yùn)行時(shí)完全需要的時(shí)候,才會(huì)被完全的反序列化(舉個(gè)例子仰楚,當(dāng)查找一個(gè)類的成員時(shí)隆判,當(dāng)申請一個(gè)實(shí)例時(shí)等等)。在這個(gè)階段僧界,會(huì)從內(nèi)核二進(jìn)制中讀取出來類的所有成員侨嘀。然而完整的函數(shù)體還沒有被反序列化,只有他們的函數(shù)簽名被序列化出來捂襟。
此時(shí)咬腕,已經(jīng)從內(nèi)核二進(jìn)制中加載了足夠多的信息來解析和執(zhí)行方法了。舉個(gè)例子笆豁,已經(jīng)可以開始解析和執(zhí)行一個(gè)庫的 main 函數(shù)了郎汪。
源碼導(dǎo)讀:package:kernel/ast.dart 定義了描述內(nèi)核抽象語法樹的類蜒滩。package:front_end處理解析 Dart 源碼和創(chuàng)建內(nèi)核抽象語法樹的過程班巩。kernel::KernelLoader::LoadEntireProgram
是一個(gè)為了反序列化內(nèi)核抽象語法樹到對(duì)應(yīng)的 VM 對(duì)象的入口點(diǎn)。pkg/vm/bin/kernel_service.dart
實(shí)現(xiàn)了內(nèi)核服務(wù) isolate,runtime/vm/kernel_isolate.cc
將 Dart 實(shí)現(xiàn)和VM的其他部分粘合在一起草穆。package:vm
包含了大部分 VM 的基礎(chǔ)方法裆操。比如多種 內(nèi)核到內(nèi)核的變換窗宇。然而由于歷史原因厌处,還是有一些 VM 相關(guān)的變換在 package:kernel
中。一個(gè)優(yōu)秀的編譯變換的例子是:package:kernel/transformations/continuation.dart
,這個(gè)類將 async凝危,async* 波俄,sync* 這些語法糖進(jìn)行了解糖操作。
嘗試:如果你對(duì)于內(nèi)核格式和他的 VM 的特定的語法感興趣蛾默,你可以使用 pkg/vm/bin/gen_kernel.dart
來從 Dart 源碼生成 內(nèi)核二進(jìn)制文件懦铺。生成的二進(jìn)制文件可以被pkg/vm/bin/dump_kernel.dart
解析。
# Take hello.dart and compile it to hello.dill Kernel binary using CFE.
$ dart pkg/vm/bin/gen_kernel.dart \
--platform out/ReleaseX64/vm_platform_strong.dill \
-o hello.dill \
hello.dart
# Dump textual representation of Kernel AST.
$ dart pkg/vm/bin/dump_kernel.dart hello.dill hello.kernel.txt
當(dāng)你嘗試使用 gen_kernel.dart 的時(shí)候支鸡,你會(huì)注意到她需要一個(gè)叫做 平臺(tái)(platform)的東西冬念,這個(gè)東西是一個(gè)包含了所有核心庫的AST文件的內(nèi)核二進(jìn)制文件。如果你已經(jīng)配置了 Dart SDK牧挣,你只需要在 out 目錄使用平臺(tái)文件急前,比如: out/ReleaseX64/vm_platform_strong.dill。另一個(gè)可以選擇的方案是使用 pkg/front_end/tool/_fasta/compile_platform.dart
來生成平臺(tái)瀑构。
# Produce outline and platform files using the given libraries list.
$ dart pkg/front_end/tool/_fasta/compile_platform.dart \
dart:core \
sdk/lib/libraries.json \
vm_outline.dill vm_platform.dill vm_outline.dill
初始化方法的時(shí)候裆针,每個(gè)方法都有一個(gè)占位符,而不是直接去找到他們真正的方法執(zhí)行體:他們指向了一個(gè) LazyCompileStub 寺晌,這個(gè)東西的功能很簡單世吨,就是向運(yùn)行時(shí)系統(tǒng)請求生成當(dāng)前方法的可執(zhí)行體,然后尾調(diào)回新生成的代碼中折剃。
函數(shù)是通過 非優(yōu)化編譯器(unoptimizing compiler) 進(jìn)行第一次編譯的另假。
非優(yōu)化編譯器在兩個(gè)過程中生成機(jī)器碼:
- 序列化函數(shù)體的抽象語法樹的方法是為函數(shù)體遍歷生成一個(gè)控制流圖(control flow graph (CFG))像屋。CFG 由一系列的包含中間語言(intermediate language (IL))指令的基礎(chǔ)塊組成怕犁。這個(gè)階段使用的 IL 指令類似于基于棧的虛擬機(jī)指令:他們從堆棧中獲取操作數(shù),執(zhí)行操作己莺,然后將結(jié)果推送到相同的堆棧中奏甫。
- 由此產(chǎn)生的 CFG 直接編譯成機(jī)器碼,使用了一對(duì)多的 IL 指令:每個(gè) IL 指令擴(kuò)展成多個(gè)機(jī)器語言指令凌受。
在這個(gè)階段阵子,沒有任何性能上的優(yōu)化。非優(yōu)化編譯器的主要目標(biāo)就是盡快的生成可執(zhí)行代碼胜蛉。
這同時(shí)意味著非優(yōu)化編譯器不會(huì)嘗試靜態(tài)分析任何未在內(nèi)核二進(jìn)制文件中的調(diào)用挠进。所以調(diào)用(MethodInvocation 或者 PropertyGet AST 節(jié)點(diǎn))都是完全動(dòng)態(tài)的進(jìn)行編譯的色乾。VM 目前不會(huì)使用任何的基于虛擬表和接口表的分發(fā)方式,而是直接使用內(nèi)聯(lián)緩存(inline caching)實(shí)現(xiàn)動(dòng)態(tài)調(diào)用领突。
內(nèi)聯(lián)緩存背后的核心思想是將方法解析的結(jié)果緩存到對(duì)應(yīng)調(diào)用點(diǎn)特定的緩存中暖璧。VM 使用的內(nèi)聯(lián)緩存機(jī)制包括:
- 一個(gè)調(diào)用棧的特定緩存(RawICData object)會(huì)將接受者的類映射到一個(gè)方法,如果接收者有匹配到這個(gè)類君旦,就應(yīng)該調(diào)用這個(gè)方法澎办。這個(gè)緩存應(yīng)該存儲(chǔ)一些輔助信息,比如調(diào)用頻率計(jì)數(shù)器金砍,他跟蹤給定類在這個(gè)調(diào)用站點(diǎn)上出現(xiàn)的頻率局蚀。
- 一個(gè)共享查找存根(stub,感覺很難翻譯)恕稠,實(shí)現(xiàn)了方法的快速查找琅绅。這個(gè)存根通過查找給定緩存,來確定他是否包含接收者的類匹配的條目鹅巍。如果這個(gè)條目被找到了奉件,存根就會(huì)調(diào)用頻率計(jì)數(shù)器+1,然后尾調(diào)回緩存方法昆著。否則存根將會(huì)調(diào)用運(yùn)行時(shí)系統(tǒng)中實(shí)現(xiàn)了方法調(diào)用邏輯的輔助器县貌。如果方法解析成功,那么緩存將會(huì)被更新凑懂,后面的調(diào)用就會(huì)命中緩存而不是進(jìn)入運(yùn)行時(shí)系統(tǒng)煤痕。
下面的圖片解釋了 animal.toFace() 調(diào)用點(diǎn)關(guān)聯(lián)的內(nèi)聯(lián)緩存的結(jié)構(gòu)和狀態(tài),這個(gè)調(diào)用點(diǎn)被 Dog 實(shí)例執(zhí)行了兩次接谨,被 Cat 實(shí)例執(zhí)行了一次摆碉。
未優(yōu)化編譯器本身就已經(jīng)可以執(zhí)行任何 Dart 代碼了。然后他生成的代碼執(zhí)行速度非常慢脓豪,這就是為什么 VM 同時(shí)還實(shí)現(xiàn)了自適應(yīng)優(yōu)化編譯管道(adaptive optimizing compilation pipeline)巷帝。他背后的思想是使用正在運(yùn)行的執(zhí)行性能數(shù)據(jù)來驅(qū)動(dòng)優(yōu)化策略。
當(dāng)未優(yōu)化的代碼運(yùn)行的時(shí)候扫夜,他會(huì)收集以下數(shù)據(jù):
- 與每個(gè)動(dòng)態(tài)調(diào)用點(diǎn)相關(guān)聯(lián)的內(nèi)聯(lián)緩存回收集想讓的觀察到的接收者類型楞泼;
- 與函數(shù)和函數(shù)基本塊關(guān)聯(lián)的執(zhí)行計(jì)數(shù)器跟蹤記錄著代碼的調(diào)用頻繁程度。
當(dāng)與某個(gè)函數(shù)相關(guān)聯(lián)的執(zhí)行計(jì)數(shù)器的計(jì)數(shù)達(dá)到了設(shè)定的閾值笤闯,這個(gè)函數(shù)會(huì)被提交給后臺(tái)優(yōu)化編譯器(background optimizing compiler )進(jìn)行優(yōu)化堕阔。
優(yōu)化編譯器與非優(yōu)化編譯器的開始方式相同:都是通過遍歷序列化的內(nèi)核抽象語法樹來為準(zhǔn)備優(yōu)化的方法創(chuàng)建非優(yōu)化的 IL。但是接下來就不一樣了颗味,他不是直接將 IL 翻譯成機(jī)器碼超陆,而是繼續(xù)將未優(yōu)化的 IL 翻譯成靜態(tài)單個(gè)任務(wù)(static single assignment (SSA))∑致恚基于 IL 的 SSA时呀,根據(jù)收集到的類型反饋张漂,進(jìn)行推測優(yōu)化,并通過一系列的常規(guī)的優(yōu)化:例如內(nèi)聯(lián)谨娜,范圍分析鹃锈,類型船舶,表示選擇瞧预,存儲(chǔ)到加載和加載到加載轉(zhuǎn)發(fā)屎债,全局值編號(hào),分配下沉等等手段垢油。最后通過線性掃描寄存器分配器和簡單的一對(duì)多降低 IL 指令盆驹,將優(yōu)化的 IL 生成為機(jī)器碼。
編譯完成之后滩愁,后臺(tái)編譯器會(huì)請求 mutator 線程進(jìn)入安全點(diǎn)躯喇,并將優(yōu)化代碼附加到函數(shù)上。下一次調(diào)用該函數(shù)時(shí)硝枉,就會(huì)使用優(yōu)化的代碼了廉丽。
源碼閱讀:編譯源碼在 runtime/vm/compiler
目錄。編譯管道入口點(diǎn)在 CompileParsedFunctionHelper::Compile
妻味。 IL 定義在 runtime/vm/compiler/backend/il.h
正压。 內(nèi)核到 IL的轉(zhuǎn)換入口在 kernel::StreamingFlowGraphBuilder::BuildGraph
,這個(gè)方法同時(shí)也處理了多種人為構(gòu)建的方法责球。StubCode::GenerateNArgsCheckInlineCacheStub
為 內(nèi)聯(lián)緩存存根生成機(jī)器碼焦履,InlineCacheMissHandler
來處理 內(nèi)聯(lián)緩存沒有命中的情況。runtime/vm/compiler/compiler_pass.cc
定義了優(yōu)化編譯器及其順序雏逾。JitCallSpecializer
做了大多數(shù)的類型反饋的工作嘉裤。
嘗試 VM 提供了可以控制 JIT 的標(biāo)志位,可以通過這個(gè)反編譯這些被 JIT 編譯出來的 IL 和 機(jī)器碼栖博。
Flag | Description |
---|---|
--print-flow-graph[-optimized] | Print IL for all (or only optimized) compilations |
--disassemble[-optimized] | Disassemble all (or only optimized) compiled functions |
--print-flow-graph-filter=xyz,abc,... | Restrict output triggered by previous flags only to the functions which contain one of the comma separated substrings in their names |
--compiler-passes=... | Fine control over compiler passes: force IL to be printed before/after a certain pass. Disable passes by name. Pass help for more information |
--no-background-compilation | Disable background compilation, and compile all hot functions on the main thread. Useful for experimentation, otherwise short running programs might finish before background compiler compiles hot function |
For example
# Run test.dart and dump optimized IL and machine code for
# function(s) that contain(s) "myFunction" in its name.
# Disable background compilation for determinism.
$ dart --print-flow-graph-optimized \
--disassemble-optimized \
--print-flow-graph-filter=myFunction \
--no-background-compilation \
test.dart
要強(qiáng)調(diào)的一點(diǎn)是屑宠,被優(yōu)化編譯器生成的代碼是基于應(yīng)用運(yùn)行時(shí)的推測和假設(shè)的。例如仇让,一個(gè)動(dòng)態(tài)調(diào)用點(diǎn)典奉,只能觀察到一個(gè)單一類 C 作為接收者的實(shí)例,然后這個(gè)調(diào)用點(diǎn)會(huì)被轉(zhuǎn)換為一個(gè)直接調(diào)用妹孙,檢查接收者是否真的是類C秋柄。然而這樣的假設(shè)可能會(huì)在程序運(yùn)行期間被證實(shí)是錯(cuò)誤的:
void printAnimal(obj) {
print('Animal {');
print(' ${obj.toString()}');
print('}');
}
// Call printAnimal(...) a lot of times with an intance of Cat.
// As a result printAnimal(...) will be optimized under the
// assumption that obj is always a Cat.
for (var i = 0; i < 50000; i++)
printAnimal(Cat());
// Now call printAnimal(...) with a Dog - optimized version
// can not handle such an object, because it was
// compiled under assumption that obj is always a Cat.
// This leads to deoptimization.
printAnimal(Dog());
每當(dāng)優(yōu)化的代碼作出一些無法從靜態(tài)不可變信息中產(chǎn)生的假設(shè)時(shí)获枝,他都需要驗(yàn)證是否有違背此假設(shè)的情況蠢正,并在這種違背的情況發(fā)生時(shí),回退到未優(yōu)化的代碼狀態(tài)省店,能夠正確執(zhí)行嚣崭。
這一流程被稱作去優(yōu)化(deoptimization): 每當(dāng)優(yōu)化的代碼遇到一個(gè)他不能處理的情況時(shí)笨触,他就將執(zhí)行點(diǎn)轉(zhuǎn)移到未優(yōu)化的函數(shù)的匹配點(diǎn),并在那里開始執(zhí)行雹舀。未優(yōu)化的版本沒有任何假設(shè)芦劣,可以處理任何的輸入。
VM 通常都會(huì)在發(fā)生反優(yōu)化之后说榆,丟棄掉他的優(yōu)化版本虚吟,并重新使用新的反饋數(shù)據(jù)進(jìn)行再次的優(yōu)化。
防止 VM 作出推測假設(shè)的方法有兩種:
- 內(nèi)聯(lián)檢查(比如 CheckSmi签财,CheckClass IL 指令)串慰,驗(yàn)證使用點(diǎn)假設(shè)的正確性。例如唱蒸,當(dāng)將動(dòng)態(tài)調(diào)用轉(zhuǎn)換為直接調(diào)用的時(shí)候邦鲫,編譯器回在直接調(diào)用前做這個(gè)檢查。發(fā)生在這些檢查上的去優(yōu)化神汹,被稱為是急切去優(yōu)化(eager deoptimization)庆捺,因?yàn)樗跈z查發(fā)生時(shí),就要做去優(yōu)化的工作屁魏。
- 全局保護(hù)滔以,他指示運(yùn)行時(shí)丟棄掉優(yōu)化版本。比如優(yōu)化編譯器可能發(fā)現(xiàn)一些類 C 在類型傳遞過程中從來不會(huì)被繼承氓拼。然后隨后的代碼可能會(huì)引入一個(gè) C 的子類醉者,那么這個(gè)推測就失效了。此時(shí)就需要運(yùn)行時(shí)找到并丟棄掉所有在假設(shè) C 沒有子類的情況下做的代碼優(yōu)化披诗。運(yùn)行時(shí)可能會(huì)發(fā)現(xiàn)一些優(yōu)化的代碼已經(jīng)無效了撬即,在這種情況下,受影響的幀會(huì)被標(biāo)記為去優(yōu)化呈队,并在執(zhí)行返回時(shí)去優(yōu)化剥槐。這種去優(yōu)化被稱作延遲去優(yōu)化:因?yàn)橹挥挟?dāng)控制流反饋到優(yōu)化的代碼時(shí)才會(huì)進(jìn)行。
源碼閱讀:去優(yōu)化的代碼位于 runtime/vm/deopt_instructions.cc
宪摧。 他本質(zhì)上是一個(gè)小型的解釋器粒竖,可以將優(yōu)化的代碼重新轉(zhuǎn)換為未優(yōu)化的代碼狀態(tài)。在優(yōu)化代碼中每個(gè)潛在可能去優(yōu)化的位置處几于,都會(huì)通過 CompilerDeoptInfo::CreateDeoptInfo
中生成的去優(yōu)化的指令蕊苗。
嘗試: 標(biāo)志 --trace-deoptimization 會(huì)讓 VM 打印出每個(gè)去優(yōu)化發(fā)生位置的具體信息。 --trace-deoptimization-verbose 會(huì)讓 VM 在去優(yōu)化期間打印每一行去優(yōu)化的指令沿彭。
1.2 從快照中運(yùn)行
VM 能夠?qū)?isolate 中的堆朽砰,或者更多具體的對(duì)象圖序列化成二進(jìn)制快照。當(dāng) VM 再次啟動(dòng)時(shí),可以利用快照恢復(fù)到之前相同的狀態(tài)瞧柔。
快照是為了啟動(dòng)速度而優(yōu)化的底層格式 —— 他實(shí)質(zhì)上是一個(gè)創(chuàng)建對(duì)象的列表漆弄,以及如何將對(duì)象關(guān)聯(lián)起來的指令≡旃快照背后的根本思想是:不需要解析 Dart 源碼然后逐漸創(chuàng)建出 VM 的數(shù)據(jù)結(jié)構(gòu)撼唾,而是直接將所有必要的數(shù)據(jù)從快照中解壓縮出來,然后快速的生成一個(gè) isolate 哥蔚。
最初的快照沒有包含機(jī)器碼倒谷,但隨著 AOT 編譯器的加入,也被引入了進(jìn)來糙箍。開發(fā) AOT 的動(dòng)機(jī)是為了讓 VM 能夠在那些限制使用 JIT 的平臺(tái)上也能使用快照恨锚。
含有代碼的快照的運(yùn)行方式幾乎與普通的快照一樣,只有一點(diǎn)不同:他們包含了一個(gè)代碼區(qū)倍靡,和快照其他部分不同的是猴伶,他們不需要反序列化。代碼區(qū)在映射到內(nèi)存后會(huì)直接稱為堆的一部分塌西。
源碼閱讀: runtime/vm/clustered_snapshot.cc
處理快照的序列化和反序列化他挎。Dart_createXyzSnapshot [ AsAssembly ]是一系列 API 函數(shù),負(fù)責(zé)寫出堆的快照(比如 Dart_CreateAppJITSnapshotAsBlobs
和Dart_CreateAppAOTSnapshotAsAssembly
)捡需。另一方面办桨,Dart_CreateIsolate
可以選擇啟動(dòng)時(shí)使用哪個(gè)快照。
1.3 從 AppJIT 的快照啟動(dòng)
AppJIT 快照的引入是為了減少類似 dartanalyzer 或 dart2js 這種大型 Dart 應(yīng)用的 JIT 預(yù)熱時(shí)間站辉。在小型項(xiàng)目上時(shí)呢撞,代碼運(yùn)行的時(shí)間可能和使用用 JIT 運(yùn)行的時(shí)間可能差不多。
而 AppJIT 就是來解決這個(gè)問題的:一個(gè)應(yīng)用在啟動(dòng) VM 的時(shí)候可以使用一些預(yù)設(shè)的訓(xùn)練好的數(shù)據(jù)和所有生成的代碼以及 VM 的內(nèi)部數(shù)據(jù)結(jié)構(gòu)都會(huì)被序列化成一個(gè) AppJIT 快照饰剥。然后可以通過下發(fā)快照而不是通過下發(fā)二進(jìn)制文件來發(fā)布應(yīng)用殊霞。通過這個(gè)快照啟動(dòng)的 VM 仍然可以進(jìn)行 JIT —— 就是當(dāng)實(shí)際上的執(zhí)行數(shù)據(jù)和預(yù)設(shè)的訓(xùn)練數(shù)據(jù)不匹配的時(shí)候,就會(huì)運(yùn)行汰蓉。
嘗試 在你運(yùn)行應(yīng)用的時(shí)候绷蹲,如果傳遞了 --snapshot-kind=app-jit --snapshot=path-to-snapshot 兩個(gè)參數(shù),就會(huì)生成 AppJIT 的快照顾孽。下面是一個(gè) dart2js 生成和使用 AppJIT 快照的例子祝钢。
# Run from source in JIT mode.
$ dart pkg/compiler/lib/src/dart2js.dart -o hello.js hello.dart
Compiled 7,359,592 characters Dart to 10,620 characters JavaScript in 2.07 seconds
Dart file (hello.dart) compiled to JavaScript: hello.js
# Training run to generate app-jit snapshot
$ dart --snapshot-kind=app-jit --snapshot=dart2js.snapshot \
pkg/compiler/lib/src/dart2js.dart -o hello.js hello.dart
Compiled 7,359,592 characters Dart to 10,620 characters JavaScript in 2.05 seconds
Dart file (hello.dart) compiled to JavaScript: hello.js
# Run from app-jit snapshot.
$ dart dart2js.snapshot -o hello.js hello.dart
Compiled 7,359,592 characters Dart to 10,620 characters JavaScript in 0.73 seconds
Dart file (hello.dart) compiled to JavaScript: hello.js
1.4 從 AppAOT 快照中運(yùn)行
AOT 最終被引入的原因是為了支持那些不支持 JIT 的平臺(tái)。但是他們也可以用于快速啟動(dòng)和避免潛在的性能損失若厚。
沒有 JIT 的能力意味著:
- AOT 快照必須應(yīng)用運(yùn)行期間所需的每一個(gè)方法的可執(zhí)行代碼拦英。
- 可執(zhí)行代碼不能依賴任何可能在運(yùn)行期會(huì)被推翻的推測假設(shè)。
為了滿足這些需求测秸,AOT 必須進(jìn)行全局的靜態(tài)分析(type flow analysis or TFA)疤估,從而決定應(yīng)用的哪部分代碼可以到達(dá)灾常,那部分代碼會(huì)被執(zhí)行,哪些實(shí)例會(huì)被分配做裙,以及他們之間的類型流動(dòng)過程岗憋。所有這些分析都是保守的: 這意味著他們在正確性方面犯了錯(cuò)誤——這與 JIT 在性能方面犯了錯(cuò)誤形成了鮮明的對(duì)比肃晚,因?yàn)樗偸窃谖磧?yōu)化的代碼中去優(yōu)化锚贱,以實(shí)現(xiàn)正確的行為。
然后將所有潛在可能會(huì)被觸發(fā)的代碼都編譯為本地代碼关串,這其中不會(huì)包含任何的推測優(yōu)化拧廊。然而,類型信息還是會(huì)被使用晋修,用于專門化代碼(比如 devirtualize 調(diào)用)吧碾。
編譯完所有的函數(shù)之后,就可以獲取堆的快照了墓卦。
生成的快照可以使用與編譯的運(yùn)行時(shí)運(yùn)行倦春,這是一個(gè)去掉了 JIT 可動(dòng)態(tài)代碼加載工具等組件的 Dart VM 的一個(gè)特殊變體。
源碼閱讀: package:vm/transformations/type_flow/transformer.dart
是基于 TFA 結(jié)果的類型流分析和轉(zhuǎn)換的切入點(diǎn)落剪。 Precompiler::DoCompileAll
是 VM 中 AOT 編譯的循環(huán)點(diǎn)睁本。
嘗試 AOT 編譯管道目前還沒有打包到 Dart SDK 中。如果有項(xiàng)目需要依賴他(比如 flutter)就必須用 SDK 提供方式手動(dòng)構(gòu)建他忠怖。 pkg/vm/tool/precompiler2
中的腳本呢堰,對(duì)于管道是如何構(gòu)建的,以及必須構(gòu)建哪些二進(jìn)制構(gòu)建才能使用它凡泣,有很高的參考價(jià)值枉疼。
# Need to build normal dart executable and runtime for running AOT code.
$ tool/build.py -m release -a x64 runtime dart_precompiled_runtime
# Now compile an application using AOT compiler
$ pkg/vm/tool/precompiler2 hello.dart hello.aot
# Execute AOT snapshot using runtime for AOT code
$ out/ReleaseX64/dart_precompiled_runtime hello.aot
Hello, World!
注意如果希望檢查生成的 AOT 代碼,可以將 --print-flow-graph-optimizedand
--disassemble-optimized 傳遞給 precompiler2 腳本鞋拟。
1.4.1 可切換調(diào)用(Switchable Calls)
即使使用了全局和局部的 AOT 靜態(tài)編譯代碼骂维,也可能仍然會(huì)有一些調(diào)用,無法完全的被靜態(tài)化贺纲。為了補(bǔ)償 AOT 的 缺憾席舍,在運(yùn)行時(shí)會(huì)用到一個(gè)叫做內(nèi)聯(lián)緩存技術(shù)的擴(kuò)展。這個(gè)擴(kuò)展的版本名稱是可切換調(diào)用哮笆。
JIT 部分已經(jīng)描述了與調(diào)用點(diǎn)相關(guān)的每個(gè)內(nèi)聯(lián)緩存由哪兩部分組成:一個(gè)緩存的對(duì)象(即一個(gè) RawICData)和一大塊本機(jī)代碼去調(diào)用(比如 InlineCacheStub)来颤。在 JIT 模式下運(yùn)行時(shí)只會(huì)更新自己的緩存。然而 AOT 的運(yùn)行時(shí)可以根據(jù)內(nèi)聯(lián)緩存的狀態(tài)稠肘,去選擇替換緩存和調(diào)用本機(jī)代碼福铅。
最初,所有的動(dòng)態(tài)調(diào)用都以非鏈接狀態(tài)開始项阴。當(dāng)這些調(diào)用首次到達(dá)了 UnlinkedCallStub滑黔,他只需要簡單的調(diào)用運(yùn)行時(shí)助手 DRT_UnlinkedCall 來鏈接這次調(diào)用笆包。
DRT_UnlinkedCall 會(huì)盡可能的將這次調(diào)用轉(zhuǎn)換為單態(tài)狀態(tài)。在這個(gè)狀態(tài)下略荡,調(diào)用點(diǎn)會(huì)直接被轉(zhuǎn)換為一次直接調(diào)用庵佣,就是說是通過一種特殊的調(diào)用點(diǎn),直接訪問已經(jīng)被驗(yàn)證過的類來實(shí)現(xiàn)調(diào)用汛兜。
[站外圖片上傳中...(image-2503e5-1592291234144)]
在上面這個(gè)簡單的例子中巴粪,我們假設(shè)當(dāng) obj.method() 首次被執(zhí)行之后,obj 是一個(gè) C 的實(shí)例粥谬,然后 obj.method 會(huì)被解析為 C.method.
下一次當(dāng)我們執(zhí)行到相同的調(diào)用點(diǎn)的時(shí)候肛根,將會(huì)繞過一些列查找方法的過程,直接去調(diào)用 C.method漏策。然而他會(huì)通過一個(gè)特殊的調(diào)用點(diǎn)來調(diào)用 C.method派哲,這個(gè)過程將會(huì)驗(yàn)證 obj 是否仍然是一個(gè) C 的實(shí)例。如果不是 DRT_MonomorphicMiss
的情況掺喻,將會(huì)重新進(jìn)入查找過程芭届,并找到下一個(gè)調(diào)用狀態(tài)。
如果 obj 是一個(gè) D 的實(shí)例感耙,而 D 繼承了 C褂乍,并且沒有重寫 C.method 方法時(shí),C.method 仍然是 obj 的一個(gè)合法調(diào)用抑月。在這種情況下树叽,我們會(huì)檢查這個(gè)調(diào)用點(diǎn)是否可以被轉(zhuǎn)換到單目標(biāo)狀態(tài),也就是 SingleTargetCallStub
(也可以參看 RawSingleTargetCache
)谦絮。
對(duì)于 AOT 編譯题诵,大多數(shù)類都使用繼承層次結(jié)構(gòu),會(huì)通過深度優(yōu)先遍歷來分配整數(shù) id层皱。如果 C 是 D0,....,的基類性锭,并且這些子類都沒有重寫 C.method 方法。然后 C.:cid <= classId(obj) < max(D0.:cid,...,Dn.:cid)叫胖,這表示 obj.method 總是會(huì)被解析為 C.method 方法草冈。在這種情況下,我們不會(huì)通過比較單態(tài)瓮增,而是通過 class id 的范圍檢查去讓所有 C 的子類怎棱,完成這次調(diào)用。
其他情況下绷跑,調(diào)用點(diǎn)會(huì)通過線性查找的方式去查找內(nèi)聯(lián)緩存拳恋,非常像 JIT 模式下用到的方法。(參看ICCallThroughCodeStub
, RawICData
和 DRT_MegamorphicCacheMissHandler
)砸捏。
最后谬运,如果線性數(shù)組中的檢查次數(shù)增長超過了閾值隙赁,則會(huì)切換為類似字典的結(jié)構(gòu)。(參看 MegamorphicCallStub
, RawMegamorphicCache
和 DRT_MegamorphicCacheMissHandler
)梆暖。