查看原文
其他

Dart编译技术在服务端的探索和应用

无浩 闲鱼技术 2022-09-06

前言


最近闲鱼技术团队在Flutter+Dart的多端一体化的基础上,实现了基于FaaS研发模式,Dart为FaaS的语言容器。Dart吸取了其它高级语言设计的精华,例如Smalltalk的Image技术,此外JVM的HotSpot和Dart编译技术又师出同门。由Dart实现的语言容器,我们相信它在启动速度、运行性能会有不错的表现。同时Dart提供了AoT、JIT的编译方式,JIT又有Kernel和AppJIT的运行模式,为了提升应用性能如何选择合理的编译方式?

另外,服务端应用一般有各自的特点,若按生命周期长短来分有短周期应用和长周期应用,编译工作在不同应用的性能影响也有不同。接下来我们用一些有典型特点的案例来引入我们在Dart编译方案的实践和思考。

短周期应用


01

EmptyMain

例子是一个空函数实现,以此来评估语言平台本身的启动性能,我们使用默认参数编译一个snapshot。

  1. #1.默认条件下的app-jit snapshot生成

  2. dart snapshot-kind=app-jit snapshot=empty_main.snapshot empty_main.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 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

测试结果

  • 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秒,还远远达不到极致体验的要求。我们这里通过一些测试,来寻找提升性能的途径。

faastool是一个完全用Dart编写的代码编译、生成工具。依托于faastool, Function的编写者不用关心如何打包、接入中间件,faastool提供一系列的模版及代码生成工具可以将用户的使用成本降低,此外faastool还提供了HotReload机制可以快速响应变更。

这次我们提供了基于AoT、Kernel、AppJIT的用例来执行Function构建流程,分别记录时间消耗、中间产物大小、产物生成时间。为了验证在JIT场景下DartVM是否可通过调整Complier的行为带来性能提升,我们增加了JIT的测试分组。

测试结果


  • AoT>AppJIT>kernel,其中AoT比优化后的AppJIT有3倍左右性能提升,性能是Source的1000倍。

  • JIT(Kernel, AppJIT)分组下,通过在运行时减少CompilerOptimize或暂停PGO可以提升性能。

很显然faas_tool最终选择了AoT编译,但是性能结果和Case2大相径庭,为了搞清楚原因我们进一步做一下CPU Profile。

04

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调优和命令代码

  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. }

测试结果


  • 上面三种无论是何种方式启动,最终的运行时性能趋向一致,编译成本在后期可以忽略不计,这也是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在编译器后端技术上对服务器级的处理器架构做更多优化。

已开源|2亿用户背后的Flutter应用框架Fish Redux

重磅系列文章|“UI2Code”智能生成Flutter代码

老代码多=过度耦合=if else?阿里工程师这么捋直老代码



附:案例环境

  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



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存