最近閒魚技術團隊在Flutter+Dart的多端一體化的基礎上,實現了FaaS研發模式。Dart吸收了其它高級語言設計的精華,例如Smalltalk的Image技術、JVM的HotSpot和Dart編譯技術又師出同門。由Dart實現的語言容器,它能夠在啓動速度、運行性能有不錯的表現。Dart提供了AoT、JIT的編譯方式,JIT擁有Kernel和AppJIT的運行模式,此外服務端應用有各自不一樣的運行特色,那麼如何選擇合理的編譯方法來提高應用的性能?接下來咱們用一些有典型特色的案例來引入咱們在Dart編譯方案的實踐和思考。前端
相應的,咱們準備了短週期應用(EmptyMain & Fibonnacci & faas_tool),長週期應用(HttpServer)分別來講明不一樣的編譯方法在各類場景下的性能表現java
#實驗機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
短週期應用git
例子是一個空函數實現,以此來評估語言平臺自己的啓動性能,咱們使用默認參數編譯一個snapshotgithub
#1.默認條件下的app-jit snapshot生成 dart snapshot-kind=app-jit snapshot=empty_main.snapshot empty_main.dart
測試結果json
Case2. Fibonacci數列後端
咱們分別用C、Java、Dart用遞歸實現Fibonacci(50)數列,來考察編譯工做對性能的影響。服務器
long fibo(long n){ if(n < 2){ return n; } return fibo(n - 1) + fibo(n - 2); }
AppJIT使用優化閾值實現激進優化,這樣編譯器在Training Run中當即得到生成Optimized代碼數據結構
#2.執行激進優化 dart --no-background-compilation \ --optimization-counter-threshold=1 \ --snapshot-kind=app-jit \ --snapshot=fibonacci.snapshot fibonacci.dart
將Fibonacci編譯成Kernel架構
#3.生成Kernel snapshot dart --snapshot=fibonacci.snapshot fibonacci.dart
AoT的Runtime不在Dart SDK裏,須要自行編譯AoT Runtime併發
#4.AoT編譯 pkg/vm/tools/precompiler2 fibonacci.dart fibonacci.aot #5.AoT的方式執行 out/ReleaseX64/dart_precompiled_runtime fibonacci.aot
測試結果
問題
AoT因爲自身的特性(和語言無關),沒法在運行時基於Profile實現代碼優化,峯值性能在此場景下要差不少,可是爲什麼Dart VM比HotSpot有25%的差距?接下來咱們針對Fibonacci作進一步優化
#6.編譯器調優,調整遞歸內聯深度 dart --inlining_recursion_depth_threshold=5 fibonacci.snapshot 50 #7.編譯器調優,HotSpot調整遞歸內聯深度 java -XX:MaxRecursiveInlineLevel=5 Fabbonacci 50
測試結果
也許也不難想象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的文章。
在前面咱們提到過Dart版本的FaaS語言容器,爲追求極致的研發體驗,咱們須要縮短用戶Function打包到部署運行的時間。就語言容器層面而言,Dart提供的Snapshot技術能夠大大提高啓動速度,可是從用戶Function到Snapshot(以下圖)生成所產生的編譯時間在不作優化的狀況下超過10秒,還遠遠達不到極致體驗的要求。咱們這裏經過一些測試,來尋找提高性能的途徑
faas_tool是一個徹底用Dart編寫的代碼編譯、生成工具。依託於faas_tool, Function的編寫者不用關心如何打包、接入中間件,faas_tool提供一系列的模版及代碼生成工具能夠將用戶的使用成本下降,此外faas_tool還提供了HotReload機制能夠快速響應變動。
此次咱們提供了基於AoT、Kernel、AppJIT的用例來執行Function構建流程,分別記錄時間消耗、中間產物大小、產物生成時間。爲了驗證在JIT場景下DartVM是否可經過調整Complier的行爲帶來性能提高,咱們增長了JIT的測試分組
測試結果
FaaS編譯工具-執行狀況柱形圖
很顯然faas_tool最終選擇了AoT編譯,可是性能結果和Case2截然不同,爲了搞清楚緣由咱們進一步作一下CPU Profile
CPU Profile
AppJIT
Dart App-jit模式 43%以上的時間參與編譯,固然取消代碼優化,可讓編譯時間大幅降低,在優化狀況下能夠將這個比率降低到13%
Kernel
Kernel模式有61%以上的CPU時間參與編譯工做, 若是關閉JIT優化代碼生成,性能有15%左右提高,反之進行激進優化將有1倍左右的性能損耗
AoT下的編譯成本
AoT模式下在運行時幾乎編譯和優化成本(CompileOptimized、CompileUnoptimized、CompileUnoptimized 佔比爲0),直接以目標平臺的代碼執行,所以性能要好不少。
P.S. DartVM 的Profile模塊在後期的版本升級更改了Tag命名, 有須要進一步瞭解的讀者參考VM Tags
附:DartVM調優和命令代碼
#8.模擬單核並執行激進優化 dart --no-background-compilation \ --optimization-counter-threshold=1 \ tmp/faas_tool.snapshot.kernel #9.JIT下關閉優化代碼生成 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
長週期應用
HttpServer
咱們用一個簡單的Dart版的HttpServer做爲典型長週期應用的測試用例,該用例中有JsonToObject、ObjectToJson的轉換,而後response輸出。咱們分別用Source、Kernel以及AppJIT的方式在必定的併發量下運行一段時間
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對象轉JsonString request.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(); } }
測試結果
Y軸表示成功的請求量,X軸爲時間_
P.S. 長週期的應用Optimize Compiler會通過Optimize->Deoptimize->Reoptimize的過程, 因爲此案例比較簡
單,沒體現Deoptimize到Reoptimize的表現
附:VM調優腳本
#12.調整當前isolate的新生代大小,默認2M最大32M的新生代大小形成頻繁的YGC dart --new_gen_semi_max_size=512 \ --new_gen_semi_initial_size=512 \ http_server.dart \ --interval=2
總結和展望
Dart編譯方式的選擇
AppJIT減小了編譯預熱的成本,這個特性很是適合對一些高併發應用在線擴容。Kernel做爲Dart編譯技術的前端,其平臺無關性將繼續做爲整個Dart編譯工具鏈的基礎。
在FaaS構建方案的選擇
經過CPU Profile得出faas_tool是一個編譯成本主導的應用,最終選擇了AoT編譯方案,結果大大提高了語言容器的構建的構建速度,很好知足了faas對開發效率的訴求
仍需改進的地方
從JIT性能表現來看,DartVM JIT的運行時性和HotSpot相比有提高餘地,因爲Dart語言做爲服務端開發的歷史不長,也許隨着Dart在服務端的技術應用全面推廣,相信DarVM在編譯器後端技術上對服務器級的處理器架構作更多優化
原文連接 本文爲雲棲社區原創內容,未經容許不得轉載。