我的处理器是
x86-64
,操作系统是Mac OSX
:
这篇文章,会通过一个非常简单的例子,来讲解一下JIT
的意思。
首先,假设我们有如下PHP
代码:
1 |
|
代码很简单,就是调用函数foo
,然后打印结果。执行后,结果是2
。
现在,我们来写一个简单解释器,省去解析PHP
代码的部分,直接来生成opcode
:
1 |
|
编译后,运行结果如下:
1 | gcc jit.c |
其中,生成opcode
的过程是一次性的。真正耗时间的是执行opcode
的部分。参考PHP
内核的实现我们会发现,每个opcode
都会对应一个handler
,例如这里的+
和-
。而这些handler
实际上是C
函数,也就意味着,每执行一条opcode
,我们就会调用一次C
函数。换句话来说,对于我们的PHP
脚本的那个foo
函数来说,执行这两条语句至少需要2
次C
层面的函数调用(实际上可能不止2
次,我没有去打印真正的opcode
)。对于我们写的这份C
代码,查看a.out
的汇编代码,我们会发现,除去调用main
和printf
,可以看到还有2
次call
指令的调用。
那么,如果我们对这个foo
函数进行jit
的话,会怎么样呢?我们来看看代码:
1 |
|
编译后执行结果如下:
1 | gcc jit.c |
查看汇编代码我们会发现,汇编代码里面并没有foo
函数,但是,因为解释器在运行脚本的过程中 ,我们已经把PHP脚本的foo函数编译成了对应的二进制代码,并且放在内存里面了。这种感觉就像是我们在用C
代码。
所以,实际上,JIT是在对我们的opcode产生的指令进行精简,减少CPU指令的条数,从而达到速度的提升(我们一定要纠正一个误区就是“没有JIT前不是跑二进制,JIT后才是跑二进制”。实际上,无论JIT不JIT,都是跑二进制,只不过跑的CPU指令会有一些变化,也就是指令被优化了)。然而,JIT要完成的工作并不仅仅是我们这个例子那么简单,所以对应的优化也并不仅仅是省去一些函数调用。
那么,从这个例子,我们也可以看出,我们在JIT的时候,是需要消耗一部分时间去把opcode转化为二进制代码。如果后期执行这些精简后的二进制代码节约的时间,远远大于把opcode翻译成二进制代码的时间,那么收益是很明显的。但是,如果后续没怎么跑我们翻译好的二进制代码,那么,像PHP这种FPM模型,一个请求就JIT一次的话,性能反而会下降(然而,这种问题PHP官方肯定也想得到,就好比opcache缓存opcode一样)。除了这种函数调用之类的优化还有其他水很深的问题,不是想JIT就JIT,例如灵剑大佬指出的方方面面,这些问题我们后面会慢慢的去探索,所以,我们会看到,JIT是可以指定JIT哪一部分的。
像我们这个例子,我们是以函数为单位来进行JIT
的,那如果我们函数的逻辑复杂一点的话,我们理论上甚至可以对某些路径进行JIT
。
后续,我们会使用LLVM
来完成yaphp
的JIT
工作。