初探 inline cache
前言
好记性不如烂笔头,为了能了解一下v8里inline cache的具体实现,特此记录一下调试的过程
环境
Ubuntu20.04 LTS
v8 version 9.8.177.9 x64
测试代码:
1 | function foo(a){ |
第1次调用
CompileLazy
code指向CompileLazy:
1 | DebugPrint: 0x3226082935dd: [Function] in OldSpace |
CompileLazy :
1 | // src/builtins/builtins-lazy-gen.cc |
runtime complielazy
会执行runtime的complielazy:
1 | // src/runtime/runtime-compiler.cc |
Compiler::Compile
编译任务主要在Compiler::Compile里完成:
1 | // src/codegen/compiler.cc |
IterativelyExecuteAndFinalizeUnoptimizedCompilationJobs 函数会调用 ExecuteSingleUnoptimizedCompilationJob和FinalizeSingleUnoptimizedCompilationJob来生成jsFunction的code,这两个函数最后会执行到 InterpreterCompilationJob::Status InterpreterCompilationJob::ExecuteJobImpl和InterpreterCompilationJob::Status InterpreterCompilationJob::FinalizeJobImpl,其实现都在src/interpreter/interpreter.cc里
FinalizeSingleUnoptimizedCompilationJob里调用了InstallUnoptimizedCode来给JSFunction安装feedback_metadata,code和BytecodeArray:
1 | // src/codegen/compiler.cc |
在这个函数调用完成后,JSFunction的code变成了Code BUILTIN InterpreterEntryTrampoline
这里再调试的时候还可以发现,feedback_metadata带了每个slot的kind,测试代码及安装好的feedback_metadata:
1 | // 测试使用的js代码 |
我猜测是再生成字节码的时候,就是调用visitxxxx函数(src/interpreter/bytecode-generator.cc)的时候填充的(如feedback_spec()->AddLoadICSlot();)
initialize feedbackCell
Cpmpiler::Compile编译好function的code之后,会初始化JSFunction对象的FeedbackCell:
1 | // static |
在JSFunction::InitializeFeedbackCell中一般会走到EnsureClosureFeedbackCellArray:
1 | // static |
注意这里的budget是bytecodeLength的长度乘以8,这也就是为什么第九次才安装feedback_vector,这个后面会解释
InterpreterEntryTrampoline
测试平台是x64,所以这里应该是src/builtins/x64/builtins-x64.cc下的 Generate_InterpreterEntryTrampoline函数:
1 | void Builtins::Generate_InterpreterEntryTrampoline(MacroAssembler* masm) { |
当成汇编看就好,其中Label do_dispatch里的__ call(kJavaScriptCallCodeStartRegister);
就是跳到第一个字节码的处理函数开始执行JSFuntion了
LdaNamedProperty
示例中的第一个字节码是LdaNamedProperty:
1 | // src/interpreter/interpreter-generator.cc |
LdaNamedProperty的字节码处理函数下load feedbackvector,在load receiver,name,context和slot,最后把处理流程全部转给LoadIC_BytecodeHandler进行处理:
1 | void AccessorAssembler::LoadIC_BytecodeHandler(const LazyLoadICParameters* p, |
该函数会先检查JSFunction是否有feedback vector,没有的话就调用LoadIC_NoFeedback来进行处理,我们是第一次调用,所以没有feedback vector,所以直接到Builtin::kLoadIC_NoFeedback :
1 | void AccessorAssembler::LoadIC_NoFeedback(const LoadICParameters* p, |
LoadIC_NoFeedback 主要是通过GenericPropertyLoad函数来进行属性的读取
Return
return字节码通常来说就是JSFunction的最后一个字节码,该字节码会更新JSFunction里的一些值:
1 | // Return the value in the accumulator. |
主要内容都在UpdateInterruptBudgetOnReturn里:
1 | // src/interpreter/interpreter-assembler.cc |
UpdateInterruptBudgetOnReturn 函数会计算出一个profiling_weight,这个profiling_weight好像正常情况下等于bytecodeArray的长度,本例子中就是5;
接着在UpdateInterruptBudget更新feedbackCell里的InterruptBudget,如果new_budget大于0,就更新InterruptBudget,如果小于0,那么就会调用:
1 | BIND(&interrupt_check); |
runtime BytecodeBudgetInterruptWithStackCheckFromBytecode函数只是前面做了一些check,最终和runtime BytecodeBudgetInterruptFromBytecode一样都会调用到BytecodeBudgetInterruptFromBytecode 函数:
1 | // src/runtime/runtime-internal.cc |
该函数在[1]处给JSFunction安装FeedbackVector,也就是说InterruptBudget要小于0才安装FeedbackVector,前面说过初始化的时候InterruptBudget的值为BytecodeArray的长度乘以8,这也就是说正常情况下就是在第九次调用的时候会给JSFunction安装FeedbackVector
第10次调用
LoadIC_BytecodeHandler
在第9次调用的时候return字节码给JSFunction安装了FeedbackVector,那么第10次的LoadIC_BytecodeHandler就会走rutime 的LoadIC_Miss:
1 | void AccessorAssembler::LoadIC_BytecodeHandler(const LazyLoadICParameters* p, |
Runtime_LoadIC_Miss
主要还是因为用于优化属性读取的handler还没有安装,所以流程会走到miss标签处,runtime LoadIC_Miss函数如下:
1 | // src/ic/ic.cc |
ic.UpdateState
先判断kind,然后调用ic.UpdateState:
1 | void IC::UpdateState(Handle<Object> lookup_start_object, Handle<Object> name) { |
state的定义:
1 | // State for inline cache call sites. Aliased as IC::State. |
LoadIC::Load
更新完state之后,调用LoadIC::Load:
1 | MaybeHandle<Object> LoadIC::Load(Handle<Object> object, Handle<Name> name, |
最关键的再于[2]处的UpdateCaches:
1 | void LoadIC::UpdateCaches(LookupIterator* lookup) { |
ComputeHandler
handler由ComputeHandler计算得出,ComputeHandler函数如下:
1 | Handle<Object> LoadIC::ComputeHandler(LookupIterator* lookup) { |
例子中走到的是LookupIterator::DATA分支,使用LoadHandler::LoadField创建了一个smi_handler:
1 | Handle<Smi> LoadHandler::LoadField(Isolate* isolate, FieldIndex field_index) { |
这个handler里应该记录了该属性的偏移,后续靠这个handler里记录的信息应该可以直接通过偏移取到想要查找的属性(当然前提是map要正确),
计算完并得到handler之后,LoadIC::UpdateCaches函数会调用SetCache函数来设置对该属性的cache:
1 | void IC::SetCache(Handle<Name> name, const MaybeObjectHandle& handler) { |
我们这是第一次,所以走的是UNINITIALIZED分支,调用的是UpdateMonomorphicIC:
1 | void IC::UpdateMonomorphicIC(const MaybeObjectHandle& handler, |
这么一通下来,config()->SetFeedbackPair更新了JSFunction的FeedbackVector:
1 | pwndbg> job 0x3a11082936b5 |
slot #0里的[0] 是对象的map,[1]是smihandler(就是前面计算出来的handler)
更新好FeedbackVector后,LoadIC::Load指令会调用Object::GetProperty来读取属性的值
第11次调用
还是回到LoadIC_BytecodeHandler,这次我们传入的对象的map还是没变,所以TryMonomorphicCase会在FeedbackVector中找到handler,那么流程就会走到:
1 | BIND(&if_handler); |
HandleLoadICHandlerCase函数:
1 | // src/ic/accessor-assembler.cc |
例子中创建的就是smi_handler,所以流程会走到if_smi_handler处,调用HandleLoadICSmiHandlerCase,由于我们的accessmode是LoadAccessMode::kLoad(这个是默认值),该函数会调用HandleLoadICSmiHandlerLoadNamedCase来处理属性的读取:
AccessorAssembler::HandleLoadICSmiHandlerLoadNamedCase
1 | void AccessorAssembler::HandleLoadICSmiHandlerLoadNamedCase( |
该函数就是定义了各种handler_kind和相对应的处理标签,对于本例子,我们会走到field标签,field标签里调用HandleLoadField来处理,该函数会根据smi_handler里存储的内容(前面encode在里面的各个标志位)来直接进行对属性的load,这里的直接就是说直接根据偏移来:
1 | void AccessorAssembler::HandleLoadField(TNode<JSObject> holder, |
自此就拿到了属性,函数返回
MONOMORPHIC && POLYMORPHIC && MEGAMORPHIC
MONOMORPHIC 代表着inline cache只收到了一种map,POLYMORPHIC 是收到了多个map,如果遇到的map过多,inline cache的状态就会变成MEGAMORPHIC,这里可以参考inline cache state的定义:
1 | // State for inline cache call sites. Aliased as IC::State. |
修改下测试代码:
1 | function foo(a){ |
在创建好handler后,我们给foo函数传入一个与c对象有不同map的对象d
回到AccessorAssembler::LoadIC_BytecodeHandler函数,因为我们传入的对象d的map和handler里缓存的map不一致,所以流程会走到runtime的LoadIC_Miss:
1 | BIND(&miss); |
在前面第10次调用一节中讲过了Runtime_LoadIC_Miss,由于这次我们的state是MONOMORPHIC,所以ic.UpdateState函数会调用ShouldRecomputeHandler来查看inline cache中的handler对应的map是否还合法,不合法就把state设置为RECOMPUTE_HANDLER
接着流程又来到LoadIC:Load,先算出handler,在调用IC::SetCache来更新feedback vector,IC::SetCache:
1 | void IC::SetCache(Handle<Name> name, const MaybeObjectHandle& handler) { |
UpdatePolymorphicIC函数:
1 | bool IC::UpdatePolymorphicIC(Handle<Name> name, |
先计算已有的map和handler,算出number_of_valid_maps,valid的map和handler以pair的形式存放在maps_and_handlers数组中,最后调用ConfigureVectorState来更新feedback_vector
可以注意到的是该函数中有一个判断:
1 | if (number_of_valid_maps >= FLAG_max_valid_polymorphic_map_count) |
在该版本中的FLAG_max_valid_polymorphic_map_count为4,当number_of_valid_maps大于等于4的时候,该函数返回false,那么在IC::SetCache的流程会走到UpdateMegaDOMIC,但是会由于FLAG_enable_mega_dom_ic的原因,直接返回false,那么流程进入:
1 | if (!is_keyed() || state() == RECOMPUTE_HANDLER) { |
IC::CopyICToMegamorphicCache
1 | void IC::CopyICToMegamorphicCache(Handle<Name> name) { |
提取出前面的map和handler对,然后调用UpdateMegamorphicCache进行cache的更新:
1 | void IC::UpdateMegamorphicCache(Handle<Map> map, Handle<Name> name, |
该函数结束后,接着调用ConfigureVectorState(MEGAMORPHIC, name);
更新feedback vector
在调用UpdateMegamorphicCache(lookup_start_object_map(), name, handler);
把map和handler加入到cache里
测试代码3次%DebugPrint(foo)的feedback vector,对应3中ic的状态:
1 | // MONOMORPHIC |
POLYMORPHIC
POLYMORPHIC状态下的LoadIC_BytecodeHandler,流程会走到:
1 | BIND(&try_polymorphic); |
HandlePolymorphicCase函数的逻辑大概就是循环取得feedback vector数组里保存的map和handler,和当前传入的receiver的map进行比较,如果比较成功了,就直接用handler进行处理,如果都没找到,那又回到Runtime_LoadIC_Miss
MEGAMORPHIC
MEGAMORPHIC状态下的LoadIC_BytecodeHandler,流程会走到:
1 | BIND(&stub_call); |
先调用Runtime的LoadIC_Noninlined:
1 | // src/ic/accessor-assembler.cc |
正常情况下都是走到try_megamorphic标签,调用TryProbeStubCache:
1 | // src/ic/accessor-assembler.cc |
在stub_cache中寻找是否已经有盖name的cache了,如果有,那就比较下map是否也相等,如果相等的话,那就用cache里该name的handler,如果不想等或者cache中没有该name的话就又回到runtime的LoadIC_Miss
这个stub_cache是在isolate实例上的,可以通过p &((v8::internal::Isolate*)isolate)->load_stub_cache_
找到,这说明MEGAMORPHIC 状态下的handler其实是共享的,只要name和map相等,那就能共享handler(不知道有没有特殊情况,通常情况下是共享的)
那么super属性的获取又是如何的呢
super property
js class的继承,那个this的指向和其它语言(c++/java)有点不同,很神奇;
测试例子:
1 | class B { |
查看classB的m函数的字节码:
1 | [generated bytecode for function: m (0x1139082934c1 <SharedFunctionInfo m>)] |
可以看到用的是LdaNamedPropertyFromSuper字节码,其处理程序为:
1 | // src/interpreter/interpreter-generator.cc |
流程交由Builtin::kLoadSuperIC处理:
1 | void AccessorAssembler::LoadSuperIC(const LoadICParameters* p) { |
和 LoadIC_BytecodeHandler几乎一模一样,不同的就是lookup_start_object,从IGNITION_HANDLER(LdaNamedPropertyFromSuper, InterpreterAssembler)
和AccessorAssembler::GenerateLoadSuperIC
可以看出,lookup_start_object来自home_object_prototype,而home_object_prototype = LoadMapPrototype(LoadMap(home_object));
,这个home_object指向的应该是B,home_object_prototype 就是a
剩下的流程好像都差不多
后记
大概就是这么粗浅的调试和看了下源码,对inline cache有一点认识