混合编译_运行

How ART works

ART uses ahead-of-time (AOT) compilation, and starting in Android 7.0 (Nougat or N), it uses a hybrid ==combination of AOT, just-in-time (JIT) compilation, and profile-guided compilation==. The combination of all these compilation modes is ==configurable== and will be discussed in this section. As an example, Pixel devices are configured with the following compilation flow:

  1. An application is initially installed without any AOT compilation. The first few times the application runs, it will be ==interpreted, and methods frequently executed will be JIT compiled==.
  2. When the device is idle and charging, ==a compilation daemon runs to AOT-compile frequently used code based on a profile generated during the first runs==.
  3. The next restart of an application will use the ==profile-guided code and avoid doing JIT compilation at runtime for methods already compiled. Methods that get JIT-compiled during the new runs will be added to the profile, which will then be picked up by the compilation daemon==.

JIT和AOT共存

  • 应用在安装的时候dex不会再被编译
  • App运行时,dex文件先通过解析器被直接执行,热点函数会被识别并被JIT编译后存储在 jit code cache 中并生成profile文件以记录热点函数的信息。以供AOT编译时生成机器码
  • 手机进入 IDLE(空闲) 或者 Charging(充电) 状态的时候,系统会扫描 App 目录下的 profile 文件并执行 AOT 过程进行编译。

ART comprises a compiler (the dex2oat tool) and a runtime (libart.so) that is loaded for starting the Zygote. The dex2oat tool takes an APK file and generates one or more compilation artifact files that the runtime loads. The number of files, their extensions, and names are subject to change across releases, but as of the Android O release, the files being generated are:

  • .vdex: contains the uncompressed DEX code of the APK, with some additional metadata to speed up verification.
  • .odex: contains AOT compiled code for methods in the APK.
  • .art (optional): contains ART internal representations of some strings and classes listed in the APK, used to speed application startup.

Compilation options

Compilation options for ART are of two categories:

  • System ROM configuration: what code gets AOT-compiled when building a system image.
  • Runtime configuration: how ART compiles and runs ==applications== on a device.

One core ART option to configure these two categories is compiler filters. Compiler filters drive how ART compiles DEX code and is an option passed to the dex2oat tool. Starting in Android O, there are four officially supported filters:

  • verify: only run DEX code verification.
  • quicken: run DEX code verification and optimize some DEX instructions to get better interpreter performance.
  • speed: run DEX code verification and AOT-compile all methods.
  • speed-profile: run DEX code verification and AOT-compile methods listed in a profile file.

Runtime configuration

Jit options

Package manager options

Dex2oat options

Implementing ART Just-In-Time (JIT) Compiler

https://source.android.com/devices/tech/dalvik/jit-compiler

Android runtime (ART) includes a just-in-time (JIT) compiler with code profiling that continually improves the performance of Android applications as they run. ==The JIT compiler complements ART’s current ahead-of-time (AOT) compiler and improves runtime performance, saves storage space, and speeds application and system updates.== It also improves upon the AOT compiler by avoiding system slowdown during automatic application updates or recompilation of applications during over-the-air (OTA) updates.

Although JIT and AOT use the same compiler with a similar set of optimizations, the generated code might not be identical. ==JIT makes use of runtime type information, can do better inlining, and makes on stack replacement (OSR) compilation possible==, all of which generates slightly different code.

JIT architecture

image

Figure 1. JIT architecture.

JIT compilation

JIT compilation involves the following activities:

image

Figure 2. Profile-guided compilation.

  1. The user runs the app, which then triggers ART to load the .dex file.
  • If the .oat file (the AOT binary for the .dex file) is available, ART uses it directly. Although .oat files are generated regularly, they don’t always contain compiled code (AOT binary).
  • If the .oat file does not contain compiled code, ART runs through JIT and the interpreter to execute the .dex file.
  1. JIT is enabled for any application that is not compiled according to the speed compilation filter (which says “compile as much as you can from the app”).
  2. The JIT profile data is dumped to a file in a system directory that only the application can access.
  3. The AOT compilation (dex2oat) daemon parses that file to drive its compilation.

JIT workflow

image


华为公布的方舟编译器到底对安卓软件生态会有多大影响?

Android 平台的绝大多数应用是使用 Java 语言写的,CPU 只能理解汇编指令,无法直接识别 Java语言的虚拟机指令;为了让 CPU 能运行 Java语言编写的程序,一般有两种办法:

  1. 「计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决」引入一个中间层,这个中间层负责 Java代码的执行,然后这个中间层本身编译为 ==CPU 能理解的汇编指令==,也就是 CPU -> 中间层 -> Java 代码。如果这个中间层采用 Java 语言直接作为输入,理解一句 Java 语句就把Java语言翻译一下让CPU 执行一段,我们一般称这种模式为「解释执行」。毋庸置疑这种方式效率是相当低效的。

  2. 直接把 Java 语言翻译成==CPU能理解的机器语言==。这里又有两种方式: 在程序运行之前直接把 Java 代码编译为机器语言。这种模式我们称之为 AOT (Ahead of time)编译。 在程序运行起来之后,实时地把 Java 语言编译为机器语言然后执行。这种模式称之为 JIT(Just in time) 编译。

背景介绍完了回到 Android 平台上面,Android 平台分为几个阶段:

  1. 在 Android 5.0 正式采用 ART 之前,Android 采用的是 解释执行 + 辣鸡 JIT 的方式执行 Java代码。在这个阶段是货真价实的「边解释边执行」的模式,代码效率相当低下,再加上那时候同样辣鸡的 GC (垃圾回收),Android 用起来真是惨不忍睹。
  2. Android 5.0 ~ Android 6.0 。Google 推出了 ART (Android Runtime)来解决之前的 Java 代码执行效率问题。这个阶段采用的是完全 AOT 模式;Android 应用在安装的时候,系统会把所有Java代码提前编译为机器码。这种模式有两个缺点不能忍:
  • 安装速度巨慢。即使是现在吊炸天的 855 采用 AOT 模式编译一下安装包比较大的应用(如支付宝)可能就要一分钟。那个时候的 CPU 可不如现在,安装一个应用都让你等得头皮发麻。更要命的时候,系统 OTA 开机会对所有的应用执行 AOT 操作,这时候你的开机速度可能要半个小时。。。
  • 占用磁盘空间,Java 代码编译为机器码之后体积会急剧膨胀。
  1. Android 7.0 ~ 现在。Google做了很大的改进,基于这样一个事实:我们使用一个应用的时候,基本每个人只使用它一小部分功能,为什么要把所有代码全编译呢?只编译你经常用的那部分代码不就 OK 了,这样安装的时候啥也不干速度飞快,等你用的时候系统就能知道哪部分代码经常被执行,把这部分代码编译为机器码,运行起来速度也快。于是 Google 又引入了 JIT,这时候的执行模式是 AOT + JIT + 解释执行。
  • 应用安装的时候不执行 AOT 编译,安装速度飞快。初次使用应用的时候没有机器码,因此只能解释执行。

  • 应用运行起来之后,系统收集经常被运行的代码的信息,做两件事:1)在必要的时候在运行时直接把 Java 代码编译为机器码 (JIT),然后使用机器码执行提高运行效率。2)把这个「经常被运行的代码信息保存起来」

  • 设备空闲的时候,系统拿出应用运行时候保存的「热点代码信息」直接把这些代码编译为机器码 (AOT)

  • 关于 Android 7.0 系统的演进可以参阅这里:http://s3.amazonaws.com/connect.linaro.org/las16/Presentations/Tuesday/LAS16-201%20-%20ART%20JIT%20in%20Android%20N.pdf

8.0上改进了解释器,解释模式执行效率大幅提升;

Android 9.0上提供了预先放置热点代码的方式,应用在安装的时候就能知道常用代码会被提前编译。可以看到,当前 Android 平台的执行模式在空间占用+安装速度+运行速度上已经达到了一个很好的平衡。

  1. 回到华为的这个方舟编译器上面,现在的 Android 是边解释边执行的吗?可以说是,也可以说不是。上面我已经提到了,现在的 Android 是 解释执行 + 还算可以的JIT + AOT 的模式。并且,你也可以手动把应用的代码全部提前编译达到完全 AOT 的效果(很多答案里面提到的 AOT 就是说的这种);不过这属于开倒车,Google 肯定不会这么做。这样做效果有多大呢?这个我有发言权。之前在支付宝做性能优化的时候,我干过这么一回事:让应用在后台运行的时候请求系统直接采用 everything 模式编译支付宝,本地测试启动速度有爆炸性提升(30%~50%);但是灰度测试的时候效果不明显,为什么呢?其一是后台全编译运行成功率低,其二是系统的 JIT + 后台 AOT 不是吃素的;考虑到耗电/占空间的问题压根没上线。所以如果华为只是简单地用这种方式去避免所谓的「边解释边执行」那就相当滴 low,但是按照 GPU Turbo这种黑科技来看,我觉得不太可能是这个。
  2. 除了 Android 系统的这种 AOT 之外,难道没有别的办法了吗?我不负责任地猜测一下,方舟编译器是不是在Android 应用打包成APK的时候,直接把 Java 代码编译为了机器码?注意这个跟Android系统的那个 AOT 是不样的,系统是在应用安装或者系统空闲的时候做编译;这种方式你下载到的安装包就是编译好的了,不需要系统动手。如果是第一种,辣鸡华为。如果是第二种,吊炸天!!!当然还有别的可能,不管咋样,静待开源 :)

9102年了,还不知道android为什么卡?

Android 源码分析(十) Dalvik 虚拟机创建过程

虚拟机 理解Android虚拟机体系结构

image

4.2 Dalvik类加载器   一个dex文件需要类加载器加载原生类和Java类,然后通过解释器根据指令集对Dalvik字节码进行解释和执行。Dalvik类加载器使用mmap函数,将dex文件映射到内存中,通过普通的内存读取操作即可访问dex文件,然后解析dex文件内容并加载其中的类到哈希表中。

4.2.1 解析dex   总的来说,dex文件可以抽象为三个部分:头部、索引、数据。通过头部可以知道索引的位置和数目,以及数据区的起始位置。将dex文件映射到内存后,Dalvik会调用dexFileParse函数对其进行分析,分析的结果放到DexFile数据结构中。DexFile中的baseAddr指向映射区的起始位置,pClassDefs指向class索引的起始位置。为了加快class的查找速度,还创建一个哈希表,对class名字进行哈希并生成索引。

4.2.2 加载class   解析工作完成后就进行class的加载,加载的类需要用ClassObject数据结构来存储。

typedef struct Object {
    ClassObject* clazz;  // 类型对象
    Lock lock;           // 锁对象
} Object;

其中clazz指向ClassObject对象,还包含一个Lock对象。如果其它线程想要获取它的锁,只有等这个线程释放。Dalvik每加载一个class都会对应一个ClassObject对象,加载过程会在内存中分配几个区域,分别存放directMethod, virtualMethod, sfield, ifield。这些信息从dex文件的数据区中读取。字段Field的定义如下:

struct Field {
    ClassObject* clazz;    //所属类型
    const char* name;      // 变量名称
    const char* signature; // 如“Landroid/os/Debug;”
    u4 accessFlags;        // 访问标记
    
    #ifdef PROFILE_FIELD_ACCESS
        u4 gets;
        u4 puts;
    #endif
};

待得到class索引后,实际的加载由loadClassFromDex来完成。首先它会读取class的具体数据,分别加载directMethod, virtualMethod, ifield和sfield,然后为ClassObject数据结构分配内存,并读取dex文件的相关信息。加载完成后,将加载的class通过dvmAddClassToHash函数放入哈希表,以方便下次查找;最后,通过dvmLinkClass查找该类的超类,如果有接口类则加载相应的接口类。

4.3 Dalvik解释器   对于任何虚拟机来说,解释器无疑是核心的部分,所有的Java字节码都经过解释器解释执行。由于Dalvik解释器的效率很重要,Android分别实现了C语言版和各种汇编语言版的解释器。解释器通常是循环执行,需要一个入口函数调用处理程序执行第一条指令,而后每条指令执行时引出下一条指令,通过函数指针调用处理程序。

5 Android的启动

启动电源,加载引导程序到RAM
BootLoader引导
Linux Kernel启动
Init进程创建
Init fork出Zygote进程,Zygote进程创建虚拟机;创建系统服务
Android Home Launcher启动

image

华为新贵!方舟编译器的荣光和使命

第一个命门

Java的“虚拟机”

前面提到,Java为了能够实现跨平台操作,便借助虚拟机来调度硬件平台资源。在虚拟机里,还需要集成翻译器或者编译器,来将Java的字节码(即中间代码)解释成机器听得懂的机器语言,或者直接编译成机器直接执行的010101的机器码。

2008年,Android 1.0刚发布的时候,使用的是一个叫Dalvik的虚拟机,里面集成了一个解释器,每次用户在安卓手机上运行APP时,就会叫醒这个解释器,来给安卓的硬件解释APP想要干嘛。这就相当于新闻发布会,发言人讲一句自己的母语,然后再由专业翻译将其翻译成外国记者听得懂的语言,效率非常低下,一个小时可能也问不了几个问题。

谷歌意识到这个问题严重拖了安卓手机的后腿,所以通过一年多的努力,在2010年中发布了2.2版本,引入了JIT(Just in Time,即时编译)机制。JIT比较聪明,当用户在安卓手机运行APP时,会同时将用户经常使用的功能编译为机器能直接执行的010101机器码,不用每一句每一句的去翻译。当出现不常用的功能时,再把解释器叫起来翻译。

JIT虽然变聪明了一点,但是每次启动APP都要先编译一次,不能一劳永逸。加上Dalvik虚拟机性能比较落后,所以谷歌在2014年10月推出了Android 5.0版本,将虚拟机从Dalvik替代成ART(Android Run Time),同时把JIT的编译器替代成AOT (Ahead of Time)。意思就是说,APP在下载后安装到手机上时同时把能编译的代码先编译成机器听得懂的101010。剩下不太好翻译的代码,就在用户使用时再叫醒解释器来翻译。AOT相比JIT的好处,就是不用每次打开APP都需要先编译一遍。但是,坏处就是用户安装APP的时间有点长。

越来越多的用户吐槽为什么安装一个APP也慢吞吞。于是,谷歌在2017年Android 7.0又做了一点改进,安装时先不编译中间代码,而是在用户空闲时将能够编译成机器码的那部分代码,通过AOT编译器先静态编译了。如果AOT还没来得及编译或者不能编译,再叫醒JIT+解释器两个难兄难弟来顶住。这种机制,相当于用时间换空间,既缩短了用户安装APP的等待时间,又将虚拟机里编译器和解释器能做的优化提升到最大效率了。

很多人以为华为方舟编译器就是Android 7.0的ART虚拟机,其实不然。

image

image

无论是编译器还是解释器,只是在虚拟机上打补丁。手机上的虚拟机+编译器+解释器本身不仅占用硬件资源,还无法最大发挥软件运行性能。正因如此,所以绝大部分手机厂商只能无奈的通过简单粗暴提升安卓手机的内存和存储空间,来弥补虚拟机的弊端。

这就是安卓的第一个命门,虚拟机先天不足。

参考

https://source.android.com/devices/tech/dalvik/

https://source.android.com/devices/tech/dalvik/configure

Using Profile-Guided Optimization (PGO)