在上一篇文章中,我们分析了node的EventEmitter
与EventLoop的关系,结论就是它们真的没多大关系,代码表现出异步只是emit
函数调用的时机被我们手动设定在eventloop里面。
OK,跟着独操引擎的男人继续我们的代码之旅~
JS对象与C++ Runtime 代码
这一部分涉及到V8引擎的代码优化,对这个不感兴趣的伙伴可以略过这一部分,直接看后面的loop分析,个人觉得理解了function
在引擎的表示有助于理解eventloop的真实面目。
之前的v8系列文章一经介绍过Handle
跟Persistent
这两个模板类,分析了V8的内存管理以及GC的回收算法,JS的源代码经过v8 Full Complier 的前端编译生成AST,然后代码生成开始做一些代码优化,JS的对象模型我会在另一篇文章进行更详细的分析,这里主要介绍内联缓存(Inline Cache
简称 IC),为什么会有IC呢,其实是一种迭代优化算法,假设一个function
编译后位于内存区的一个Stub X
,相对于一次具体的调用过程P,编译器认为源代码并不需要这么复杂, 所以为它生成了一段优化代码的Stub X1
,这个X1是个简单粗暴的形式,编译器并不能确保永远都执行这个Stub,因为执行器到现在只运行了一次invocation
,所以它居然大胆地断言,AST中所有挂载这个function
的结点都用这个Stub X1替代(mapping
),这下好办了,C++ Runtime已经不用担心有自己的事了,因为编译器完全可以分配一个寄存器Rx
,将X1在段中的偏移地址读到Rx
,然后CPU每次调用直接高速读取Rx的地址,直接跳转过去就完事了,生成的代码大概是这样(假设Stub在R[ebx] + n), Rx = eax x86机器架构):
mov %eax -0x0n(%ebx) ;load the address of stub, cache the code for all invocation
call <entry of the stub> ;invocation
...
entry:
tst <if this stub works>
jne <missed>
...
missed:
<new stub X2 generated by C++ runtime>
所以一旦发生了另外一次调用P1
,编译器发现这个stub无法满足优化它的条件,这时候就是cache miss
了,C++ runtime代码负责善后,针对旧的Stub再次协调新的的调用需求,生成另外一个优化代码Stub X2
,这下X2可能还是简单粗暴的版本,但相对于P
,P1
两次调用,好像也没什么问题,但是让代码变得简单,可能只是简单的从寄存器读取或者偶尔的一两次加减操作,不涉及什么循环移位,消耗更少的CPU时间,好的,现在又可以将这个X2缓存起来,让所有的调用直接跳到X2的入口地址,如果不行再退回runtime代码处理新的优化,如此迭代,就可以生成一个有穷状态机,对于一个Pi
,我都可以进入一个优化状态Si
,比如下面一个switch语句的描述:
switch(P) {
case Pi:
//turn to Si
__stub_pi_();
break;
case Pj:
//turn to Sj
__stub_pj_();
break;
...
default:
//turn back to initialized state
//notify runtime code to generate a new stub
__runtime_to_generate_new_stub();
}
回想上一篇文章中的emit函数的实现,是不是觉得也有个跟上面很像的switch语句结构,没错那其实也相当于模拟了引擎层的IC,为个别的调用情况生成一些特例化的版本,跟一个回退的非优化版本,其实这种结构的代码在node的源代码中出现的还是比较多的。
一段神奇的代码
为了说明eventloop的各个阶段,我特意写了一段代码:
const timeOut = 10;
let horribleLoop = () => {
//entry nextTick event invoked by Module.runMain() (within JS code stack), so the node::MakeCallback is never called util node will be exited
process.nextTick(() => {
logger.log('entry next tick task executed');
});
//check event
setImmediate(() => {
logger.log('end this phase of event loop');
//but the AsyncWrap::MakeCallback invoked nextTick callback block this turn of event loop
for (let i = 0; i < 5; i++) {
process.nextTick(() => {
logger.log('current stack phase id ==> ' + (++recursiveCounter));
});
}
}, timeOut);
//timer event
setTimeout(() => {
logger.log('timer task executed');
}, timeOut);
//poll event
fs.readFile(path.resolve('../cherry'), (err, data) => {
if(err) {
logger.log(err.message);