Kotlin 开发者社区
01
—
概述
“ 方舟编译器概述:方舟编译器是为支持多种编程语言、多种芯片平台的联合编译、运行而设计的统一编程平台,包含编译器、工具链、运行时等关键部件。方舟编译器还在持续演进中,陆续将上述能力实现和开源。”
面向多设备、支持多语言的统一编程平台。
OpenArkCompiler是来自华为方舟编译器的开源项目。
能够将不同语言代码编译成一套可执行文件,在运行环境中高效执行:
支持多语言联合优化、消除跨语言调用开销;
更轻量的语言运行时;
软硬协同充分发挥硬件能效;
支持多样化的终端设备平台
硬件发展趋势:智能时代,万物互联,终端设备复杂多样,逐步形成以手机为中心,多设备互联互通的发展趋势。
软件生态发展诉求:终端设备多种多样,应用场景层出不穷,编程语言、运行环境多样化,不同编程语言之间的互通效率持续影响应用性能,不同设备平台的差异对开发者带来不便。
方舟编译器带来的解决方案:通过多语言统一IR表示,可实现应用中多种编程语言联合编译优化提升性能;在支持多平台的同时,根据设备特征提供便捷的开发与部署策略提升效率。
方舟编译器从 2019 年 8 月 31日开源。 开源计划:2019 年 8 月重点开源框架部分;后续将陆续开源编译器前端、后端;支持 Java、Kotlin 程序编译、JavaScript 语言应用的编译等。
02
—
方舟编译器架构设计
(一切皆是映射)
方舟编译器架构示意图
当前方舟编译器支持Java/Kotlin程序字节码的前端输入,其它编程语言的支持(如 C/C++/JS 等)还在规划中,方舟编译器的中间表示(IR)转换器将前端输入转换成方舟IR,并输送给后端的优化器,最终生成二进制文件,二进制文件与编译器运行时库文件链接生成可执行文件,在方舟的运行环境中就可执行该文件
方舟编译器IR是支持程序编译和运行的中间程序表示。程序源代码中的任何信息对于程序分析和优化都是有帮助的,所以方舟IR的目标是尽可能完整详细地提供源程序的信息。关于方舟编译器IR的详细信息,请参考文档:方舟IR设计
方舟编译器开源范围请参考 这里。
首次开源范围是编译器 IR( Intermediate Representation)、RC(Reference Counting)和多语言设计思想等,用于与业界、学术界沟通交流。后续将陆续开源编译器前端、后端,支持其它语言(如 JavaScript)的编译等,当前部分Java语言特性和JVM虚拟机特性的支持未包括在本次开源代码中,包括:annotation、lambda表达式、泛型等。目前仍有很多地方不完善,会在社区陆续迭代,遇到问题请在社区提交 issue,欢迎在社区继续讨论设计和代码共建。
RC API 引用计数(Reference Counting, RC)是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。
使用引用计数技术可以实现自动资源管理的目的。同时引用计数还可以指使用引用计数技术回收未使用资源的垃圾回收算法。 由于需要支持RC操作,运行时为方舟编译器提供了如下API,以便其更好的生成相关代码。
https://gitee.com/harmonyos/OpenArkCompiler/blob/master/doc/RC_API.md
引用计数(Reference Counting, RC)是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术可以实现自动资源管理的目的。同时引用计数还可以指使用引用计数技术回收未使用资源的垃圾回收算法。朴素版RC(Naive RC)是一种简单直接的RC插入操作。
对象的引用计数的来源:
堆内对象(其它对象、本身)的引用
栈上的引用(包含寄存器)
静态、全局变量
引用计数操作的插入规则(编译器和运行时),对应上面的:
Object field的赋值,需要将field指向的新对象+1,原对象计数-1
读取对象到栈上局部变量(含寄存器),需要对读取的对象引用计数+1
局部变量Last Use后引用计数-1
返回对象,引用计数+1,补偿局部变量Last Use后-1
简单示例
class A { static Object static_field; Object instance_field; A() { static_field = new Object(); } } Object foo(){ A a = new A(); bar(a, new Object()) return a.instance_field; } void bar(A a, Object o) { a.instance_field = o; }
class A { A() { local_var t = new Object(); // t是赋值给static_field过程中使用的临时变量 old = static_field; static_field = t; IncRef(t); DecRef(old); // 更新堆上RC DecRef(t); // 函数退出释放栈上RC } } Object foo(){ A a = new A(); bar(a, new Object()); locl_var t = a.instance_field; IncRef(t) // 栈上变量引用RC+1 IncRef(t) // 函数返回,返回值RC+1 DecRef(a) // 函数退出释放栈上RC,释放a DecRef(t) // 函数退出释放栈上RC return t; } void bar(A a, Object o) { old = a.instance_field a.instance_field = o; IncRef(o); DecRef(old); }
插入后
插入前
03
—
IR 代码示例
编译器不是直接将源语言翻译为目标语言,而是翻译为一种“中间语言”,称之为“IR”--指令集,之后再由中间语言,利用后端程序和设备翻译为目标平台的汇编语言。中间语言相当于一款编译器前端和后端的“桥梁”。不同编译器的中间语言IR是不一样的,而IR可以说是集中体现了这款编译器的特征—-他的算法,优化方式,汇编流程等。
函数定义和算术表达式 Function definitions and arithmetic expressions:
C 代码:
int foo(int i,int j){ return(i + j)* -998; }
Maple IR:
func &foo (var %i i32, var %j i32) i32 { return ( mul i32 ( add i32 (dread i32 %i, dread i32 %j), constval i32 -998))}
循环和数组 Loops and arrays
C source:
float a[10]; void init(Void){ int I; for(i=0; I<10; i++) a[i]=i*3; }
Maple IR:
var $a <[10] f32> func &init() void{ var %i i32 dassign %i(constval i32 0) while( It i32 i32(dread i32 %i, constval i32 10)){ iassign<*[10] f32>( array a32<*[10] f32>(addrof a32 $a, dread i32 %i), mul f32(dread i32 %i, constval i32 3)) dassign %i( add i32(dread i32 %i, constval i32 1))}}
Types and structs
C source:
typedef struct SS{ int f1; char f2:6; char f3:2; }SS; SS foo(SS x){ x.f2=33; return x; }
Maple IR:
type $SSfunc &foo (var %x <$SS>) <$SS> { dassign %x 2 (constval i32 32 ) return (dread agg %x)}
if-then-else, call and block
C source:
int fact(int n) { if(n!=1) return n*fact(n-1); else return 1; }
Maple IR:
func &fact (var %n i32) i32 { if (ne i32 (dread i32 %n, constval i32 1)) { call $fact (sub i32 (dread i32 %n, constval i32 1)) return (mul i32 (dread i32 %n, regread i32 %%retval)) } return(constval i32 1) }
参考文档:https://gitee.com/harmonyos/OpenArkCompiler/blob/master/doc/MapleIRDesign.md
04
—
方舟编译器编码规范和设计原则
我们参考Kent Beck的简单设计四原则来指导我们的如何写出优秀的代码,如何有效地判断我们的代码是优秀的。
通过所有测试(Passes its tests)
尽可能消除重复 (Minimizes duplication)
尽可能清晰表达 (Maximizes clarity)
更少代码元素 (Has fewer elements)
以上四个原则的重要程度依次降低。这组定义被称做简单设计原则。
第一条强调的是外部需求,这是代码实现最重要的;第二点就是代码的模块架构设计,保证代码的正交性,保证代码更容易修改;第三点是代码的可阅读性,保证代码是容易阅读的;最后一点才是保证代码是简洁的,在简洁和表达力之间,我们更看重表达力。
C++是典型的面向对象编程语言,软件工程界已经有很多OOP原则来指导我们编写大规模的,高可扩展的,可维护性的代码:
高内聚,低耦合的基本原则
SOLID原则
迪米特法则
“Tell,Don’t ask”原则
组合/聚合复用原则
我们希望C++应该是静态类型安全的,这样可以减少运行时的错误,提高代码的健壮性。但是由于C++的下面的特性存在,会破坏C++静态类型安全,我们针对这部分特性要仔细处理。
unions联合体
类型转换cast
缩窄转换narrowing conversions
类型退化type decay
范围错误range errors
void*
类型指针
我们可以通过约束这些特性的使用,或者使用C++的新特性,比如variant(C++17),GSL的span,narrow_cast等来解决这些问题,提高C++代码的健壮性。
希望通过使用ISO C++标准的特性来编写C++代码,对于ISO标准中未定义的或者编译器实现的特性要谨慎使用,对于GCC等编译器的提供的扩展特性也需要谨慎使用,这些特性会导致代码的可移植性比较差。
注意:如果模块中需要使用相关的扩展特性来,那么尽可能将这些特性封装成独立的接口,并且可以通过编译选项关闭或者编译这些特性。对于这些扩展特性的使用,请模块制定特性编程指南来指导这些特性的使用。
通过编译器来优先保证代码健壮性,而不是通过编写错误处理代码来处理编译就可以发现的异常,比如:
通过const来保证数据的不变性,防止数据被无意修改。
通过gsl::span等来保证char数组不越界,而不是通过运行时的length检查。
通过static_assert来进行编译时检查。
全局变量,全局常量和全局类型定义由于都属于全局作用域,在项目中,使用第三方库中容易出现冲突。
命名空间将作用域细分为独立的,具名的作用域,可有效地防止全局作用域的命名冲突。
class,struct等都具有自己的类作用域。
具名的namespace可以实现类作用域更上层的作用域。
匿名namespace和static可以实现文件作用域。
对于没有作用域的宏变量,宏函数强烈建议不使用。
作用域的一些缺点:
虽然可以通过作用域来区分两个命名相同的类型,但是还是具有迷惑性。
内联命名空间会让命名空间内部的成员摆脱限制,让人迷惑。
通过多重嵌套来定义namespace,会让完整的命名空间比较冗长。
所以,我们使用命名空间的建议如下:
对于变量,常量和类型定义尽可能使用namespace,减少全局作用域的冲突
不要在头文件中使用using namespace
不要使用内联命名空间
鼓励在.cpp文件中通过匿名namespace或者static来封装,防止不必要的定义通过API暴露出去。
C++比起C语言更加类型安全,更加抽象。我们更推荐使用C++的语言特性来编程,比如使用string而不是char*
, 使用vector而不是原生数组,使用namespace而不是static。
参考文档:https://gitee.com/harmonyos/OpenArkCompiler/blob/master/doc/Programming_Specifications.md
05
—
方舟编译器的合作伙伴
国内第一Kotlin 开发者社区公众号,主要分享、交流 Kotlin 编程语言、Spring Boot、Android、React.js/Node.js、函数式编程、编程思想等相关主题。