Chromium Code
v8的jit优化引擎叫做Turbofan,Turbofan使用了节点之海(Sea of Nodes)的概念,通俗来讲就是将代码中的各种变量对象、控制流、效果转化为node以及edge进行处理:
随便找到一处会处理node对象的代码对其下断,并用查看其结构:
这其中关于mark_与type_字段具体用途暂且未知,不过这两个字段从命名上来看主要起到一个标记与类型标识的作用且这两个字段并不常用暂且搁置。
node对象由 Node::New函数创建,该函数会去调用Node::NewImpl函数
NewImpl函数会先创建几个空对象。
随后根据input_count依次判断input是否都不为空,如果有空的input就报错:
然后会去判断input_count是否大于kMaxInlineCapacity,在目前最新v8版本下kMaxInlineCapacity实际值为0xE。
此处之所以会如此比较主要与node的两种内存布局有关,源码中的注释如下,当node布局为inline模式时Use在Node之前的低地址,Input指针在Node之后的高地址,两者相互对应,通过Use可以找到input,通过input同样也可以找到Use,Use以node-1(指针加减运算,并非真的是地址-1)的位置开始从右至左通过计算依次获取地址将该地址作为相应的input的use,以node+1为起始,将input对象地址从左往右排布存放在通过计算得到的相应位置,也就是说node右边第一个input对应node左边第一个use。当input个数大于最大内联容量(kMaxInlineCapacity)时就会采用OOL(out-of-line)的布局方式去布局node,OOL模式下的use、node、input为了防止触发Scavenge GC启动导致造成额外的开销,会将它们三个放在不连续的内存区域中,也就是说当input>kMaxInlineCapacity时就会采取OOL布局,由于大多数情况下一个节点有超过14个input的情况并不多见,所以此处只对inline布局进行说明。
//============================================================================
//== Memory layout ===========================================================
//============================================================================
// Saving space for big graphs is important. We use a memory layout trick to
// be able to map {Node} objects to {Use} objects and vice-versa in a
// space-efficient manner.
//
// {Use} links are laid out in memory directly before a {Node}, followed by
// direct pointers to input {Nodes}.
//
// inline case:
// |Use #N |Use #N-1|...|Use #1 |Use #0 |Node xxxx |I#0|I#1|...|I#N-1|I#N|
// ^ ^ ^
// + Use + Node + Input
//
// Since every {Use} instance records its {input_index}, pointer arithmetic
// can compute the {Node}.
//
// out-of-line case:
// |Node xxxx |
// ^ + outline ------------------+
// +----------------------------------------+
// | |
// v | node
// |Use #N |Use #N-1|...|Use #1 |Use #0 |OOL xxxxx |I#0|I#1|...|I#N-1|I#N|
// ^ ^
// + Use + Input
//
// Out-of-line storage of input lists is needed if appending an input to
// a node exceeds the maximum inline capacity.
当inputr<=kMaxInlineCapacity时,就会进入以下分支采取Inline模式,此模式下use、node、input内存地址连续。
然后会开始初始化input与use
当第一次执行循环时current为0,执行完inpu_ptr[current] = to
后node布局,红色部分就是放入的to也就是input:
当执行完Use* use = use_Ptr - 1 -current
后use等于node - 0x18 - current,0x18是Use类对象的大小
最后会去执行to->AppendUse(use)
,AppendUse函数会将first_use地址放入use->next位置,最后再将use放入first_use_字段位置,此处操作实际上就是更新了use链表的头,use对象实际上是一个链表,通过Use::next与Use::prev两个指针连接。
当通过Use来查找input时,会先通过input_index函数来获取相应input的下标,随后通过当前use+1+index的方式来找到node此node地址就是inputs数组的起始地址,最后再通过inputs和index来查找对应的input。
input_index函数会通过InputIndexField::decode函数来解析bit_field_字段来获取input_index
前面提到过use对象,此处对该对象再详细说明一下,use主要用来获取input。节点{n}的每个input{i}都有一个关联的{Use},这两者可以相互获取对方。当创建某个节点时并不会为节点自身初始化use,只会将node地址-1-index处的地址作为其input的use,但是当在处理之后的node时之前处理过的node有可能会成为当前正在处理的node的input,此时再为之前处理过的node初始化use,之所以会将node - 1 -index的地址作为use,主要目的是为了标记此节点分别是哪些节点的input之前提到过use与布局在节点后面的input是对应可以互相查找的,同时利用这种机制也可以找到当前节点作为input时它的node都有哪些。
以某个input_count为6的node为例:
此时input_ptr为node+sizeof(node)
也就是说input指针会从node+0x20处开始保存
随后会通过use_ptr - 1 - index来作为use对象的地址,要注意的是在c++中指针-1并非真的是指针地址-1,而是指针地址减去一个指针类型大小,例如此处use_ptr与node为同一个地址,但是经过强制类型转换变为了use类型,use对象大小为0x18所以此处use_ptr - 1时实际上是减去了0x18,对于index也是一样,当计算第一个input的use时实际上是use_ptr - 1 - 0。
随后将use保存到input->first_use中
计算第二个input的use时就是use_ptr - 1 - 1,然后继续通过以上方法再把use保存到input->first_use中,其余的use也是类似的计算方法。
也就是说当通过input查找use时直接获取input->first_use字段即可,而通过use获取input时就需要前面提到过的input_ptr函数。
Use对象除了能用input_ptr来获取input对象外还能通过from函数来获取node对象:
use类里还定义了InlineField与InputIndexField两个别名,这两个别名主要用来获取inline标记与Input_Index:
之后当在处理后续节点的时候前面处理过的节点就会变成input,再通过同样的办法进行初始化first_use,而很多时候一个node有可能会是多个其他node的input,而node自身有可能会有多个input,在获取自己的input时直接获取即可,在获取依赖于自己的node时,就需要通过use来获取如此一来当前节点上下的节点就都可以获取到了。
通过图来说明,当在处理节点7时,它会将节点5、12、0当作input,并分别为5、12、0设置use这是为了方便它们三个在处理时可以找到节点7(这并不意味着这三个节点就只有节点7这一个use),之后当在处理节点27时他就会为节点7初始化use,节点42也会为7初始化一个新的use,节点30也同样,节点27为7初始化的use保存在节点7的node->first_use
,节点42创建的use保存在节点7的node->first_use->next
,节点30创建的use保存在节点7的node->first_use->next->next
。
相应的既然一个节点可能会有多个use那肯定会有数量计算寒素
此部分比较烧脑建议结合IR图、代码、调试去尝试理解。
edge类是一种用于与单个use_node作为另一node的输入相关联的信息的封装。
通常在使用此类的时候往往会通过input_edge或是use_edge函数来迭代获取edge对象,类似于以下形式:
edge类中有两个比较重要的函数:to和from,这两个函数分别用于获取边(edge)两端的节点。
通过input_edges与use_edges函数获取的edge to与from函数得到的节点不一样,通过node.input_edge函数获取的edge对象,from函数获取的是node自身。
而通过node.use_edge函数获取的edge对象,to函数获取的是node自身。
之所以会出现不一样的情况主要是由于它们两者表示的边并不相同,举例来讲,以下图为例(此图为了能更明确的表达所以只有两条边但实际情况往往都有多条边),其中绿色部分的边需要通过input_edges函数来获取,红色部分的边需要用use_edges函数来获取,也就是说input_edges与use_edges分别表示节点输入与输出两部分的边。
通过以上结论可知对于end节点来讲应该是只有input_edges而没有use_edges的,通过调试器来进行验证:
end节点往往只有一个input,那就是return节点,所以可以确认是正确的,而start节点正好相反只有use_edges而没有input_edges:
同时也能通过代码来验证,Input_edges在返回迭代器时会先判断是outline还是inline,随后将相应的参数传进去初始化InputEdges对象并返回
在InputEdges对象的构造函数定义中它会将传入的input指针最为input_root,node-1作为use_root,在这种情况下node还是node,node的input还是它的input:
而use_edges在返回迭代器时,会直接将node传入UseEdges的构造函数并且依然用它来初始化UseEdges::node_成员
在UseEdges::begin函数实现中他会用node_成员作为参数传入迭代器构造函数中,迭代器构造函数会将node->first_use作为current也就是说他是的时会将传入的node作为use去查找依赖于它的node。