Dart編譯技術在服務端的探索和應用

閒魚技術發表於2019-04-14

前言

最近閒魚技術團隊在Flutter+Dart的多端一體化的基礎上,實現了基於FaaS研發模式,Dart為FaaS的語言容器。Dart吸取了其它高階語言設計的精華,例如Smalltalk的Image技術,此外JVM的HotSpot和Dart編譯技術又師出同門。由Dart實現的語言容器,我們相信它在啟動速度、執行效能會有不錯的表現。同時Dart提供了AoT、JIT的編譯方式,JIT又有Kernel和AppJIT的執行模式,為了提升應用效能如何選擇合理的編譯方式?

另外,服務端應用一般有各自的特點,若按生命週期長短來分有短週期應用和長週期應用,編譯工作在不同應用的效能影響也有不同。接下來我們用一些有典型特點的案例來引入我們在Dart編譯方案的實踐和思考。

短週期應用

0EmptyMain

例子是一個空函式實現,以此來評估語言平臺本身的啟動效能,我們使用預設引數編譯一個snapshot。

  1. #1.預設條件下的app-jit snapshot生成

  2. dart snapshot-kind=app-jit snapshot=empty_main.snapshot empty_main.dart

測試結果

Dart編譯技術在服務端的探索和應用

Dart編譯技術在服務端的探索和應用

  • 作為現代高階語言Dart和Java在啟動速度上在同一水平線;

  • C語言的啟動速度是其它語言的20x,基本原因是C沒有Java、Dart語言平臺的Runtime;

  • Kernel和AppJIT方式執行有穩定的微小差異,總體AppJIT優於Kernel。

02 Fibonacci數列

我們分別用C、Java、Dart用遞迴實現Fibonacci(50)數列,來考察編譯工作對效能的影響。

  1. long fibo(long n){

  2. if(n < 2){

  3. return n;

  4. }

  5. return fibo(n - 1) + fibo(n - 2);

  6. }

AppJIT使用最佳化閾值實現激進最佳化,這樣編譯器在Training Run中立即獲得生成Optimized程式碼

  1. #2.執行激進最佳化

  2. dart --no-background-compilation \

  3. --optimization-counter-threshold=1 \

  4. --snapshot-kind=app-jit \

  5. --snapshot=fibonacci.snapshot

  6. fibonacci.dart

將Fibonacci編譯成Kernel

  1. #3.生成Kernel snapshot

  2. dart --snapshot=fibonacci.snapshot fibonacci.dart

AoT的Runtime不在Dart SDK裡,需要自行編譯AoT Runtime

  1. #4.AoT編譯

  2. pkg/vm/tools/precompiler2 fibonacci.dart fibonacci.aot

  3. #5.AoT的方式執行

  4. out/ReleaseX64/dart_precompiled_runtime fibonacci.aot

測試結果

Dart編譯技術在服務端的探索和應用

Dart編譯技術在服務端的探索和應用

  • Dart JIT對比下,AppJIT在激進最佳化後效能稍好於Kernel,差距微小,編譯的成本佔比可以忽略不計;

  • Dart AoT模式下的效能約為JIT的1/6不到;

  • JIT執行模式下,HotSpot的執行效能最優,優於Dart AppJIT 25%以上;

  • 包括C語言在內的AoT執行模式效能均低於JIT,Dart AppJIT效能優於25%。

問題

AoT由於自身的特性(和語言無關),無法在執行時基於Profile實現程式碼最佳化,峰值效能在此場景下要差很多,但是為何Dart VM比HotSpot有25%的差距?接下來我們針對Fibonacci做進一步最佳化。

  1. #6.編譯器調優,調整遞迴內聯深度

  2. dart --inlining_recursion_depth_threshold=5 fibonacci.snapshot 50

  3. #7.編譯器調優,HotSpot調整遞迴內聯深度

  4. java -XX:MaxRecursiveInlineLevel=5 Fabbonacci 50

測試結果

Dart編譯技術在服務端的探索和應用

Dart編譯技術在服務端的探索和應用

  • HotSpot VM效能全面領先於Dart VM;兩者在最優情況下HotSpot VM的效能優於Dart 9%左右;

  • Dart VM 藉助JIT調優,效能有大幅提升,相比預設情況有40%左右的提升;

  • Dart AppJIT 效能微弱領先Kernel。

也許也不難想象JVM HotSpot目前在伺服器開發領域上的相對Dart成熟,相比HotSpot,DartVM的“出廠設定”比較保守,當然我們也可以大膽猜測,在服務端應用下應該還有除JIT的其它最佳化空間;
和Case1相同,Kernel模式的效能依然低於AppJIT,主要原因是Kernel在執行前期需要把AST轉換為堆資料結構、經歷Compile、Compile Optimize等過程,而在適當Training run後的AppJIT snapshot在VM啟動時以最佳化後的IL(中間程式碼)執行,但很快Kernel會追上App-jit,最後效能保持持平。有興趣的讀者可以參閱Vyacheslav Egorov Dart VM的文章。

03 Faas容器編譯工具


在前面我們提到過Dart版本的FaaS語言容器,為追求極致的研發體驗,我們需要縮短使用者Function打包到部署執行的時間。就語言容器層面而言,Dart提供的Snapshot技術可以大大提升啟動速度,但是從使用者Function到Snapshot(如下圖)生成所產生的編譯時間在不做最佳化的情況下超過10秒,還遠遠達不到極致體驗的要求。我們這裡透過一些測試,來尋找提升效能的途徑。

Dart編譯技術在服務端的探索和應用

faastool是一個完全用Dart編寫的程式碼編譯、生成工具。依託於faastool, Function的編寫者不用關心如何打包、接入中介軟體,faastool提供一系列的模版及程式碼生成工具可以將使用者的使用成本降低,此外faastool還提供了HotReload機制可以快速響應變更。

這次我們提供了基於AoT、Kernel、AppJIT的用例來執行Function構建流程,分別記錄時間消耗、中間產物大小、產物生成時間。為了驗證在JIT場景下DartVM是否可透過調整Complier的行為帶來效能提升,我們增加了JIT的測試分組。

測試結果

Dart編譯技術在服務端的探索和應用
Dart編譯技術在服務端的探索和應用
  • AoT>AppJIT>kernel,其中AoT比最佳化後的AppJIT有3倍左右效能提升,效能是Source的1000倍。

  • JIT(Kernel, AppJIT)分組下,透過在執行時減少CompilerOptimize或暫停PGO可以提升效能。

很顯然faas_tool最終選擇了AoT編譯,但是效能結果和Case2大相徑庭,為了搞清楚原因我們進一步做一下CPU Profile。

04 CPU profile

AppJIT

Dart編譯技術在服務端的探索和應用

Dart App-jit模式 43%以上的時間參與編譯,當然取消程式碼最佳化,可以讓編譯時間大幅下降,在最佳化情況下可以將這個比率下降到13%。

Kernel

Dart編譯技術在服務端的探索和應用

Kernel模式有61%以上的CPU時間參與編譯工作, 如果關閉JIT最佳化程式碼生成,效能有15%左右提升,反之進行激進最佳化將有1倍左右的效能損耗。

AoT下的編譯成本
Dart編譯技術在服務端的探索和應用AoT模式下在執行時幾乎編譯和最佳化成本(CompileOptimized、CompileUnoptimized、CompileUnoptimized 佔比為0),直接以目標平臺的程式碼執行,因此效能要好很多。

P.S. DartVM 的Profile模組在後期的版本升級更改了Tag命名, 有需要進一步瞭解的讀者參考VM Tags

附:DartVM調優和命令程式碼

  1. #8.模擬單核並執行激進最佳化

  2. dart --no-background-compilation \

  3. --optimization-counter-threshold=1 \

  4. tmp/faas_tool.snapshot.kernel

  5. #9.JIT下關閉最佳化程式碼生成

  6. dart --optimization-counter-threshold=-1 \

  7. tmp/faas_tool.snapshot.kernel

  8. #10. Appjit verbose snapshot

  9. dart --print_snapshot_sizes \

  10. --print_snapshot_sizes_verbose \

  11. --deterministic \

  12. --snapshot-kind=app-jit \

  13. --snapshot=/tmp/faas_tool.snapshot faas_tool.dart \

  14. #11.Profile CPU 和 timeline

  15. dart --profiler=true \

  16. --startup_timeline=true \

  17. --timeline_dir=/tmp \

  18. --enable-vm-service \

  19. --pause-isolates-on-exit faas_tool.snapshot

長週期應用

01 HttpServer

我們用一個簡單的Dart版的HttpServer作為典型長週期應用的測試用例,該用例中有JsonToObject、ObjectToJson的轉換,然後response輸出。我們分別用Source、Kernel以及AppJIT的方式在一定的併發量下執行一段時間。

  1. void processReq(HttpRequest request){

  2. try{

  3. final List<Map<String,dynamic>> buf = <Map<String,dynamic>>[];

  4. final Boss boss = new Boss(numOfEmployee: 10);

  5. //Json反序列化物件

  6. getHeadCount(max: 20).forEach((hc){

  7. boss.hire(hc.idType, hc.docId);

  8. buf.add(hc.toJson());

  9. });

  10. request.response.headers.add('cal','${boss.calc()}');

  11. //Json物件轉JsonString

  12. request.response.write(jsonEncode(buf));

  13. request.response.close()

  14. .then((v) => counter_success ++)

  15. .timeout(new Duration(seconds:3))

  16. .catchError((e) => counter_fail ++));

  17. }

  18. catch(e){

  19. request.response.statusCode = 500;

  20. counter_fail ++;

  21. request.response.close();

  22. }

  23. }

測試結果

Dart編譯技術在服務端的探索和應用

  • 上面三種無論是何種方式啟動,最終的執行時效能趨向一致,編譯成本在後期可以忽略不計,這也是JIT的執行特點。

  • 在AppJIT模式下在應用啟動起初就有接近峰值的效能,即使在Kernel模式下也需要時間預熱達到峰值效能,Source模式下VM啟動需要2秒以上,因此需要相對更長時間達到峰值效能。從另一方面看應用很快完成了預熱,不久達到了峰值效能。

P.S. 長週期的應用Optimize Compiler會經過Optimize->Deoptimize->Reoptimize的過程, 由於此案例比較簡 單,沒體現Deoptimize到Reoptimize的表現

VM調優指令碼

  1. #12.調整當前isolate的新生代大小,預設2M最大32M的新生代大小造成頻繁的YGC

  2. dart --new_gen_semi_max_size=512 \

  3. --new_gen_semi_initial_size=512 \

  4. http_server.dart \

  5. --interval=2

總結和展望

我們透過對在服務端開發中幾種常見特徵應用的測試,我們瞭解到,

Dart編譯方式的選擇

  • 編譯成本為主導的應用,優先考慮AoT來提高應用效能;

  • 大多數長週期的應用在啟動後期編譯成本可忽略,應該選擇JIT方式並開啟Optimize Compiler執行;

  • 大多數長週期的應用可以選擇Kernel的方式來提升啟動速度,透過AppJIT的方式進一步縮短warmup時間。

AppJIT減少了編譯預熱的成本,這個特性非常適合對一些高併發應用線上擴容。Kernel作為Dart編譯技術的前端,其平臺無關性將繼續作為整個Dart編譯工具鏈的基礎。

在FaaS構建方案的選擇

透過CPU Profile得出faas_tool是一個編譯成本主導的應用,最終選擇了AoT編譯方案,結果大大提升了語言容器的構建的構建速度,很好滿足了faas對開發效率的訴求。

仍需改進的地方

從JIT效能表現來看,DartVM JIT的執行時性和HotSpot相比有提升餘地,由於Dart語言作為服務端開發的歷史不長,也許隨著Dart在服務端的技術應用全面推廣,相信DarVM在編譯器後端技術上對伺服器級的處理器架構做更多最佳化。


附:案例環境

  1. #實驗機1

  2. Mac OS X 10.14.3

  3. Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz * 4 / 16GB RAM

  4. #實驗機2

  5. Linux x86_64

  6. Intel(R) Xeon(R) CPU E5-2650 v2 @ 2.60GHz * 4 / 8GB RAM

  7. #Dart版本

  8. Dart Ver. 2.2.1-edge.eeb8fc8ccdcef46e835993a22b3b48c0a2ccc6f1

  9. #Java HotSpot版本

  10. Java build 1.8.0_121-b13

  11. Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

  12. #GCC版本

  13. Apple LLVM version 10.0.1 (clang-1001.0.46.3)

  14. Target: x86_64-apple-darwin18.2.0

  15. Thread model: posix

相關文章