X

曜彤.手记

随记,关于互联网技术、产品与创业

吉 ICP 备10004938号

《Advanced Design and Implementation of VMs》读书笔记(一)


也是很久之前无意间搜罗到的一本关于 VM 的好书,趁此疫情 WFH 期间时间充足,赶紧读起来。PS. 果然国人写的英语书籍就是比外国人写的好读很多,主要是因为 —— 废话少。

Chapter 1:虚拟机简介

  1. (Page:4)四种常见的 VM 类型:
  1. (Page:4)针对 ABI 和实际 ISA 的虚拟机通常又被称为“仿真器(Emulator)”。而基于 V-ISA 和具体语言的虚拟机有时也被称为“运行时引擎”。
  2. (Page:5)安全型语言的特征:
  1. (Page:6)存在着通过硬件实现的 V-ISA 虚拟机。
  2. (Page:6)可以通过以静态单分配(SSA)表示形式构建的控制流图(CFG)来进行更高级的 IR 优化(如 Type Specialization、Inline Function、DCE 等)。

Chapter 2:走入虚拟机内部

  1. (Page:9)VM 的核心组成主要包括:
  1. (Page:14)虚拟机模型:

  1. (Page:15)V-ISA(Wasm、Java 字节码)一般仅作为其他高级语言的编译目标。JavaScript 既可以作为一种源语言,又可以被作为其他语言的编译目标(如 Emscripten)。
  2. (Page:15)源语言、虚拟机与字节码之间的关系:字节码(OpCode 的二进制形式)作为一种编译目标与源语言(比如 Java)没有直接关系;虚拟机类比于 i386 的 CPU 来执行 X86 指令;而任何类型的源语言都有可能被编译成某种特定的字节码目标。

  1. (Page:16)一般来说,安全性语言(JavaScript、Python、Java 等)由于缺乏对资源的细粒度掌控能力而不便于(运行成本较高)用来开发 VM。
  2. (Page:18)Java 平台(如 “Java SE 8”)是 Java 语言、JVM 虚拟机、Java 类库和相关工具的规范集合。Java 实现(如 “OpenJDK 8”)是 Java 平台的完整实现。Java 平台具有不同的版本,分别称为标准版 SE,企业版 EE 等。它们都共享相同的 Java 语言和 JVM 规范,但定义了不同的库,并且可能具有不同的实现
  3. (Page:19)CLI 平台和 Java 平台在各类概念上的横向对比:

Chapter 3:虚拟机内部数据结构

  1. (Page:21)JVM 语言具有两种数据类型:基本类型(保存一个直接值)和引用类型(包含指向对象的指针)。其中类 Object 是所有类的基类(所有类都是 Object 的派生类。Object 含有一个 getClass() 方法可以获得一个 java.lang.Class 对象,其中含有关于该类的一些信息),类 Class 是所有其他类的类型(所有类都是 Class 的一个实例),即所有其他类都是一种 Class 类型。其中的某些 Instance-Of 和 Subclass-Of 的关系是由 VM 来维护的。

  1. (Page:22)通常来说,一个类对象的数据包含两部分:个体实例数据+类数据;由于 Java 支持继承和多态,因此对类多态方法的调用以及类成员的访问便成了两个最为频繁的操作。虚表属于类的而非对象,其内部含有指向虚方法(virtual)的指针。同一个类的所有对象都使用同一个虚表,因此每个对象都含有一个指向该类虚表的指针。综上,下图(b)中的资源管理方式相较于(a)会有着更好的性能(使得类对象多态方法调用和类成员访问所需要的步骤更少)。

  1. (Page:23)JVM 中函数类型的大致描述结构:
typedef struct Method {
  char *name;                 // method name.
  char *descriptor;           // method descriptor.  
  Class *owner_class;         // class that owns this method.     
  unsigned char *byte_code;   // byte code sequence.    
  Handler *handlers;          // exception handlers.    
  LineNum *linenums;          // line number table.    
  LocalVar *localvars;        // local variables.    
  Exception *exceptions;      // exceptions that may throw.    
  uint16 modifier;            // method access modifier.    
  uint16 max_stack;           // max stack depth.    
  uint16 max_locals;          // max number of local vars.    
  uint16 vtable_offset;       // offset in vtable.    
  JIT_STATUS state;           // JIT compilation status.    
  unsigned char *jitted_code; // compiled code.
  struct {        
    unsigned is_init        : 1;        
    unsigned is_clinit      : 1;        
    unsigned is_finalize    : 1;        
    unsigned is_overridden  : 1;        
    unsigned is_nop         : 1;    
  } flags; // properties of the methods.
} Method;

上述结构中包含了方法需要在进行编译调试Profile链接GC 以及异常处理时所需要的所有运行时信息。其中 jitted_code 字段包含了该函数对应的 AOT 代码。is_nop 字段表示函数体是否为空,以供虚拟机进行优化处理。

Chapter 4:执行引擎的设计

  1. (Page:27)GC 一般会同时囊括堆内存的分配(即在代码中需要通过 GC 来分配内存)和清理过程,比如 C++ 中常用的“贝姆”(Boehm)垃圾收集器。
  2. (Page:28)在解释器模式下的常用优化方法:
  1. (Page:30)基于函数的 JIT:流程与 ELF 共享库的动态链接过程类似,函数索引表可以类比为 PLT。当第一次调用函数时,索引表中对应项(函数指针或其他方式)会先调用内部编译器来编译函数,并生成对应的的机器码;然后,待编译完成后,用生成机器码的入口地址作为索引表项的内容,并将该地址返回给调用者,这样后续再次调用该函数时将直接使用函数的机器码(Code Cache)。

该方式的问题在于:JIT 粒度较为粗糙,热代码的命中率较低(比如函数体内耗时的循环结构无法捕捉)。

  1. (Page:31)当多个线程要调用同一方法并触发该方法的 JIT 编译时,VM 需要确保相互排斥对同一方法的编译(比如使用单例模式的“双检查锁”,或“自旋锁”)。
  2. (Page:32)基于 Trace 的 JIT:Trace 是指在运行时所执行的一段热代码的路径,而该种 JIT 方式只编译位于该路径上的代码。其几个重要步骤如下:

一个例子(Python):

def square(x):
    return x * x

i = 0
y = 0
while True:
    y += square(i)
    if y > 100000:
        break
    i = i + 1

对应的 Trace(伪代码,方法 square 可以被内联):

loopstart(i1, y1)
  i2 = int_mul(i1, i1)  # x * x;
  y2 = int_add(y1, i2)  # y += i * i;
  b1 = int_gt(y2, 100000)
  guard_false(b1)
  i3 = int_add(i1, 1)        # i = i + 1;
  jump(i3, y2)

* 由于普遍性能较差且高性能 JIT 的实现复杂度,截止 2015 年,大部分知名 VM 实现都已经不再使用基于 Trace 的 JIT 方案。

  1. (Page:35)基于“循环”和“基本块”的 Tracing-JIT 均无法很好地处理递归调用(直接/间接)结构;
  2. (Page:36)Region-Based JIT:以 “tracelet” 为单位进行的、“Outlining”(将函数实现的一部分进行分离,分离的部分将被编译成新的函数,并代替原代码的位置进行调用)方式的 JIT。
  3. (Page:39)JIT 和 AOT 的区别:

Chapter 5:GC 的设计

  1. (Page:45)静态生存期分析(Liveness Analysis)的问题:
  1. (Page:46)引用计数(Reference Counting)的常用原语:

RC 相关原语的 OpCode 需要 Compiler 在编译源代码到字节码的过程中插入到程序的特定位置。其中的 “updSlot” 主要用于支持赋值语句中对象引用的变化情况。

  1. (Page:48)当 RC 的计数将要溢出时,VM 可以选择放弃后续的计数过程,并假设该引用对应的资源将永远不会被释放。常用的 RC 变量大小可以选择为1 字节。当然也可以选择为 1 位,即所有对象仅能拥有在创建时生成的一个引用,超过该引用次数,则资源永远不会被释放。
  2. (Page:48)在多线程环境中,可以选择使用原子性的 RC 更新;或者选择在多个线程更新同一个变量的 RC 时,将资源永久化。做法需要 RC 保存其创建时对应的线程 ID,然在线程更新 RC 时判断该线程 ID 与自己保存的创建线程 ID 是否相同,若相同则更新 RC,否则直接溢出,使资源永久化。
  3. (Page:48)RC 的一个问题在于“循环引用”的存在。并且插入到目标文件中的 GC 原语也会使文件的体积增大,这对于一些存储空间有限的场景来说可能并不友好。
  4. (Page:49)对象跟踪(Object Tracing):标记-清除、标记-整理、标记-拷贝。


这是文章底线,下面是评论
  暂无评论,欢迎勾搭 :)