作者:xianyuqiang 编译器首席架构师
ArkCompiler(方舟编译器)是组件化、可配置的多语言编译和运行平台,它既能支撑单一语言运行环境,也能支撑多种语言组合的运行环境。它目前主要支持的语言是JavaScript、TypeScript和Java。
一、概述
HarmonyOS的设计目标,是成为打通手机、PC、平板、电视、车机和智能穿戴等多种设备的统一操作系统。
图1 多设备互联
其应用开发有多编程语言、多范式的支持需求,其中高级编程语言包括JavaScript、TypeScript、Java等,开发范式包括声明式UI范式、分布式编程范式。我们需要相应的编译器和运行时来支撑这些高级应用编程语言的高效开发、部署和运行。使应用开发者能使用同一套开发框架实现一次开发多端部署运行。并且让使用HarmonyOS设备的用户,能获得统一的用户体验。于是,ArkCompiler应运而生。
- 目标
ArkCompiler是为支持多种编程语言、多种芯片平台的联合编译、运行而设计的统一编程平台,其设计目标是提供一个语言可插拔、组件可配置的多语言编译器运行时。
语言可插拔:设计架构上支持多种语言接入,ArkCompiler有能力提供具有高效执行性能且具有跨语言优势的多语言运行时,也可以在小设备上提供高效轻量的单一语言运行时。
组件可配置:ArkCompiler具有丰富的编译器运行时组件系统。通过定制化配置编译运行时的语言和组件,以支持手机、PC、平板、电视、汽车和智能穿戴等多种设备上不同的性能和内存需求。
- 架构
如图2所示,ArkCompiler包含编译器、工具链、运行时等关键部件。ArkCompiler工具链实现对应语言的前端编译器,将前端开发框架的高级语言编译成统一的字节码/二进制文件。根据不同的应用场景,通过ArkCompiler运行时解释器解释执行字节码文件或JIT/AOT编译器编译执行对应体系架构的优化机器码,从而提升运行效率和启动性能。
图2 ArkCompiler运行原理
下面,本文将从前端编译器,运行时展开介绍。
二、前端编译器
前端编译器是高级语言通往语言运行时的桥梁,它按照语言规范,将编程语言表达的语义翻译为运行时能够理解的介质,在ArkCompiler解决方案里,这体现为ArkCompiler字节码。即图3中的ArkCompiler Bytecode(简称abc)。部分语言,也支持通过ArkCompiler的AOT Compiler组件直接将字节码编译成对应体系架构的优化机器码。
图3 ArkCompiler前端
- 前端编译器功能
在需要支持多种语言的ArkCompiler中,前端编译器的主要作用是在Host侧把源码生成字节码文件,这样的优点:
利用Host强大的计算能力,能够在运行前做更多更复杂的算法优化,减少运行时的工作,提高运行效率。
相比常见的JavaScript运行时,可以把端侧的编译解析过程提前到发布前,提升程序的启动性能。
图4 JavaScript运行流程
编译优化
ArkCompiler提供对TypeScript(TS)的原生支持。在前端编译TS源码时,会利用TS的显式类型声明,应用类型推导进行类型优化,并且将推导出的类型信息通过字节码文件保留至运行时,由此运行时可以直接利用类型信息执行快速路径。此外,静态的类型分析和推导也使得TS AOT (Ahead of Time) Compiler成为可能,静态分析得到的类型信息帮助AOT Compiler直接编译生成高质量的机器码,使得TS源码可以直接以机器码形式运行,进一步提升运行性能。
图5 编译优化
- ArkCompiler字节码
ArkCompiler字节码(ArkCompiler Bytecode)是运行时解释器能够解析运行的一种硬件和平台无关的中间表现形式,以紧凑、可扩展、多语言支持作为设计目标。屏蔽设备的差异,支持应用的跨设备分发、部署和运行。ArkCompiler采用的是基于寄存器的字节码格式。每个寄存器的宽度为64位,最多支持65536个寄存器。
(1)寄存器
ArkCompiler寄存器要求能够放置对象引用和基本类型,宽度采用64位。寄存器的作用域是以函数栈帧为范围。在字节码指令编码中,寄存器索引支持4位、8位以及16位的变长编码,在支持方法内不同数量范围的寄存器寻址的同时减小字节码尺寸。
(2)累加寄存器
累加寄存器,俗称累加器,是一个特殊的寄存器,被指令隐含使用。使用累加器的主要目的是在不损失性能的前提下改善指令编码密度。在ArkCompiler字节码中,上一条指令利用累加器作为结果输出,下一条指令将此累加器作为输入,可以有效改善指令密度,减小字节码的尺寸。同时,通过在生成字节码阶段的数据流及控制流分析和优化,前端编译器可以有效消除冗余的累加器load和store操作。
(3)基本类型支持
ArkCompiler字节码提供对32位(i32)和64位(i64)整型数值的寄存器操作支持,8位和16位数值通过扩展到32位来模拟。支持对IEEE-754双精度浮点f64值的寄存器的操作,f32数据类型(IEEE-754单精度)也通过转换为f64值进行模拟。基本数据类型不需要虚拟机进行记录、跟踪和推导,而是通过操作不同基本数据类型的专用字节码进行表示,包括整数值的符号性。为了更有效地利用字节码的指令空间,设计中对高频使用的数据类型和操作引入更多的专用字节码,而对低频使用的数据类型和操作采用更通用的字节码。
(4)语言相关类型支持
ArkCompiler根据其执行的语言支持层次化的类型系统。这样,创建或者从常量池加载的字符串、数组、异常对象等,都是含有相应层次关系的、和具体语言规范相匹配的数据对象。
(5)动态类型语言支持
为支持类似JS/TS的动态类型语言,ArkCompiler通过特殊的标记值("Any")表示动态类型值,其包装了值本身和相应的类型信息(包括基本类型和对象引用类型数据)。虚拟寄存器的宽度可以容纳“Any”值。同时,在动态类型语言代码的执行上下文中,也可能使用到包含类型检查指令在内的静态确定类型指令序列,以表示动态类型相关语义。
三、ArkCompiler运行时
ArkCompiler运行时,如图6所示,被分为了核心运行时(Core Runtime)和各自语言独立的运行时插件(Runtime Plugin)。
核心运行时主要由运行时的公共核心组件构成,包含定义字节码格式和行为的Public ISA模块,对接系统调用的ArkCompiler Base Platform模块, 支持Debugger、Profiler等工具的Common Tool模块和承载字节码文件处理的ArkCompiler File模块等。也提供了可选的语言无关的解释器、内存管理、编译器和并发等基础设施组件。
各语言运行时插件则包含各语言特有的特性实现以及标准库来支撑语言的运行行为符合对应的语言规范,由各语言按需定制。
图6 运行时框架
- 执行引擎
ArkCompiler运行时执行引擎有多种组件,包括解释器、JIT编译器和AOT编译器,如图7所示。
图7 执行引擎结构
(1)解释器
解释器可直接运行前端编译器输出的字节码。
(2)JIT Compiler
JIT编译器一般需要运行时执行代码一段时间,Profiler生成了profiling数据之后,根据profiling数据即时编译生成高质量的机器码(上图Optimized Code II)来运行。(JIT可以根据代码执行情况实时编译生成最优机器指令)
(3)AOT Compiler
AOT编译器则是在运行前根据静态信息直接编译生成高质量的目标机器码(上图Optimized Code I)在设备上运行,PGO(Profile Guided Optimization)配置文件可以作为AOT Compiler的输入之一,给AOT Compiler一些指示,比如编译的范围以及编译某个方法时使用哪些优化技术。通常这种PGO配置文件由在同等规格的设备上经过运行时profiling或者大数据分析生成。
无论是JIT 编译器生成的优化代码,还是AOT编译器生成的优化代码,通常都是在一定优化假设或者优化推断的前提下生成的。如果这个前提在运行时不成立,则需要进行Deopt(逆优化),回退到解释器执行,这种情况一般较少发生。
- 定制化需求
各个执行引擎的性能如图8所示:
图8 各执行引擎的性能对比
ArkCompiler运行时通过不同执行模式的按需组合,支持多种设备不同的定制化需求。
在低端IOT设备上,ArkCompiler执行引擎支持纯解释器的执行模式,以满足小设备的内存限制条件;
在高端设备上,ArkCompiler执行引擎支持解释器配合AOT编译器以及JIT编译器的模式运行,对相当部分代码使用AOT编译器编译,使得程序一开始就可以运行在高质量的优化代码上,获得最好的执行性能;
在其它设备上,则根据设备的硬件条件限制来选择策略,设定高频使用需要AOT编译的代码范围,其它代码则依靠解释器配合JIT Compiler运行,使得应用执行性能能够得到最大化。
为了提升解释执行性能,在特定的体系架构下,解释器约定了将解释执行上下文中某些频繁使用的数据放在对应的物理寄存器中,比如在Arm64架构下,上下文中当前字节码指令地址、累加器值、解释器栈帧、指令映射表、当前线程对象等,直接放在固定的寄存器上,避免了在栈上频繁的加载和写入操作。
- 并发
复杂移动应用的开发和运行对并发有较强的需求。ArkCompiler运行时除了提供标准的“Java多线程编程”和“运行支持”之外,也提供响应式的Actor并发编程模型支持。此模型下执行体之间不共享任何数据,通过消息机制进行通信。当前,业界的一些Actor并发模型,例如传统JS引擎的web-worker实现,有启动速度慢、内存占用高等缺陷。
为了利用设备的多核能力获得更好的性能提升,在Actor内存隔离模型的基础上,ArkCompiler运行时通过共享Actor实例中的不可变或者不易变的对象、内建代码块、方法字节码等,提升Actor的启动性能和节省内存开销,达到实现轻量级Actor并发模型的目标。
图9 轻量级Actor实现
- 跨语言优化
HarmonyOS应用在某些情况下实际上是由多种语言的代码组成的。例如对HarmonyOS JS/TS应用,有一些系统库、框架和应用依赖的部分能力的实现使用了C/C++和Java语言。HarmonyOS开发框架也提供了JS/TS与C/C++交互的JS NAPI以及JS/TS与Java交互的Channel机制。考虑不同语言之间的交互场景的开发和运行效率需求,ArkCompiler和开发框架联合设计,提供了对应的优化机制。
(1)JS/TS与C/C++交互
在TS 版本的操作系统平台API实现中,通常需要面临C/C++代码访问和操作TS对象的场景。对这个业务场景,ArkCompiler可以根据TS源码的class声明和运行时约定,生成包含TS对象布局描述的C/C++头文件,以及操作这些TS对象的C/C++实现库。在C/C++代码中,通过包含TS对象描述头文件以及链接对应实现库,实现直接操作TS对象的效果。需要说明的是,由于TS类型或其内在布局并非总是固定不变的,因此在TS对象操作的代码实现中,会插入类型检查,如果对象类型或布局在运行时发生变化,则回退执行通用的慢速路径。
图10 跨语言交互
(2)JS/TS与Java交互
HarmonyOS中有一些应用所需的能力是通过系统、框架或应用的Java库提供的。因此在HarmonyOS应用中,也存在较多JS/TS代码与Java代码交互的场景。常见的案例中,由于JS/TS代码和Java代码有各自独立的运行环境,相互之间对于对方的数据表示、调用约定都是不可知的,所以JS/TS与Java的数据交互通常需要经过标准的JSON序列化和反序列化流程,以及经由Native层桥接的相互调用。这造成在一些场景中开销较大,影响用户体验。
ArkCompiler利用同时支持多语言的优势,运行时具备不同语言的数据表示、对象布局、函数调用约定等信息,这使得跨语言之间的直接数据访问、对象操作和方法调用成为可能,同时Java代码提供的更多确定的类型信息也成为JS/TS类型推导的额外输入,利于对JS/TS的编译优化。另一方面,这也使我们能为开发者提供一个更简化的多语言编程模型,减少需要额外手工编写的业务无关的跨语言交互代码工作量。
图11 简化的多语言编程模型
四、总结
HarmonyOS所支持的IoT时代下,结合应用生态、开发体验和用户体验等方面的需求, ArkCompiler与硬件、操作系统、开发框架、编程语言协同设计,在多语言统一编译运行和多设备支持的基础上,实现对HarmonyOS应用在开发和运行效率等方面的提升。
未来,ArkCompiler在持续优化基础体验的同时,更会进一步结合HarmonyOS万物互联的需求,在跨端迁移、多端协同等创新场景,从编译器和运行时等方面提供底层的解决方案和优化机制,提升分布式应用的开发和运行体验。