还有一个是从AST直接生成机器码,但是这个现在已经被换成了Ignition(解释器)+TurboFan(类型优化编译器)的架构了。
先说隐藏类:对一个JS对象的属性访问而言,最简单的解释器实现会把属性建模为运行时的hash<string, object>查询。然而这个性能太慢,怎么优化呢?简单的说就是参考静态编译器的思路,把属性field的按名字访问,抹掉名字信息,变成按offset访问。——不过这样就需要一个class的描述符数据结构。
The implementation of an IC is called a stub. Stubs behave like functions in the sense that you call them, and they return, but they don't necessarily set up a stack frame and follow the full calling convention. Stubs are usually generated on the fly, but for common cases, they can be cached and reused by multiple ICs. The stub which implements an IC typically contains optimized code which handles the types of operands that particular IC has encountered in the past (which is why it's called a cache). If the stub encounters a case it's not prepared to handle, it "misses" and calls the C++ runtime code. The runtime handles the case, then generates a new stub which can handle the missed case (as well as previous cases). The call to the old stub in the full compiled code is rewritten to call the new stub, and execution resumes as if the stub had been called normally.
JS是一门动态类型的语言,但要让JS运行时变快,就要尽量在运行时作为静态类型的语言来处理。——当然,这个方面做到极致就是要考虑CPU片内缓存的编译器后端优化。。。
OK,接下来说Inline Cache。IC其实就是把运行时动态的类型switch-case直接特化生成单独的Stub函数版本。比如,举个例子来说,我们要实现一个图像的矩形区域copyRect算法。这个图像的像素格式可以是RGBA,也可能是A8灰度类型。一般人可能实现一个copy就是在一个循环里if-else处理。但是这会导致CPU片内缓存频繁失效,导致很差的性能。所以正确的做法就是为不同的像素格式实现2个版本的copyRect函数,然后根据输入图像的像素类型直接跳转/调用不同的copyRect函数。
像以前的GDI+、或者Skia这些2D图形引擎,其raster模块的BitBlit,都是这么做的。
举JS里面的代码实例来说,譬如属性访问:a.x。假如没有IC的话,那么标准实现只能是一个解释器:——需要判断x这个属性到底哪里来的,是“Own”属性呢?还是基类Prototype上的,亦或者说是不存在的。这么做(运行时if-else / switch-case)很浪费性能,所以用IC来实现,就是判断a对象的class类型,(通过class描述符),这就能够知道属性x究竟是什么类型,然后生成对应的Stub函数(此Stub函数就是运行时特定于,比如说,x是Own属性的情况),直接调用执行。
当然,由于是把动态语言的if-else解释器执行的本质,通过IC,换成了“预先判断出对象的当时当地的类型 + 生成特定于这种类型的Stub函数 + 执行特化的JIT Stub函数”,等于是切换到了静态执行的性质。当然前提是运行时必须作检查,以保证此类型是没有问题的。
于是,IC在我看来,就是一种适合于JIT编译器的优化思想,而不是某种编程技巧。——当然,说到技巧,也就是那个特定于不同CPU ABI的OSR了,或者称不同Stack Frame之间的跳转(解释器栈 <--> JIT生成的Native栈)。