ART虚拟机中的InlineCache

熟悉JS语言的同学可能都听说过InlineCache,这是一种加速动态语言执行效率的重要手段。解释器在执行一条字节码时,需要获取一些隐含的信息作为该条字节码的输入,比如invoke指令需要知道方法的入口地址,或者需要知道对象某个字段的偏移地址。由于动态特性这些信息无法像C/C++那样在编译期就能确定,所以解释器的一个任务就是动态的解析出这些信息,这也是解释器性能无法达到机器码那么高效的一个原因。如果每次执行到同一条字节码时都去动态解析那些隐含的信息,那解释器花在动态解析上的时间可能是真正执行时间的数倍,这显然是不可接受的。 InlineCache的作用就是缓存这些动态解析的结果,在下次执行到同一条字节码时,就可以从缓存获取这些隐含信息。InlineCache另一个作用就是指导Jit将字节码编译为机器码,这点不是本文的重点,在此不再赘述。

art虚拟机里面也有个类似的机制,叫做InterpreterCache,这个对象位于线程本地缓存中:

class Thread {
 public:
 ...
  // Small thread-local cache to be used from the interpreter.
  // It is keyed by dex instruction pointer.
  // The value is opcode-depended (e.g. field offset).
  InterpreterCache interpreter_cache_;
 ...
 }

那具体的运行机制如何呢?先看个简单的例子:

public class InterpreterCache {

    public static void main(String[] args) {
        args[0].charAt(0);
    }
}

对应的字节码为:

000154: 1200            |0000: const/4 v0, #int 0 // #0
000156: 4601 0200       |0001: aget-object v1, v2, v0
00015a: 6e20 0300 0100  |0003: invoke-virtual {v1, v0}, Ljava/lang/String;.charAt:(I)C // method@0003
000160: 0e00            |0006: return-void

解释器在执行到String.charAt调用时,只知道方法的method_id为3,需要动态的解析出真正的ArtMethod对象,但在此之前,解释器会先查询InterpreterCache,如果命中,则返回值就是对应的ArtMethod指针。先看下 invoke-virtual对应的解释器代码:

%def invoke_virtual(helper="", range=""):
   EXPORT_PC
   // Fast-path which gets the method from thread-local cache.
%  fetch_from_thread_cache("%rdi", miss_label="2f") //这里便是从InterpreterCache中查询ArtMethod指针,如果查询成功,放在寄存器rdi中
1:
   // First argument is the 'this' pointer.
   movzwl 4(rPC), %r11d // arguments
   .if !$range
   andq $$0xf, %r11
   .endif
   movl (rFP, %r11, 4), %esi
   // Note: if esi is null, this will be handled by our SIGSEGV handler.
   movl MIRROR_OBJECT_CLASS_OFFSET(%esi), %edx
   UNPOISON_HEAP_REF edx
   movq MIRROR_CLASS_VTABLE_OFFSET_64(%edx, %edi, 8), %rdi
   jmp $helper
2:  //cache miss跳转处
   movq rSELF:THREAD_SELF_OFFSET, %rdi
   movq 0(%rsp), %rsi
   movq rPC, %rdx
   call nterp_get_method //动态解析ArtMethod
   movl %eax, %edi
   jmp 1b

查询过程简单来讲就是将当前指令的地址也就是dexPC指针作为索引,访问interpreter_cache_中定义的cache数组,因此具有O(1)的时间复杂度。interpreter_cache_被定义成4096字节固定大小的线程局部变量,因此每个线程只有256条缓存条目,这应该是一个内存占用和性能的一个折衷。

在这里插入图片描述

fetch_from_thread_cache具体的处理流程如下:

%def fetch_from_thread_cache(dest_reg, miss_label):
   movq rSELF:THREAD_SELF_OFFSET, %rax
   movq rPC, %rdx    //pc指针作为key值,因此可以保证key的唯一性
   salq MACRO_LITERAL(THREAD_INTERPRETER_CACHE_SIZE_SHIFT), %rdx
   andq MACRO_LITERAL(THREAD_INTERPRETER_CACHE_SIZE_MASK), %rdx //对key值做简单的hash计算,得到数组index
   cmpq THREAD_INTERPRETER_CACHE_OFFSET(%rax, %rdx, 1), rPC //对比缓存条目中存储的key值和dexPC,如果相等,则缓存命中
   jne ${miss_label} //不相等,则跳转到慢速路径
   movq __SIZEOF_POINTER__+THREAD_INTERPRETER_CACHE_OFFSET(%rax, %rdx, 1), ${dest_reg}

到这里大家可能有个疑问,缓存是什么时候填充的呢?答案也很简单,就是在动态解析出结果后更新的缓存:

extern "C" size_t NterpGetMethod(Thread* self, ArtMethod* caller, const uint16_t* dex_pc_ptr)
    REQUIRES_SHARED(Locks::mutator_lock_) {
  UpdateHotness(caller);
  const Instruction* inst = Instruction::At(dex_pc_ptr);
  ...
  uint16_t method_index =
      (opcode >= Instruction::INVOKE_VIRTUAL_RANGE) ? inst->VRegB_3rc() : inst->VRegB_35c(); //从指令里面获取method_index
   ArtMethod* resolved_method = caller->SkipAccessChecks()
      ? class_linker->ResolveMethod<ClassLinker::ResolveMode::kNoChecks>(
            self, method_index, caller, invoke_type)
      : class_linker->ResolveMethod<ClassLinker::ResolveMode::kCheckICCEAndIAE>(
            self, method_index, caller, invoke_type); //调用class_linker查找对应的ArtMethod
  ...
  } else if (invoke_type == kVirtual) {
    UpdateCache(self, dex_pc_ptr, resolved_method->GetMethodIndex()); //更新缓存
    return resolved_method->GetMethodIndex();
  } else {
  ...
 }

如果你看到了这里,觉得文章写得不错就给个赞呗?
更多Android进阶指南 可以扫码 解锁更多Android进阶资料

在这里插入图片描述
敲代码不易,关注一下吧。ღ( ´・ᴗ・` )

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值