动态对象访问在 V8 中是如何工作的?

How does dynamic object access work in V8?

我惊讶地发现 JavaScript 对象 are not in fact hash maps under the hood, instead they are more similar to structs。据我了解,获取和设置对象的属性很快,因为值的内存位置位于固定偏移量处,就像在结构或 class 中一样。我不明白的是语法如何映射到那个固定的偏移量。即当编译器看到 obj.aobj[‘a’] 时会发生什么。该语法是否在 运行 时间或编译时间或 JIT 转换为整数偏移量?我想我想了解的是如何在不执行类似 index = hash(‘a’) % objectLength.

的情况下有效地将传入的字符串“a”转换为整数索引

也许我的知识差距是我没有完全了解结构在编译器级别的工作方式。

我认为查看一个对象在内存中的样子的过于简化的示例会有所帮助:

 { a: 1, b: 2 }
 // represented as
 address | 0  | 1  | 2  |  3   | 4  | 5  |
 value   | 3  | 1  | 2  |  a   | b  |    |

对象值存放在地址0,第一个值3指向“隐藏class”。要检索对象在 0 处的键“a”的值,可以读取 0,然后读取存储在 0 中的地址处的值,然后向上计数并查找该地址的值,直到找到“a”。幸运的是它是第一个键,所以我们可以回到对象,将偏移量 1 添加到我们的地址 0,并找到 0 + 1 处的值。或者在伪代码 (C++) 中:

 void* obj = 0;
 void* hidden_class = *obj;
 int offset = 0;
 while(*(hidden_class + offset) != 'a') offset++;
 int value = *(obj + offset + 1);

现在,如果我们有另一个看起来像这样的对象:

 address | 100  | 101  | 102 |
 value   |   3  |   5  |   7 |

然后我们可以使用与上面相同的方法,或者如果我们所做的地方 obj.a 总是获取传入的对象是隐藏的 class,我们也可以这样做:

  void* obj = 100;
  assert(*obj == 3);
  int value = *(obj + 1);

因此,如果引擎发现具有相同隐藏 class 的对象被传递给一个函数,它可以编译该函数,而不是使用字典中的搜索算法,它可以直接编译生成的偏移量进入功能。但是,如果传入的对象具有不同的隐藏 class,那么这不起作用,因此 属性 a 可能不在相同的偏移量处。因此引擎需要检查对象是否属于某个隐藏的class,如果不是,则回退到解释/去优化。

上面的例子肯定是非常简化的(不是每个值都适合一个字节)。

(此处为 V8 开发人员。)

JavaScript objects are not in fact hash maps under the hood, instead they are more similar to structs.

郑重声明,Bergi 正确地指出,这在一个引擎中是正确的,即使在那个引擎中也不总是如此。 JavaScript 引擎在如何准确地在内部表示对象方面有很大的自由度,并且它们确实利用了这种自由度。

What I don’t understand is how the syntax maps to that fixed offset. Ie what happens when the compiler sees obj.a or obj[‘a’]. Is that syntax transformed into an integer offset at run time or compile time or JIT?

系统基于缓存和“隐藏 classes”(有时称为“对象形状”或“[对象] 形状描述符”)。

当你有一个对象 obj = {a: 42, b: "hello", c: null} 时,它将有一个隐藏的 class(我们称它为 hiddenClassA,它列出了所有属性及其偏移量,例如“属性 a 存储在偏移量 12".

第一次执行包含 属性 负载的函数,如 obj.a 将使用未优化的代码。此代码必须检查对象,在其隐藏的 class' 属性列表中找到 a,从那里检索正确的偏移量,然后从对象中的偏移量读取以获得 属性的价值。然后为这个特定的 属性 负载缓存这对(隐藏 class,偏移量),因此下一次查找(即使在仍未优化的代码中)将 运行 快一点,如果另一个具有相同隐藏 class 的对象下次出现。

如果函数 运行 足够热门,它最终会得到优化。优化编译器查看未优化代码缓存的隐藏 classes 和偏移量,并假设您的应用程序未来的行为与过去的行为一样,因此它会发出如下代码序列:

  1. 验证obj隐藏了classhiddenClassA,否则反优化
  2. 从偏移量 12 加载

其中“去优化”意味着必须丢弃此函数的整个优化代码,因为它显然是基于无效的假设,并且执行将返回到未优化的代码以收集更多类型反馈(直到如果它仍然 运行 足够热,可能会在以后使用新的反馈进行重新优化)。不过,只要它 不需要 取消优化,优化后的代码将几乎与 C 对结构所做的一样快,而且它不必做任何 属性 查找,因为它只依赖于缓存的偏移量。

这种机制也是立即编译优化代码没有意义的原因:当优化编译器没有缓存类型信息(由未优化生成的)时,无法合理优化 属性 访问执行)可用。因为那时优化编译器会问与您完全相同的问题:“我究竟应该如何弄清楚偏移量 属性 a 映射到什么???”