问题描述
例如,访问一个 Point 对象的 x 属性的代码如下:
point.x在 V8 中,对应生成的机器码如下:
; ebx = the point object
cmp [ebx, <hidden class offset>], <cached hidden class>
jne <inline cache miss>
mov eax, [ebx, <cached x offset>]题主可能是想岔了,或者是没有正确理解读到的文章的意思。
题主给出的问题描述里的内容,显然是从V8的官方文档中引用来的:
For example, the JavaScript code to access property x from a Point object is:
point.x
In V8, the machine code generated for accessing x is:
# ebx = the point object
cmp [ebx,<hidden class offset>],<cached hidden class>
jne <inline cache miss>
mov eax,[ebx, <cached x offset>]If the object's hidden class does not match the cached hidden class, execution jumps to the V8 runtime system that handles inline cache misses and patches the inline cache code. If there is a match, which is the common case, the value of the x property is simply retrieved.
其实这里的代码只是一个为文档说明而做的示意,并不是说V8会在运行时生成这种格式的文本形式的“汇编”。这里只是用基本上是x86的Intel语法的汇编的形式的伪代码,来说明V8运行时生成的代码的形状。这里采用的语法形式是:
- <some constant>:尖括号所括住的内容表示这里实际生成的是一个直接量(immediate)
- 例如 jne <inline cache miss> 的意思就是一个条件跳转(条件为not equal),跳转目标为某个固定的地址,该目标地址是处理inline cache未命中的情况用的。
- [base, offset]:方括号所括住的内容表示这里实际是一个内存地址计算,其中逗号前的是基地址,逗号后的是偏移量
- 例如 mov eax, [ebx, <cached x offset>] 的意思就是一个内存到寄存器的传输指令,目标是寄存器 eax;源是一个内存地址,以 ebx 为基地址,以某个代表 x 字段所在偏移量的常量为偏移量。
实际上V8的JIT编译器是直接在内存中生成机器码的,并不会先生成文本形式的汇编然后再使用汇编器去转换为机器码。“动态生成机器码”听起来可能有点玄乎,其实根本没啥,就是往内存里写字节,这些字节正好是某些机器码的意思,然后把这块内存当作函数去调用就是了。由于代码自身就是动态生成的,在生成的代码里直接嵌入resolve好的各种值其实就相当于传统编译流程里的“动态链接”的效果。
顺手放俩我以前博客的传送门:
V8实际上自带一个用C++实现的“汇编器库”用来动态生成机器码。它并不把文本形式的汇编转换为机器码,而是提供一组C++ API,调用这个API的函数就可以在内存里生成机器码来。有兴趣的同学可能会知道,V8的MacroAssembler库源自Animorphic的Strongtalk VM,而Strongtalk VM也是HotSpot JVM的前辈。
例如说,使用这个汇编器库可以写出类似这样的C++代码:
MacroAssembler masm;
masm->pushq(rbp);
masm->movp(rbp, rsp);
masm->subp(rsp, Immediate(20));
masm->movl(rax, Immediate(42));
masm->movp(rsp, rbp);
masm->popq(rbp);
masm->ret(0);
这连串对MacroAssembler的调用就会在内存中生成这样的数据:
{ 0x55, 0x48, 0x89, 0xE5, 0x48, 0x83, 0xEC, 0x14, 0xB8, 0x2A, 0x00, 0x00, 0x00, 0x48, 0x89, 0xEC, 0x5D, 0xC3 }对这堆字节反汇编为文本形式来表示的话,就是:
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: 48 83 ec 14 sub rsp,0x14
8: b8 2a 00 00 00 mov eax,0x2a
d: 48 89 ec mov rsp,rbp
10: 5d pop rbp
11: c3 ret就这么简单而已。
在V8的实际代码中,常常会有诸如
#define __ masm.
这样的代码。于是上面的例子就可以写成:
MacroAssembler masm;
__ pushq(rbp);
__ movp(rbp, rsp);
__ subp(rsp, Immediate(20));
__ movl(rax, Immediate(42));
__ movp(rsp, rbp);
__ popq(rbp);
__ ret(0);
这C++代码要眯着眼睛看的话是不是就看起来挺像普通的汇编文本了?
引用一段实际的V8代码:
void EmitJumpIfNotSmi(Register reg,
Label* target,
Label::Distance near_jump = Label::kFar) {
__ testb(reg, Immediate(kSmiTagMask));
EmitJump(not_carry, target, near_jump); // Always taken before patched.
}
// jc will be patched with jz, jnc will become jnz.
void EmitJump(Condition cc, Label* target, Label::Distance near_jump) {
DCHECK(!patch_site_.is_bound() && !info_emitted_);
DCHECK(cc == carry || cc == not_carry);
__ bind(&patch_site_);
__ j(cc, target, near_jump);
}
=================================
那么实际的V8对Point的p.x这样的例子会生成怎样的代码呢?
这里让我用个稍微老一点版本的V8,v4.4.0来演示一下。
对于这样的代码:
function Point(x, y) { this.x = x; this.y = y }
function foo(p) { return p.x }
point = new Point(2, 3)
for (i = 0; i < 4000; i++) { foo(point) }
执行过后,在Mac OS X / x86-64上,V8 4.4.0的Crankshaft编译器对 foo() 中的 p.x 表达式所生成的机器码是这样的:
48 8b 45 10 a8 01 0f 84 27 00 00 00 49 ba d1 79 b1 cc 97 2c 00 00 4c 39 50 ff 0f 85 18 00 00 00 8b 40 1b 8b d8 48 c1 e3 20(这里我特意用这种一串十六进制表示的字节的形式来避免题主又误解成别的东西了)
把这串机器码反汇编成适合人阅读的文本形式的话,可以是这样(Intel语法):
2b476c41aa43 48 8b 45 10 mov rax,QWORD PTR [rbp+0x10]
2b476c41aa47 a8 01 test al,0x1
2b476c41aa49 0f 84 27 00 00 00 je 0x2b476c41aa76
2b476c41aa4f 49 ba d1 79 b1 cc 97 2c 00 00 movabs r10,0x2c97ccb179d1
2b476c41aa59 4c 39 50 ff cmp QWORD PTR [rax-0x1],r10
2b476c41aa5d 0f 85 18 00 00 00 jne 0x2b476c41aa7b
2b476c41aa63 8b 40 1b mov eax,DWORD PTR [rax+0x1b]
2b476c41aa66 8b d8 mov ebx,eax
2b476c41aa68 48 c1 e3 20 shl rbx,0x20我们其实可以用V8自带的“d8”命令行工具通过传入 --print-code-all 来让V8自己把动态生成的机器码以文本形式显示出来,输出的文本像下面这样:
0x2b476c41aa43 35 488b4510 REX.W movq rax,[rbp+0x10]
;;; <@16,#12> check-non-smi
0x2b476c41aa47 39 a801 test al,0x1
0x2b476c41aa49 41 0f8427000000 jz 86 (0x2b476c41aa76)
;;; <@18,#13> check-maps
0x2b476c41aa4f 47 49bad179b1cc972c0000 REX.W movq r10,0x2c97ccb179d1 ;; object: 0x2c97ccb179d1 <Map(elements=3)>
0x2b476c41aa59 57 4c3950ff REX.W cmpq [rax-0x1],r10
0x2b476c41aa5d 61 0f8518000000 jnz 91 (0x2b476c41aa7b)
;;; <@20,#14> load-named-field
0x2b476c41aa63 67 8b401b movl rax,[rax+0x1b]
;;; <@22,#18> smi-tag
0x2b476c41aa66 70 8bd8 movl rbx,rax
0x2b476c41aa68 72 48c1e320 REX.W shlq rbx, 32这段代码的意思,如果用类似C++的伪代码来表示的话,会是:
Map* const EXPECTED_MAP = (Map*)0x2c97ccb179d1;
const size_t OFFSETOF_X = EXPECTED_MAP->GetInObjectPropertyOffset(0)
+ (kPointerSize / 2) // load upper 32-bits of Smi directly
- kHeapObjectTag; // adjust for the tagged pointer
Object* foo(Context* ctx /* rsi */, JSFunction* func /* rdi */, Object* p /* stack param 0 */) {
// prologue
// ...
// check-non-smi
if (p->IsSmi())
deoptimize();
// check-maps
if (p->map() != EXPECTED_MAP)
deoptimize();
// load-named-field
int raw_value = LoadInt32FromOffset(p, OFFSETOF_X);
// smi-tag
Object* result = Smi::FromInt(raw_value);
// epilogue
// ...
return result;
}
这比起题主的问题描述以及本回答开头所引用的V8 Design Elements里所描述的代码其实又更进了一步。
V8 Design Elements文档里所描述的是最初期的V8的状态。当时的V8只有一个JIT编译器,一个JavaScript函数通常只会被JIT编译一次。这个JIT编译器做的优化也不是很多。
后来V8演化为拥有两个JIT编译器,一个初级编译器(baseline compiler,名字叫做Full Code Generator,简称FullCodeGen),和一个优化编译器(optimizing compiler,名字叫做Crankshaft),两个编译器结合在一次构成双层编译。JavaScript函数通常会先被FullCodeGen编译,然后如果还继续执行很多次的话则会再被Crankshaft重新编译一遍,生成更优化的代码。
在这个架构中,FullCodeGen里生成的代码还是跟V8 Design Elements的相似,会通过inline cache来实现property access;而这些inline cache不但用于实现fast property access,更重要的是它们会被用于收集profile,然后等到Crankshaft编译的时候,它就可以看先前收集的profile来做profile-guided optimization。
以这个 function foo(p) { return p.x } 为例,参数p没有任何特别的地方,所以JavaScript引擎也无法知道p到底可能有怎样的值。但通过FullCodeGen生成的代码所收集到的profile信息,Crankshaft再去编译 foo() 的时候就可以知道p之前通常指向一个Map(hidden class)为0x2c97ccb179d1的类型的对象。这个类型的constructor为Point、[[Prototype]] 为Point.prototype、对象里有足够空间容纳10个内嵌的字段(in-object property),并且其中2个slot被用于存储Smi类型,剩余的8个slot未被使用。
0x2c97ccb179d1: [Map]
- type: JS_OBJECT_TYPE
- instance size: 104
- inobject properties: 10
- elements kind: FAST_HOLEY_ELEMENTS
- pre-allocated property fields: 0
- unused property fields: 8
- back pointer: 0x2c97ccb17979 <Map(elements=3)>
- instance descriptors (own) #2: 0x1b84a4040c31 <FixedArray[8]>
- layout descriptor: 0
- prototype: 0x1b84a40409f9 <an Object with map 0x2c97ccb17921>
- constructor: 0x5c0c3d866e1 <JS Function Point (SharedFunctionInfo 0x5c0c3d864d1)>
- code cache: 0x5c0c3d95161 <CodeCache>
- dependent code: 0x5c0c3d98a89 <FixedArray[9]>
Descriptor array 2
0: Descriptor 0x5c0c3d7b009 <String[1]: x> @ 0 (data: s, field_index: 0, p: 1, attrs: [WEC])
1: Descriptor 0x5c0c3d76671 <String[1]: y> @ 0 (data: s, field_index: 1, p: 0, attrs: [WEC])这个时候Crankshaft就有机会生成更高效的代码形式,例如说访问属性可以通过“guarded inlined property access”来的方式来实现。这个例子只访问了一个 p.x 或许看不出它跟 inline cache 的区别(都是 cmp + jne + mov),但如果改为下面的代码例子:
function bar(p) { return p.x + p.y }
则如果用 inline cache 方式来实现的话逻辑会像这样:
LoadIC IC_001("x"); // cmp, jne, mov; fallback to Runtime_LoadIC_Miss
LoadIC IC_002("y"); // cmp, jne, mov; fallback to Runtime_LoadIC_Miss
BinaryOpIC IC_003("+"); // ...; Runtime_BinaryOpIC_Miss
Object* bar(Context* ctx /* rsi */, JSFunction* func /* rdi */, Object* p /* stack param 0 */) {
// prologue
// ...
// p.x
Object* left;
if (p->IsSmi()) {
left = heap->undefined_value();
} else {
// inline cache
left = IC_001.call(p);
}
// p.y
Object* right;
if (p->IsSmi()) {
right = heap->undefined_value();
} else {
// inline cache
right = IC_002.call(p);
}
// +
Object* result = IC_003.call(left, right);
// epilogue
// ...
return result;
}
这里每次调用LoadIC都会有自带的一个类型检查。而如果用guarded inlined property access 的话,那两个冗余类型检查就可以被优化为只做一次:
Object* bar(Context* ctx /* rsi */, JSFunction* func /* rdi */, Object* p /* stack param 0 */) {
// prologue
// ..
// Smi guard
if (p->IsSmi())
deoptimize();
// Map guard
if (p->map() != EXPECTED_MAP)
deoptimize();
// inlined p.x load
int32_t left = LoadInt32FromOffset(p, OFFSETOF_X);
// inlined p.y load
int32_t right = LoadInt32FromOffset(p, OFFSETOF_Y);
int32_t raw_result = left + right;
// smi-tag
Obejct* result = Smi::FromInt(raw_result);
// epilogue
// ...
return result;
}
这就舒爽多了。
而现在的V8的编译架构又有了新的变化,使用新的解释器 Ignition 和新的优化JIT编译器 TurboFan 搭配构成混合模式的解释+编译系统。这种配置下 Ignition 会用 inline cache,而 TurboFan 会用 guarded inline property access。