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

30次阅读

共计 5606 个字符,预计需要花费 15 分钟才能阅读完成。

前言
最近闲鱼技术团队在 Flutter+Dart 的多端一体化的基础上,实现了 FaaS 研发模式。Dart 吸取了其它高级语言设计的精华,例如 Smalltalk 的 Image 技术、JVM 的 HotSpot 和 Dart 编译技术又师出同门。由 Dart 实现的语言容器,它可以在启动速度、运行性能有不错的表现。Dart 提供了 AoT、JIT 的编译方式,JIT 拥有 Kernel 和 AppJIT 的运行模式,此外服务端应用有各自不同的运行特点,那么如何选择合理的编译方法来提升应用的性能?接下来我们用一些有典型特点的案例来引入我们在 Dart 编译方案的实践和思考。
案例详情
相应的,我们准备了短周期应用(EmptyMain & Fibonnacci & faas_tool), 长周期应用(HttpServer)分别来说明不同的编译方法在各种场景下的性能表现
测试环境参考
#实验机 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
短周期应用
Case1. EmptyMain
例子是一个空函数实现,以此来评估语言平台本身的启动性能,我们使用默认参数编译一个 snapshot
#1. 默认条件下的 app-jit snapshot 生成
dart snapshot-kind=app-jit snapshot=empty_main.snapshot empty_main.dart
测试结果

作为现代高级语言 Dart 和 Java 在启动速度上在同一水平线
C 语言的启动速度是其它语言的 20x,基本原因是 C 没有 Java、Dart 语言平台的 Runtime
Kernel 和 AppJIT 方式运行有稳定的微小差异,总体 AppJIT 优于 Kernel

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
测试结果

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 做进一步优化
#6. 编译器调优,调整递归内联深度
dart –inlining_recursion_depth_threshold=5 fibonacci.snapshot 50

#7. 编译器调优,HotSpot 调整递归内联深度
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 的文章。
Case3. FaaS 容器编译工具
在前面我们提到过 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 的测试分组
测试结果

AoT>AppJIT>kernel,其中 AoT 比优化后的 AppJIT 有 3 倍左右性能提升,性能是 Source 的 1000 倍
JIT(Kernel, AppJIT) 分组下,通过在运行时减少 CompilerOptimize 或暂停 PGO 可以提升性能

很显然 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();
}
}
测试结果
                                                                                                   

上面三种无论是何种方式启动,最终的运行时性能趋向一致,编译成本在后期可以忽略不计,这也是 JIT 的运行特点
在 AppJIT 模式下在应用启动起初就有接近峰值的性能,即使在 Kernel 模式下也需要时间预热达到峰值性能,Source 模式下 VM 启动需要 2 秒以上,因此需要相对更长时间达到峰值性能。从另一方面看应用很快完成了预热,不久达到了峰值性能

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 编译方式的选择

编译成本为主导的应用,应优先考虑 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 在编译器后端技术上对服务器级的处理器架构做更多优化。

本文作者:闲鱼技术 - 无浩阅读原文
本文为云栖社区原创内容,未经允许不得转载。

正文完
 0