当前位置: 首页 > 科技观察

华为方舟编译器做了什么让安卓手感“丝滑”?

时间:2023-03-16 11:23:13 科技观察

敲黑板,先说几个名词:1.JIT的全称是Just-in-time,即时编译;当Java字节码运行在JVM上时,JVM要实时将字节码编译成机器码,这就是JIT。2、AOT的全称是Ahead-of-time,预编译;对应JIT,你的JIT不是实时的吗?那我就提前编译好了,也就是AOT。3.IR的整个过程是Intermediaterepresentation,即中间表示。中间表示是原始表示和目标表示之间的中间层。现代编译器分为前端和后端,前端和后端的分界线是IR。现代编译器的一般流程:词法分析->语法分析->语义分析->IR->优化->生成目标代码。关于华为给出的方舟编译器的解释,我们先看看方舟做了什么,推测一下方舟可能做了什么,或者说方舟能做什么。1、不需要虚拟机运行我们都知道Java字节码需要在Java虚拟机(JVM)上运行。JVM有两个最重要的功能:执行字节码和内存管理;我们分开说吧。执行字节码JVM在运行字节码时,会一条一条读取指令,然后将指令翻译成当前机器的机器码并执行操作,比如将当前栈上的两个数相加,然后再次压入Stack等,这个方法叫做解释执行。当JVM发现某些指令经常被执行时,每次翻译都会降低运行效率,于是JVM直接将这些指令编译成当前机器的机器码,下次直接执行机器码,不需要逐句翻译同样,这是JIT。内存管理写C代码的同学,要使用内存的时候,需要调用malloc函数动态申请一块内存。当不再使用内存时,他们需要调用free函数来释放内存。写Java代码的同学就没有这个困惑了,因为这个事情是JVM承包的。在执行字节码的过程中,JVM会调用gc(垃圾回收),gc会帮我们释放不需要的内存。方舟是怎么做到的?把上面的流程弄清楚后,我们就可以明白方舟编译器是怎么做的了。既然JVM在运行时可以将字节码编译成机器码(JIT),为什么不能在运行字节码之前将字节码编译成机器码呢?没错,方舟就是这么做的。我们称它为AOT。JVM的两大功能之一不需要执行字节码。内存管理函数呢?这也好办。华为可以提供一个实现gc所有功能的库。我们称这个库为runtime。以前我们是用JVM跑一段字节码,现在流程变了,先把字节码(或源程序)编译成机器码,然后带入runtime直接在操作系统上运行.虚拟机不见了。不再需要VM,运行时必不可少。这个运行时需要处理以下事情包括但不限于:创建对象、gc、函数调用、异常处理、锁、同步、多线程和反射。我们已经带了那么多功能了,再带一个解释器吧,再带一个也不嫌多。这些东西似曾相识。AndroidART好像也是这样?我猜是。由于Java语言本身和Java运行时库等一些历史原因,要推翻和去除所有这些东西是非常复杂的。高的;所以安卓之父谷歌也在这些基础上鼓捣。当然,华为也可以选择不支持Java中的一些动态特性,比如反射,这样这个runtime可能会被简化。方舟编译器和安卓现有的ART有什么区别,我们拭目以待。2.多语言联合优化编译这很神奇吧?C语言可以和Java语言一起编译。我们知道C语言的代码编译成二进制文件,Java语言的代码编译成字节码;事实上,现代编译器在编译过程中有很多层中间表示。如果将源代码层视为最高层,则将目标语言视为最高层,在编译过程中逐层下降,最低层下降到目标层,这与当我们走下楼梯,那不是自由落体,对吧?例如,源代码经过前端编译器后变成抽象语法树(AST),而抽象语法树又可以转化为另一种更底层的中间表示(IR),再从IR到目标层。因此,方舟可以定义一个中间表示(IR),将C语言和Java语言都编译到这个中间表示层,然后对中间表示层进行一系列的优化或分析,再从中间表示层编译到机器代码,从而实现多语言联合编译。是不是可以把不同的语言编译成同一个IR就万事大吉了?不是这样!为什么方舟会把多种语言编译在一起?好玩吗?当然不是!多语言联合编译至少有以下几点好处:减少跨语言调用开销不同的语言有不同的类型系统、调用规范、数据布局等,所以不同的时候会有一些额外的开销语言互相呼唤。我们知道Java调用C的接口规范叫做JNI。JNI帮助我们弥合了语言鸿沟,实现了Java和C之间的相互调用。AOT在跨越语言鸿沟方面有一些优势。不同的语言用同一个IR表示,runtime也是定制的。对象的数据布局与C中的对象数据布局保持一致,例如可以让C兼容Java的类型系统(Java语言可以看作是C++语言的子集)等;一致,运行程序时不需要再次转换,可以减少开销。跨语言优化一般情况下,不同的语言是分开编译的。方舟编译器将不同语言编译成同一个IR,方便合并不同语言的代码进行全局优化,比如常量传播、函数内联等。当所有代码都在同一个IR上时,还可以针对Java语言的特点做一些特定的静态分析,根据分析结果进行特定的优化,比如针对不同类型的函数调用去虚拟化等。什么是去虚拟化?简单来说,有些函数调用是间接调用,类似于函数指针调用。通过对这些间接调用的分析,部分间接调用可以转化为直接调用,属于跨语言直接调用。太奇妙了。3.更高效的内存回收机制内存回收是个大问题,Androidapp卡顿的部分原因就是内存回收。前面说过,Java的内存回收工作是由JVM接管的。写Java代码的同学不需要手动进行内存回收。JVM会在“适当”的时候进行内存回收。这个“合适”的时间,通常是在没有办法的时候,在记忆力耗尽的时候;比如我有一张干净的桌子(堆内存),我们在桌子上放了一些东西(消耗内存),当没有地方放新东西的时候,需要妈妈帮忙整理一下桌面(内存)回收)。JVM中的GC是如何判断哪些内存是需要的,哪些内存是不需要的呢?有一种技术叫做可达性分析,可以帮助我们判断哪些内存是可以回收的。可达性分析的总体思路是在JVM运行过程中创建了很多对象,这些对象之间存在着复杂的依赖关系。JVM首先确定一些对象是根对象。从根对象开始,标记所有直接依赖的对象和间接依赖的对象,不依赖的对象不需要使用,可以回收。当循环中有程序创建大量新对象时,内存会很快耗尽,然后会触发gc回收内存;频繁触发gc回收大量内存。这种现象称为内存抖动,是造成Android应用卡顿的原因之一。很重要的原因。写iOS应用的同学都说我没有管理内存,但是我写的应用却丝滑如丝。是的,iOS应用程序很少会出现内存抖动,并使用一种称为引用计数的方法。其实这也是可达性分析技术之一,在Objective-C中称为ARC。引用计数就是这样一种算法。每个对象都有一个计数器。当对象被创建或者其他对象引用这个对象时,计数器号也加1;当其他对象不再引用它时,计数器编号递减。1、当计数器的数字归0时,对象被回收。还是和之前一样的循环,大量的对象被创建,只要这个循环结束,刚刚创建的对象就可以被回收,不会造成内存抖动。引用计数加1这个动作很容易理解。这是用户编写的代码。用户的代码会明确说明什么时候创建对象,什么时候有新的引用;谁来将引用计数减1什么?这时候,编译器就派上用场了。编译器可以分析对象的生命周期,在合适的地方插入对象减1的代码,这样程序运行时引用计数就会增减。方舟编译器的宣传资料中提到“随用随用”,所以应该是使用了类似引用计数的技术来减少内存抖动。当然,由于Java语言本身的问题,引用计数并不能解决所有的问题。即使使用了引用计数,也需要gc来帮助回收内存。宣传资料中,“回收时无需暂停应用”,应该实现或改进ConcurrentGC,尽可能减少应用的暂停。通过引用计数和改进GC,优化内存回收,减少内存回收次数,减少停顿时间;既然有了统一的IR,能不能不受约束,能不能在上面做更多的优化?前面提到引用计数可以解决局部变量用完就回收的问题,但是全局变量就没法处理了。那么方舟编译器或许可以做一些这方面的文章,比如可以通过解析把一些全局变量变成局部变量;例如,它可以分析全局变量的生命周期,也可以对全局变量进行引用计数。简而言之,立即释放更多未使用的内存可以减少GC并减少卡顿。好了,废话说完,还是等方舟编译器开源了,再一探究竟吧。【本文为专栏作家“刘欣”原创稿件,转载请通过作者微信获取授权公众号coderising】点此查看该作者更多好文