熟悉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的method_index。先看下
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 对应的method_index,如果查询成功,放在寄存器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 //根据method_index查找vtable获取真正的ArtMethod,保存在寄存器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 {
...
}