写点什么

HDC 技术分论坛:ArkCompiler 原理解析

  • 2021 年 11 月 18 日
  • 本文字数:4706 字

    阅读完需:约 15 分钟

HDC技术分论坛:ArkCompiler原理解析

作者:xianyuqiang 编译器首席架构师

ArkCompiler(方舟编译器)是组件化、可配置的多语言编译和运行平台,它既能支撑单一语言运行环境,也能支撑多种语言组合的运行环境。它目前主要支持的语言是 JavaScript、TypeScript 和 Java。

一、概述

HarmonyOS 的设计目标,是成为打通手机、PC、平板、电视、车机和智能穿戴等多种设备的统一操作系统。

图 1 多设备互联

其应用开发有多编程语言、多范式的支持需求,其中高级编程语言包括 JavaScript、TypeScript、Java 等,开发范式包括声明式 UI 范式、分布式编程范式。我们需要相应的编译器和运行时来支撑这些高级应用编程语言的高效开发、部署和运行。使应用开发者能使用同一套开发框架实现一次开发多端部署运行。并且让使用 HarmonyOS 设备的用户,能获得统一的用户体验。于是,ArkCompiler 应运而生。

1. 目标

ArkCompiler 是为支持多种编程语言、多种芯片平台的联合编译、运行而设计的统一编程平台,其设计目标是提供一个语言可插拔、组件可配置的多语言编译器运行时。

语言可插拔:设计架构上支持多种语言接入,ArkCompiler 有能力提供具有高效执行性能且具有跨语言优势的多语言运行时,也可以在小设备上提供高效轻量的单一语言运行时。

组件可配置:ArkCompiler 具有丰富的编译器运行时组件系统。通过定制化配置编译运行时的语言和组件,以支持手机、PC、平板、电视、汽车和智能穿戴等多种设备上不同的性能和内存需求。

2. 架构

如图 2 所示,ArkCompiler 包含编译器、工具链、运行时等关键部件。ArkCompiler 工具链实现对应语言的前端编译器,将前端开发框架的高级语言编译成统一的字节码/二进制文件。根据不同的应用场景,通过 ArkCompiler 运行时解释器解释执行字节码文件或 JIT/AOT 编译器编译执行对应体系架构的优化机器码,从而提升运行效率和启动性能。

图 2 ArkCompiler 运行原理

下面,本文将从前端编译器,运行时展开介绍。

二、前端编译器

前端编译器是高级语言通往语言运行时的桥梁,它按照语言规范,将编程语言表达的语义翻译为运行时能够理解的介质,在 ArkCompiler 解决方案里,这体现为 ArkCompiler 字节码。即图 3 中的 ArkCompiler Bytecode(简称 abc)。部分语言,也支持通过 ArkCompiler 的 AOT Compiler 组件直接将字节码编译成对应体系架构的优化机器码。

图 3 ArkCompiler 前端

1. 前端编译器功能

在需要支持多种语言的 ArkCompiler 中,前端编译器的主要作用是在 Host 侧把源码生成字节码文件,这样的优点:

  • 利用 Host 强大的计算能力,能够在运行前做更多更复杂的算法优化,减少运行时的工作,提高运行效率。

  • 相比常见的 JavaScript 运行时,可以把端侧的编译解析过程提前到发布前,提升程序的启动性能。

图 4 JavaScript 运行流程

编译优化

ArkCompiler 提供对 TypeScript(TS)的原生支持。在前端编译 TS 源码时,会利用 TS 的显式类型声明,应用类型推导进行类型优化,并且将推导出的类型信息通过字节码文件保留至运行时,由此运行时可以直接利用类型信息执行快速路径。此外,静态的类型分析和推导也使得 TS AOT (Ahead of Time) Compiler 成为可能,静态分析得到的类型信息帮助 AOT Compiler 直接编译生成高质量的机器码,使得 TS 源码可以直接以机器码形式运行,进一步提升运行性能。

图 5 编译优化

2. 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 运行时框架

1. 执行引擎

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(逆优化),回退到解释器执行,这种情况一般较少发生。

2. 定制化需求

各个执行引擎的性能如图 8 所示:

图 8 各执行引擎的性能对比

ArkCompiler 运行时通过不同执行模式的按需组合,支持多种设备不同的定制化需求。

  • 在低端 IOT 设备上,ArkCompiler 执行引擎支持纯解释器的执行模式,以满足小设备的内存限制条件;

  • 在高端设备上,ArkCompiler 执行引擎支持解释器配合 AOT 编译器以及 JIT 编译器的模式运行,对相当部分代码使用 AOT 编译器编译,使得程序一开始就可以运行在高质量的优化代码上,获得最好的执行性能;

  • 在其它设备上,则根据设备的硬件条件限制来选择策略,设定高频使用需要 AOT 编译的代码范围,其它代码则依靠解释器配合 JIT Compiler 运行,使得应用执行性能能够得到最大化。

为了提升解释执行性能,在特定的体系架构下,解释器约定了将解释执行上下文中某些频繁使用的数据放在对应的物理寄存器中,比如在 Arm64 架构下,上下文中当前字节码指令地址、累加器值、解释器栈帧、指令映射表、当前线程对象等,直接放在固定的寄存器上,避免了在栈上频繁的加载和写入操作。

3. 并发

复杂移动应用的开发和运行对并发有较强的需求。ArkCompiler 运行时除了提供标准的“Java 多线程编程”和“运行支持”之外,也提供响应式的 Actor 并发编程模型支持。此模型下执行体之间不共享任何数据,通过消息机制进行通信。当前,业界的一些 Actor 并发模型,例如传统 JS 引擎的 web-worker 实现,有启动速度慢、内存占用高等缺陷。

为了利用设备的多核能力获得更好的性能提升,在 Actor 内存隔离模型的基础上,ArkCompiler 运行时通过共享 Actor 实例中的不可变或者不易变的对象、内建代码块、方法字节码等,提升 Actor 的启动性能和节省内存开销,达到实现轻量级 Actor 并发模型的目标。

图 9 轻量级 Actor 实现

4. 跨语言优化

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 万物互联的需求,在跨端迁移、多端协同等创新场景,从编译器和运行时等方面提供底层的解决方案和优化机制,提升分布式应用的开发和运行体验。


用户头像

每一位开发者都是华为要汇聚的星星之火 2021.10.15 加入

提供HarmonyOS关键技术解析、版本更新、开发者实践和活动资讯,欢迎各位开发者加入HarmonyOS生态,一起创造无限可能!

评论

发布
暂无评论
HDC技术分论坛:ArkCompiler原理解析