标签:map 对象 隐藏 机器码 DebugPrint V8 内联 浅析 属性
最近看到一篇文章,详细讲述了浏览器是如何工作的,感觉非常好,所以决定一点点摘录及研究下。
接上篇:浅析浏览器是如何工作的(二):一等公民、闭包惰性解析与预解析器、V8存储对象的快属性与慢属性、栈空间与堆空间、继承(隐藏属性__proto__)、构造函数怎样创建对象
一、机器码、字节码
1、V8 为什么要引入字节码
早期的 V8 为了提升代码的执行速度,直接将 JavaScript 源代码编译成了没有优化的二进制机器代码,如果某一段二进制代码执行频率过高,那么 V8 会将其标记为热点代码,热点代码会被优化编译器优化,优化后的机器代码执行效率更高。
随着移动设备的普及,V8 团队逐渐发现将 JavaScript 源码直接编译成二进制代码存在两个致命的问题:
(1)时间问题:编译时间过久,影响代码启动速度;
(2)空间问题:缓存编译后的二进制代码占用更多的内存。
这两个问题无疑会阻碍 V8 在移动设备上的普及,于是 V8 团队大规模重构代码,引入了中间的字节码。
字节码的优势有如下三点:
(1)解决启动问题:生成字节码的时间很短;
(2)解决空间问题:字节码虽然占用的空间比原始的 JavaScript 多,但是相较于机器代码,字节码还是小了太多,缓存字节码会大大降低内存的使用。
(3)代码架构清晰:采用字节码,可以简化程序的复杂度,使得 V8 移植到不同的 CPU 架构平台更加容易。
Bytecode 某种程度上就是汇编语言,只是它没有对应特定的 CPU,或者说它对应的是虚拟的 CPU。这样的话,生成 Bytecode 时简单很多,无需为不同的 CPU 生产不同的代码。要知道,V8 支持 9 种不同的 CPU,引入一个中间层 Bytecode,可以简化 V8 的编译流程,提高可扩展性。
如果我们在不同硬件上去生成 Bytecode,会发现生成代码的指令是一样的。
2、如何查看字节码
// test.js function add(x, y) { var z = x + y; return z; } console.log(add(1, 2));
运行./d8 ./test.js --print-bytecode
:
[generated bytecode for function: add (0x01000824fe59 <SharedFunctionInfo add>)] Parameter count 3 #三个参数,包括了显式地传入的 x 和 y,还有一个隐式地传入的 this Register count 1 Frame size 8 0x10008250026 @ 0 : 25 02 Ldar a1 #将a1寄存器中的值加载到累加器中,LoaD Accumulator from Register 0x10008250028 @ 2 : 34 03 00 Add a0, [0] 0x1000825002b @ 5 : 26 fb Star r0 #Store Accumulator to Register,把累加器中的值保存到r0寄存器中 0x1000825002d @ 7 : aa Return #结束当前函数的执行,并将控制权传回给调用方 Constant pool (size = 0) Handler Table (size = 0) Source Position Table (size = 0) 3
3、常用字节码指令:
- Ldar:表示将寄存器中的值加载到累加器中,你可以把它理解为 LoaD Accumulator from Register,就是把某个寄存器中的值,加载到累加器中。
- Star:表示 Store Accumulator Register, 你可以把它理解为 Store Accumulator to Register,就是把累加器中的值保存到某个寄存器中
-
Add:
Add a0, [0]
是从 a0 寄存器加载值并将其与累加器中的值相加,然后将结果再次放入累加器。add a0 后面的[0]称之为 feedback vector slot,又叫反馈向量槽,它是一个数组,解释器将解释执行过程中的一些数据类型的分析信息都保存在这个反馈向量槽中了,目的是为了给 TurboFan 优化编译器提供优化信息,很多字节码都会为反馈向量槽提供运行时信息。
- LdaSmi:将小整数(Smi)加载到累加器寄存器中
- Return:结束当前函数的执行,并将控制权传回给调用方。返回的值是累加器中的值。
二、隐藏类和内联缓存
JavaScript 是一门动态语言,其执行效率要低于静态语言,V8 为了提升 JavaScript 的执行速度,借鉴了很多静态语言的特性,比如实现了 JIT 机制,为了提升对象的属性访问速度而引入了隐藏类,为了加速运算而引入了内联缓存。
1、为什么静态语言的效率更高?
静态语言中,如 C++ 在声明一个对象之前需要定义该对象的结构,代码在执行之前需要先被编译,编译的时候,每个对象的形状都是固定的,也就是说,在代码的执行过程中是无法被改变的。可以直接通过偏移量查询来查询对象的属性值,这也就是静态语言的执行效率高的一个原因。
JavaScript 在运行时,对象的属性是可以被修改的,所以当 V8 使用了一个对象时,比如使用了 obj.x 的时候,它并不知道该对象中是否有 x,也不知道 x 相对于对象的偏移量是多少,也就是说 V8 并不知道该对象的具体的形状。那么,当在 JavaScript 中要查询对象 obj 中的 x 属性时,V8 会按照具体的规则一步一步来查询,这个过程非常的慢且耗时。
2、将静态的特性引入到 V8
(1)V8 采用的一个思路就是将 JavaScript 中的对象静态化,也就是 V8 在运行 JavaScript 的过程中,会假设 JavaScript 中的对象是静态的。
具体地讲,V8 对每个对象做如下两点假设:
(1)对象创建好了之后就不会添加新的属性;
(2)对象创建好了之后也不会删除属性。
符合这两个假设之后,V8 就可以对 JavaScript 中的对象做深度优化了。V8 会为每个对象创建一个隐藏类,对象的隐藏类中记录了该对象一些基础的布局信息,包括以下两点:
(1)对象中所包含的所有的属性;
(2)每个属性相对于对象的偏移量。
有了隐藏类之后,那么当 V8 访问某个对象中的某个属性时,就会先去隐藏类中查找该属性相对于它的对象的偏移量,有了偏移量和属性类型,V8 就可以直接去内存中取出对应的属性值,而不需要经历一系列的查找过程,那么这就大大提升了 V8 查找对象的效率。
(2)在 V8 中,把隐藏类又称为 map,每个对象都有一个 map 属性,其值指向内存中的隐藏类;
map 描述了对象的内存布局,比如对象都包括了哪些属性,这些数据对应于对象的偏移量是多少。
3、通过 d8 查看隐藏类
// test.js let point1 = { x: 100, y: 200 }; let point2 = { x: 200, y: 300 }; let point3 = { x: 100 }; %DebugPrint(point1); %DebugPrint(point2); %DebugPrint(point3);
./d8 --allow-natives-syntax ./test.js
# =============== DebugPrint: 0x1ea3080c5bc5: [JS_OBJECT_TYPE] # V8 为 point1 对象创建的隐藏类 - map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x1ea3080406e9 <FixedArray[0]> { #x: 100 (const data field 0) #y: 200 (const data field 1) } 0x1ea308284ce9: [Map] - type: JS_OBJECT_TYPE - instance size: 20 - inobject properties: 2 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x1ea308284cc1 <Map(HOLEY_ELEMENTS)> - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1> - instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]> - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)> - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0 # =============== DebugPrint: 0x1ea3080c5c1d: [JS_OBJECT_TYPE] # V8 为 point2 对象创建的隐藏类 - map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x1ea3080406e9 <FixedArray[0]> { #x: 200 (const data field 0) #y: 300 (const data field 1) } 0x1ea308284ce9: [Map] - type: JS_OBJECT_TYPE - instance size: 20 - inobject properties: 2 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x1ea308284cc1 <Map(HOLEY_ELEMENTS)> - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1> - instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]> - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)> - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0 # =============== DebugPrint: 0x1ea3080c5c31: [JS_OBJECT_TYPE] # V8 为 point3 对象创建的隐藏类 - map: 0x1ea308284d39 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x1ea3080406e9 <FixedArray[0]> { #x: 100 (const data field 0) } 0x1ea308284d39: [Map] - type: JS_OBJECT_TYPE - instance size: 16 - inobject properties: 1 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x1ea308284d11 <Map(HOLEY_ELEMENTS)> - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1> - instance descriptors (own) #1: 0x1ea3080c5c41 <DescriptorArray[1]> - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)> - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0
4、多个对象共用一个隐藏类
(1)在 V8 中,每个对象都有一个 map 属性,该属性值指向该对象的隐藏类。不过如果两个对象的形状是相同的,V8 就会为其复用同一个隐藏类,这样有两个好处:
第一:减少隐藏类的创建次数,也间接加速了代码的执行速度;
第二:减少了隐藏类的存储空间。
(2)那么,什么情况下两个对象的形状是相同的,要满足以下两点:
第一:需要有相同的属性名称;
第二:需要有相等的属性个数。
5、重新构建隐藏类
给一个对象添加新的属性,删除新的属性,或者改变某个属性的数据类型都会改变这个对象的形状,那么势必也就会触发 V8 为改变形状后的对象重建新的隐藏类。
// test.js let point = {}; %DebugPrint(point); point.x = 100; %DebugPrint(point); point.y = 200; %DebugPrint(point);
# ./d8 --allow-natives-syntax ./test.js DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE] - map: 0x32c7082802d9 <Map(HOLEY_ELEMENTS)> [FastProperties] ... DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE] - map: 0x32c708284cc1 <Map(HOLEY_ELEMENTS)> [FastProperties] ... DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE] - map: 0x32c708284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties] ...
每次给对象添加了一个新属性之后,该对象的隐藏类的地址都会改变,这也就意味着隐藏类也随着改变了;如果删除对象的某个属性,那么对象的形状也就随着发生了改变,这时 V8 也会重建该对象的隐藏类;
最佳实践:
(1)使用字面量初始化对象时,要保证属性的顺序是一致的;
(2)尽量使用字面量一次性初始化完整对象属性;
(3)尽量避免使用 delete 方法。
6、通过内联缓存来提升函数执行效率
虽然隐藏类能够加速查找对象的速度,但是在 V8 查找对象属性值的过程中,依然有查找对象的隐藏类和根据隐藏类来查找对象属性值的过程。如果一个函数中利用了对象的属性,并且这个函数会被多次执行:
function loadX(obj) { return obj.x; } var obj = { x: 1, y: 3 }; var obj1 = { x: 3, y: 6 }; var obj2 = { x: 3, y: 6, z: 8 }; for (var i = 0; i < 100; i++) { // 对比时间差异 console.time(`---${i}----`) loadX(obj); console.timeEnd(`---${i}----`) loadX(obj1); // 产生多态 loadX(obj2); }
作者:独钓寒江雪
原文链接:https://segmentfault.com/a/1190000037435824
标签:map,对象,隐藏,机器码,DebugPrint,V8,内联,浅析,属性 来源: https://www.cnblogs.com/goloving/p/14440875.html
本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享; 2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关; 3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关; 4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除; 5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。