Dart编译技术在服务端的探索和应用
前言
最近閑魚技術(shù)團隊在Flutter+Dart的多端一體化的基礎(chǔ)上,實現(xiàn)了FaaS研發(fā)模式。Dart吸取了其它高級語言設(shè)計的精華,例如Smalltalk的Image技術(shù)、JVM的HotSpot和Dart編譯技術(shù)又師出同門。由Dart實現(xiàn)的語言容器,它可以在啟動速度、運行性能有不錯的表現(xiàn)。Dart提供了AoT、JIT的編譯方式,JIT擁有Kernel和AppJIT的運行模式,此外服務(wù)端應(yīng)用有各自不同的運行特點,那么如何選擇合理的編譯方法來提升應(yīng)用的性能?接下來我們用一些有典型特點的案例來引入我們在Dart編譯方案的實踐和思考。
案例詳情
相應(yīng)的,我們準(zhǔn)備了短周期應(yīng)用(EmptyMain & Fibonnacci & faas_tool),長周期應(yīng)用(HttpServer)分別來說明不同的編譯方法在各種場景下的性能表現(xiàn)
測試環(huán)境參考
#實驗機1 Mac OS X 10.14.3 Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz * 4 / 16GB RAM#實驗機2 Linux x86_64 Intel(R) Xeon(R) CPU E5-2650 v2 @ 2.60GHz * 4 / 8GB RAM#Dart版本 Dart Ver. 2.2.1-edge.eeb8fc8ccdcef46e835993a22b3b48c0a2ccc6f1 #Java HotSpot版本 Java build 1.8.0_121-b13 Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)#GCC版本 Apple LLVM version 10.0.1 (clang-1001.0.46.3) Target: x86_64-apple-darwin18.2.0 Thread model: posix短周期應(yīng)用
Case1. EmptyMain
例子是一個空函數(shù)實現(xiàn),以此來評估語言平臺本身的啟動性能,我們使用默認參數(shù)編譯一個snapshot
#1.默認條件下的app-jit snapshot生成 dart snapshot-kind=app-jit snapshot=empty_main.snapshot empty_main.dart測試結(jié)果
?
- 作為現(xiàn)代高級語言Dart和Java在啟動速度上在同一水平線
- C語言的啟動速度是其它語言的20x,基本原因是C沒有Java、Dart語言平臺的Runtime
- Kernel和AppJIT方式運行有穩(wěn)定的微小差異,總體AppJIT優(yōu)于Kernel
Case2. Fibonacci數(shù)列
我們分別用C、Java、Dart用遞歸實現(xiàn)Fibonacci(50)數(shù)列,來考察編譯工作對性能的影響。
long fibo(long n){if(n < 2){return n;}return fibo(n - 1) + fibo(n - 2); }AppJIT使用優(yōu)化閾值實現(xiàn)激進優(yōu)化,這樣編譯器在Training Run中立即獲得生成Optimized代碼
#2.執(zhí)行激進優(yōu)化 dart --no-background-compilation \--optimization-counter-threshold=1 \ --snapshot-kind=app-jit \--snapshot=fibonacci.snapshotfibonacci.dart將Fibonacci編譯成Kernel
#3.生成Kernel snapshot dart --snapshot=fibonacci.snapshot fibonacci.dartAoT的Runtime不在Dart SDK里,需要自行編譯AoT Runtime
#4.AoT編譯 pkg/vm/tools/precompiler2 fibonacci.dart fibonacci.aot#5.AoT的方式執(zhí)行 out/ReleaseX64/dart_precompiled_runtime fibonacci.aot測試結(jié)果
?
- Dart JIT對比下,AppJIT在激進優(yōu)化后性能稍好于Kernel,差距微小,編譯的成本占比可以忽略不計
- Dart AoT模式下的性能約為JIT的1/6不到
- JIT運行模式下,HotSpot的執(zhí)行性能最優(yōu),優(yōu)于Dart AppJIT 25%以上
- 包括C語言在內(nèi)的AoT運行模式性能均低于JIT,Dart AppJIT性能優(yōu)于25%
問題
AoT由于自身的特性(和語言無關(guān)),無法在運行時基于Profile實現(xiàn)代碼優(yōu)化,峰值性能在此場景下要差很多,但是為何Dart VM比HotSpot有25%的差距?接下來我們針對Fibonacci做進一步優(yōu)化
#6.編譯器調(diào)優(yōu),調(diào)整遞歸內(nèi)聯(lián)深度 dart --inlining_recursion_depth_threshold=5 fibonacci.snapshot 50#7.編譯器調(diào)優(yōu),HotSpot調(diào)整遞歸內(nèi)聯(lián)深度 java -XX:MaxRecursiveInlineLevel=5 Fabbonacci 50測試結(jié)果
- HotSpot VM性能全面領(lǐng)先于Dart VM;兩者在最優(yōu)情況下HotSpot VM的性能優(yōu)于Dart 9%左右
- Dart VM 借助JIT調(diào)優(yōu),性能有大幅提升,相比默認情況有40%左右的提升
- Dart AppJIT 性能微弱領(lǐng)先Kernel
也許也不難想象JVM HotSpot目前在服務(wù)器開發(fā)領(lǐng)域上的相對Dart成熟,相比HotSpot,DartVM的“出廠設(shè)置”比較保守,當(dāng)然我們也可以大膽猜測,在服務(wù)端應(yīng)用下應(yīng)該還有除JIT的其它優(yōu)化空間;
和Case1相同,Kernel模式的性能依然低于AppJIT,主要原因是Kernel在運行前期需要把AST轉(zhuǎn)換為堆數(shù)據(jù)結(jié)構(gòu)、經(jīng)歷Compile、Compile Optimize等過程,而在適當(dāng)Training run后的AppJIT snapshot在VM啟動時以優(yōu)化后的IL(中間代碼)執(zhí)行,但很快Kernel會追上App-jit,最后性能保持持平。有興趣的讀者可以參閱Vyacheslav Egorov?Dart VM的文章。
Case3. FaaS容器編譯工具
在前面我們提到過Dart版本的FaaS語言容器,為追求極致的研發(fā)體驗,我們需要縮短用戶Function打包到部署運行的時間。就語言容器層面而言,Dart提供的Snapshot技術(shù)可以大大提升啟動速度,但是從用戶Function到Snapshot(如下圖)生成所產(chǎn)生的編譯時間在不做優(yōu)化的情況下超過10秒,還遠遠達不到極致體驗的要求。我們這里通過一些測試,來尋找提升性能的途徑
faas_tool是一個完全用Dart編寫的代碼編譯、生成工具。依托于faas_tool, Function的編寫者不用關(guān)心如何打包、接入中間件,faas_tool提供一系列的模版及代碼生成工具可以將用戶的使用成本降低,此外faas_tool還提供了HotReload機制可以快速響應(yīng)變更。
這次我們提供了基于AoT、Kernel、AppJIT的用例來執(zhí)行Function構(gòu)建流程,分別記錄時間消耗、中間產(chǎn)物大小、產(chǎn)物生成時間。為了驗證在JIT場景下DartVM是否可通過調(diào)整Complier的行為帶來性能提升,我們增加了JIT的測試分組
測試結(jié)果
FaaS編譯工具-執(zhí)行情況柱形圖?
- AoT>AppJIT>kernel,其中AoT比優(yōu)化后的AppJIT有3倍左右性能提升,性能是Source的1000倍
- JIT(Kernel, AppJIT)分組下,通過在運行時減少CompilerOptimize或暫停PGO可以提升性能
很顯然faas_tool最終選擇了AoT編譯,但是性能結(jié)果和Case2大相徑庭,為了搞清楚原因我們進一步做一下CPU Profile
CPU Profile
AppJIT
Dart App-jit模式 43%以上的時間參與編譯,當(dāng)然取消代碼優(yōu)化,可以讓編譯時間大幅下降,在優(yōu)化情況下可以將這個比率下降到13%
Kernel
Kernel模式有61%以上的CPU時間參與編譯工作, 如果關(guān)閉JIT優(yōu)化代碼生成,性能有15%左右提升,反之進行激進優(yōu)化將有1倍左右的性能損耗
AoT下的編譯成本
AoT模式下在運行時幾乎編譯和優(yōu)化成本(CompileOptimized、CompileUnoptimized、CompileUnoptimized 占比為0),直接以目標(biāo)平臺的代碼執(zhí)行,因此性能要好很多。
P.S. DartVM 的Profile模塊在后期的版本升級更改了Tag命名, 有需要進一步了解的讀者參考VM Tags
附:DartVM調(diào)優(yōu)和命令代碼
#8.模擬單核并執(zhí)行激進優(yōu)化 dart --no-background-compilation \--optimization-counter-threshold=1 \ tmp/faas_tool.snapshot.kernel #9.JIT下關(guān)閉優(yōu)化代碼生成 dart --optimization-counter-threshold=-1 \ tmp/faas_tool.snapshot.kernel #10. Appjit verbose snapshot dart --print_snapshot_sizes \--print_snapshot_sizes_verbose \--deterministic \--snapshot-kind=app-jit \--snapshot=/tmp/faas_tool.snapshot faas_tool.dart \#11.Profile CPU 和 timeline dart --profiler=true \--startup_timeline=true \--timeline_dir=/tmp \--enable-vm-service \--pause-isolates-on-exit faas_tool.snapshot長周期應(yīng)用
HttpServer
我們用一個簡單的Dart版的HttpServer作為典型長周期應(yīng)用的測試用例,該用例中有JsonToObject、ObjectToJson的轉(zhuǎn)換,然后response輸出。我們分別用Source、Kernel以及AppJIT的方式在一定的并發(fā)量下運行一段時間
void processReq(HttpRequest request){try{final List<Map<String,dynamic>> buf = <Map<String,dynamic>>[];final Boss boss = new Boss(numOfEmployee: 10);//Json反序列化對象getHeadCount(max: 20).forEach((hc){boss.hire(hc.idType, hc.docId);buf.add(hc.toJson());});request.response.headers.add('cal','${boss.calc()}');//Json對象轉(zhuǎn)JsonStringrequest.response.write(jsonEncode(buf));request.response.close().then((v) => counter_success ++).timeout(new Duration(seconds:3)).catchError((e) => counter_fail ++));} catch(e){request.response.statusCode = 500;counter_fail ++;request.response.close();} }測試結(jié)果
?
Y軸表示成功的請求量,X軸為時間
- 上面三種無論是何種方式啟動,最終的運行時性能趨向一致,編譯成本在后期可以忽略不計,這也是JIT的運行特點
- 在AppJIT模式下在應(yīng)用啟動起初就有接近峰值的性能,即使在Kernel模式下也需要時間預(yù)熱達到峰值性能,Source模式下VM啟動需要2秒以上,因此需要相對更長時間達到峰值性能。從另一方面看應(yīng)用很快完成了預(yù)熱,不久達到了峰值性能
P.S. 長周期的應(yīng)用Optimize Compiler會經(jīng)過Optimize->Deoptimize->Reoptimize的過程, 由于此案例比較簡
單,沒體現(xiàn)Deoptimize到Reoptimize的表現(xiàn)
附:VM調(diào)優(yōu)腳本
#12.調(diào)整當(dāng)前isolate的新生代大小,默認2M最大32M的新生代大小造成頻繁的YGC dart --new_gen_semi_max_size=512 \--new_gen_semi_initial_size=512 \ http_server.dart \--interval=2總結(jié)和展望
Dart編譯方式的選擇
- 編譯成本為主導(dǎo)的應(yīng)用,應(yīng)優(yōu)先考慮AoT來提高應(yīng)用性能
- 長周期的應(yīng)用在啟動后期編譯成本可忽略,應(yīng)該選擇JIT方式并開啟Optimize?Compiler,讓優(yōu)化器介入
- 長周期的應(yīng)用可以選擇Kernel的方式來提升啟動速度,通過AppJIT的方式進一步縮短warmup時間
AppJIT減少了編譯預(yù)熱的成本,這個特性非常適合對一些高并發(fā)應(yīng)用在線擴容。Kernel作為Dart編譯技術(shù)的前端,其平臺無關(guān)性將繼續(xù)作為整個Dart編譯工具鏈的基礎(chǔ)。
在FaaS構(gòu)建方案的選擇
通過CPU Profile得出faas_tool是一個編譯成本主導(dǎo)的應(yīng)用,最終選擇了AoT編譯方案,結(jié)果大大提升了語言容器的構(gòu)建的構(gòu)建速度,很好滿足了faas對開發(fā)效率的訴求
仍需改進的地方
從JIT性能表現(xiàn)來看,DartVM JIT的運行時性和HotSpot相比有提升余地,由于Dart語言作為服務(wù)端開發(fā)的歷史不長,也許隨著Dart在服務(wù)端的技術(shù)應(yīng)用全面推廣,相信DarVM在編譯器后端技術(shù)上對服務(wù)器級的處理器架構(gòu)做更多優(yōu)化
?
原文鏈接
本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
總結(jié)
以上是生活随笔為你收集整理的Dart编译技术在服务端的探索和应用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 云栖专辑 | 阿里开发者们的第19个感悟
- 下一篇: 双11大考 POLARDB分钟级弹性让企