Dart VM 介紹

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 快照恕酸。

然而這些方式的主要不同堪滨,主要在于 VM 何時(shí),怎樣將 Dart 源碼轉(zhuǎn)換成可執(zhí)行的代碼蕊温。而運(yùn)行時(shí)環(huán)境卻基本一樣袱箱。
1.1 isolate

任何的 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_RunLoopMessageHandler 來了解 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)力崇。

1.1 命令行源碼執(zhí)行流程

為了保留直接從獨(dú)立源碼直接執(zhí)行 Dart 的便利性,專門還提供了一個(gè)輔助 isolate 赢织,叫做 kernel service 亮靴,專門用來處理 Dart 源碼編譯成內(nèi)核可執(zhí)行文件的過程。之后 VM 就能直接執(zhí)行生成的內(nèi)核二進(jìn)制文件了于置。

1.2 kernel service

然而這并不是 VM 和 CFE 唯一運(yùn)行 Dart 代碼的方式茧吊。舉個(gè)例子,F(xiàn)lutter 完全分離了編譯和執(zhí)行的過程俱两,編譯過程發(fā)生在開發(fā)過程中饱狂,而用戶則獲取這些編譯好的文件直接在裝有 flutter 工具的移動(dòng)設(shè)備上運(yùn)行。

1.3 flutter stream

注意 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í)候橄唬,可以按需加載赋焕。

1.4 內(nèi)核二進(jìn)制加載到 VM 中

那些類的信息只有當(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ù)簽名被序列化出來捂襟。

1.5 類成員加載過程

此時(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)回新生成的代碼中折剃。

Lazy Compilation

函數(shù)是通過 非優(yōu)化編譯器(unoptimizing compiler) 進(jìn)行第一次編譯的另假。

1.7 首次編譯

非優(yōu)化編譯器在兩個(gè)過程中生成機(jī)器碼:

  1. 序列化函數(shù)體的抽象語法樹的方法是為函數(shù)體遍歷生成一個(gè)控制流圖(control flow graph (CFG))像屋。CFG 由一系列的包含中間語言(intermediate language (IL))指令的基礎(chǔ)塊組成怕犁。這個(gè)階段使用的 IL 指令類似于基于棧的虛擬機(jī)指令:他們從堆棧中獲取操作數(shù),執(zhí)行操作己莺,然后將結(jié)果推送到相同的堆棧中奏甫。
  2. 由此產(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í)行了一次摆碉。

1.8 inline cache

未優(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)化的代碼了廉丽。

y

源碼閱讀:編譯源碼在 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)瞧柔。

1.2 snapshot

快照是為了啟動(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ì)直接稱為堆的一部分塌西。

1.2.2 snapshot-with-code work

源碼閱讀: runtime/vm/clustered_snapshot.cc 處理快照的序列化和反序列化他挎。Dart_createXyzSnapshot [ AsAssembly ]是一系列 API 函數(shù),負(fù)責(zé)寫出堆的快照(比如 Dart_CreateAppJITSnapshotAsBlobsDart_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)行汰蓉。

1.3.1 AppJIT

嘗試 在你運(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 的能力意味著:

  1. AOT 快照必須應(yīng)用運(yùn)行期間所需的每一個(gè)方法的可執(zhí)行代碼拦英。
  2. 可執(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è)特殊變體。

1.4.1 appAOT

源碼閱讀: 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ī)代碼福铅。

1.4.1-1

最初,所有的動(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)谦絮。

1.4.1.3

對(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, RawICDataDRT_MegamorphicCacheMissHandler)砸捏。

1.4.1.4

最后谬运,如果線性數(shù)組中的檢查次數(shù)增長超過了閾值隙赁,則會(huì)切換為類似字典的結(jié)構(gòu)。(參看 MegamorphicCallStub, RawMegamorphicCacheDRT_MegamorphicCacheMissHandler)梆暖。

1.4.1.5

2 運(yùn)行時(shí)系統(tǒng)(譯注:原文未完)

3 對(duì)象模型(譯注:原文未完)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末伞访,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子轰驳,更是在濱河造成了極大的恐慌厚掷,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件滑废,死亡現(xiàn)場離奇詭異蝗肪,居然都是意外死亡袜爪,警方通過查閱死者的電腦和手機(jī)蠕趁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辛馆,“玉大人俺陋,你說我怎么就攤上這事£几荩” “怎么了腊状?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長苔可。 經(jīng)常有香客問我缴挖,道長,這世上最難降的妖魔是什么焚辅? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任映屋,我火速辦了婚禮,結(jié)果婚禮上同蜻,老公的妹妹穿的比我還像新娘棚点。我一直安慰自己,他們只是感情好湾蔓,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布瘫析。 她就那樣靜靜地躺著,像睡著了一般默责。 火紅的嫁衣襯著肌膚如雪贬循。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天桃序,我揣著相機(jī)與錄音杖虾,去河邊找鬼。 笑死葡缰,一個(gè)胖子當(dāng)著我的面吹牛亏掀,可吹牛的內(nèi)容都是我干的忱反。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼滤愕,長吁一口氣:“原來是場噩夢啊……” “哼温算!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起间影,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤注竿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后魂贬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體巩割,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年付燥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宣谈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡键科,死狀恐怖闻丑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情勋颖,我是刑警寧澤嗦嗡,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站饭玲,受9級(jí)特大地震影響侥祭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜茄厘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一矮冬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蚕断,春花似錦欢伏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至葛假,卻和暖如春障陶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背聊训。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國打工抱究, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人带斑。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓鼓寺,卻偏偏與公主長得像勋拟,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子妈候,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354