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