ECMAScript 杂谈:快慢数组

前言

ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。

通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。

欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇

快慢模式

JS中的数组在V8中对应的是 JSArray, 其源码有一段很直白的注释

github.com/v8/v8/blob/...

  • 快模式: 采用FixedArray来存储,length <= elements.length()。push 和 pop 操作会增加或者缩小数组。
  • 慢模式: 用数字为键的哈希表(HashTable)来存储。

这里提到了一个elements,可以通过 %DebugPrint打印数组的信息来查看:

比如采用如下代码:

javascript 复制代码
var array1 = [0,1,2];
%DebugPrint(array1);

array1[1027] = 1027;
%DebugPrint(array1);

在输出的内容中,有elements字段,第一次打印的时候其为FixedArray, 第二次打印已经变为NumberDictonary。

javascript 复制代码
DebugPrint: 000001074AA7FB89: [JSArray]
 - map: 0x03702b7032d9 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x0134264c5971 <JSArray[0]>
 - elements: 0x015c079b7139 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
 - length: 3
 - properties: 0x01ce95b01309 <FixedArray[0]>
 - All own properties (excluding elements): {
    000001CE95B04D59: [String] in ReadOnlySpace: #length: 0x00cbc4501189 <AccessorInfo> (const accessor descriptor)


DebugPrint: 000001074AA7FB89: [JSArray]
 - map: 0x03702b732211 <Map(DICTIONARY_ELEMENTS)> [FastProperties]
 - prototype: 0x0134264c5971 <JSArray[0]>
 - elements: 0x01074aa7fc39 <NumberDictionary[28]> [DICTIONARY_ELEMENTS]
 - length: 1028
 - properties: 0x01ce95b01309 <FixedArray[0]>
 - All own properties (excluding elements): {
    000001CE95B04D59: [String] in ReadOnlySpace: #length: 0x00cbc4501189 <AccessorInfo> (const accessor descriptor)
  

这快慢模式,体现的其实是两种不同的思想:

  • 快模式 : 空间换时间

其会申请连续的内存,提高速度。

  • 慢模式: 时间换空间

其不需要申请连续的内容,节约内存,但是降低了速度。

数组扩容

数组容量不够实用的时候,会进行扩容,扩容方法如下:JSObject::NewElementsCapacity

诶,为啥是 JSObject的方法呢? 数组也是对象哦。

javascript 复制代码
// https://github.com/v8/v8/blob/9.3.345/src/objects/js-objects.h#L558
static uint32_t NewElementsCapacity(uint32_t old_capacity) {
  //  old_capacity + (old_capacity >> 1) + 16
  return old_capacity + (old_capacity >> 1) + kMinAddedElementsCapacity;
}

// https://github.com/v8/v8/blob/9.3.345/src/objects/js-objects.h#L555
static const uint32_t kMinAddedElementsCapacity = 16;

结合上面的图来看,这里的 old_capacity并不是 旧容量, 而是 新的最大索引值 + 1,或者新的数组长度,不过还是这么称呼她吧。

新容积 = 旧容量+ (旧容量>> 1) + 16

示例1

一起来看个例子

  • 一个初始化容量为 5的数组,
  • 设置索引 6 为 6, 套入公式,其容量扩容到 (6 + 1) + ( (6+1) >>1 ) + 16 = 26
javascript 复制代码
// 容量:5
var array1 = [0, 1, 2, 3, 4];

console.log('array1.length:', array1.length);
%DebugPrint(array1);

// 旧容量+ (旧容量>> 1) + 16 = (6 + 1) + ( (6+1) >>1 ) + 16  = 26
array1[6] = 6;  

%DebugPrint(array1);

扩容前:5

扩容后:26

示例2

哦,懂了,懂了,懂了,少年,你没懂。接着看

javascript 复制代码
var array1 = new Array();

console.log('array1.length:', array1.length);

// 容量:4
%DebugPrint(array1);

// ??  旧容量+ (旧容量>> 1) + 16 = (5000 + 1) + ((5000 + 1) >>1 ) + 16  = 7517
array1[5000] = 5000;  

%DebugPrint(array1);

new Array() 数组长度为0, 容量却是 4 , 这是 js-array.h kPreallocatedArrayElements变量定义的,学到没?

诶,这怎么不按照规矩扩容了呢? 仔细看 这已经不是 数组了,而是字典呢, 变成慢数组了。

所以呢:

  • 之前提到的扩容,不是一定能得到生效的,如果扩容后还是快数组,是会生效的,但是
  • 从上面的示例也看到了,快数组扩容后,可能转为慢数组。

那么接下来,一起探究。。。。。。

快转慢

1024 :快数组新增的索引与原数组长度的差值大于等于1024,快数组会被转换会慢数组

v8 js-objects.h 里方法 ShouldConvertToSlowElements 就是做这个判断。返回true, 表示应该转为慢数组。

只关注返回true部分的代码,代码变量JSObject::kMaxGap 位于 github.com/v8/v8/blob/..., 其值为1024。

javascript 复制代码
  // Maximal gap that can be introduced by adding an element beyond
  // the current elements length.
  static const uint32_t kMaxGap = 1024;

注意 (1) 新索引 - 数组长度 >= 1024 部分, 代码15行

javascript 复制代码
static inline bool ShouldConvertToSlowElements(JSObject object,
                                               uint32_t capacity,
                                               uint32_t index,
                                               uint32_t* new_capacity) {
  static_assert(JSObject::kMaxUncheckedOldFastElementsLength <=
                JSObject::kMaxUncheckedFastElementsLength);
  if (index < capacity) {
    *new_capacity = capacity;
    return false;
  }
  
  // **(1)  新索引 - 数组长度  >=  1024**
  if (index - capacity >= JSObject::kMaxGap) return true;

  // (index + 1) + ((index + 1) >> 1) + 16
  *new_capacity = JSObject::NewElementsCapacity(index + 1);
  DCHECK_LT(index, *new_capacity);
  if (*new_capacity <= JSObject::kMaxUncheckedOldFastElementsLength ||
      (*new_capacity <= JSObject::kMaxUncheckedFastElementsLength &&
       ObjectInYoungGeneration(object))) {
    return false;
  }
  return ShouldConvertToSlowElements(object.GetFastElementsUsage(),
                                     *new_capacity);
}


// https://github.com/v8/v8/blob/9.3.345/src/objects/js-objects.h#L558
static uint32_t NewElementsCapacity(uint32_t old_capacity) {
  // (old_capacity + 50%) + kMinAddedElementsCapacity
  return old_capacity + (old_capacity >> 1) + kMinAddedElementsCapacity;
}

// https://github.com/v8/v8/blob/9.3.345/src/objects/js-objects.h#L555
static const uint32_t kMinAddedElementsCapacity = 16;

下面以一个长度为3的数组为例, 3 + 1024 = 1027 , 如果新的索引值大于等于1027, 数组类型即会发生变化。

如下分别以新索引值为1026和1027来测试:

新索引 1026:(FixedArray)

javascript 复制代码
var array1 = [0,1,2];
array1[1026] = 1026;
%DebugPrint(array1);
javascript 复制代码
DebugPrint: 000000A96B7BFB91: [JSArray]
 - map: 0x03c9db003291 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x025eba505971 <JSArray[0]>
 - elements: 0x03d1638c1119 <FixedArray[1556]> [HOLEY_SMI_ELEMENTS]
 - length: 1027

新索引 1027:(NumberDictionary)

javascript 复制代码
var array1 = [0,1,2];
array1[1027] = 1027;
%DebugPrint(array1);
javascript 复制代码
DebugPrint: 0000036B3FBBFB99: [JSArray]
 - map: 0x019d429b2211 <Map(DICTIONARY_ELEMENTS)> [FastProperties]
 - prototype: 0x001664b05971 <JSArray[0]>
 - elements: 0x036b3fbbfc49 <NumberDictionary[28]> [DICTIONARY_ELEMENTS]
 - length: 1028

可以看到新索引值为1027的时候, elements变为了 NumberDictionary,而不是原来的 FixedArray, 快转慢了。

500 和 5000

注意:这个数值扩容后的容量。

扩容后的容量, 满足下面条件之一,不会快转慢:

  • 如果扩容后容量小于等于 500 (JSObject::kMaxUncheckedOldFastElementsLength)
  • 如果扩容后容量小于等于 5000 (JSObject::kMaxUncheckedFastElementsLength) 并且 在新生代。
    垃圾回收的新生代存放的是: 新创建的对象,小对象,短生命周期对象,存活时间短的对象
    数组呢,一次垃圾回收后,是可能可以从新生代 转移到 老生代的。

注意19,20行

javascript 复制代码
static inline bool ShouldConvertToSlowElements(JSObject object,
                                               uint32_t capacity,
                                               uint32_t index,
                                               uint32_t* new_capacity) {
  static_assert(JSObject::kMaxUncheckedOldFastElementsLength <=
                JSObject::kMaxUncheckedFastElementsLength);
  if (index < capacity) {
    *new_capacity = capacity;
    return false;
  }
  
  //(1)  新索引 - 数组长度  >=  1024
  if (index - capacity >= JSObject::kMaxGap) return true;

  // (index + 1) + ((index + 1) >> 1) + 16
  *new_capacity = JSObject::NewElementsCapacity(index + 1);
  DCHECK_LT(index, *new_capacity);

  // 新容积 <=  500 不转为慢数组
  // 或者 新容积 <= 5000 并且 在新生代
  if (*new_capacity <= JSObject::kMaxUncheckedOldFastElementsLength ||
      (*new_capacity <= JSObject::kMaxUncheckedFastElementsLength &&
       ObjectInYoungGeneration(object))) {
    return false;
  }
  
  return ShouldConvertToSlowElements(object.GetFastElementsUsage(),
                                     *new_capacity);
}

// https://github.com/v8/v8/blob/10.7.116/src/objects/js-objects.h#L833
// Maximal length of fast elements array that won't be checked for
// being dense enough on expansion.
static const int kMaxUncheckedFastElementsLength = 5000;

// https://github.com/v8/v8/blob/10.7.116/src/objects/js-objects.h#L837
// Same as above but for old arrays. This limit is more strict. We
// don't want to be wasteful with long lived objects.
static const int kMaxUncheckedOldFastElementsLength = 500;

扩容后容量后数组容量(小于500)和 新生代数组(容量小于5000)做了特殊的处理,原因嘛,v8大家都懂,性能问题。

怎么去模拟非新生代呢? 其实也不难,可以添加一个额外的选项,让程序可以手动的执行gc, 比如:

node --allow-natives-syntax --expose-gc "xxx.js"

500示例

这里知道扩容后是 500,为了推导出阈值,这里需要反推 index 的最大值。

根据扩容公式: 新容量 = (index + 1) + ((index + 1) >> 1) + 16

这里可以推导值,大概是 321 附近的值,

带入值 推导 最终值
321 (321 + 1) + ((321 + 1) >> 1) + 16 499
322 (322 + 1) + ((322 + 1) >> 1) + 16 500
323 (323 + 1) + ((323 + 1) >> 1) + 16 502

所以 阈值322, 如下的 323 代码执行后,数组转为了慢模式,elements 转为了 NumberDictionary

javascript 复制代码
var array1 = [0,1,2];

array1[0] = -0;
// 垃圾回收, 触发转移到老生代
global.gc();

array1[323] = 323;
%DebugPrint(array1);


DebugPrint: 0000030A8F59ED79: [JSArray] in OldSpace
 - map: 0x0049607eece9 <Map(DICTIONARY_ELEMENTS)> [FastProperties]
 - prototype: 0x01c670ac5971 <JSArray[0]>
 - elements: 0x0248426411b9 <NumberDictionary[28]> [DICTIONARY_ELEMENTS]
 - length: 502
 - properties: 0x016767481309 <FixedArray[0]>
 - All own properties (excluding elements): {
    0000016767484D59: [String] in ReadOnlySpace: #length: 0x000c6eb01189 <AccessorInfo> (const accessor descriptor)

如下不调用global.gc()的代码执行不会发生改变,依旧还是快模式, 为什么呢?因为其后还有一个 5000 的限制。

javascript 复制代码
var array1 = [0,1,2];

// array1[0] = -0;
// global.gc();

array1[501] = 501;
%DebugPrint(array1);


DebugPrint: 0000033A2BE3FAA9: [JSArray]
 - map: 0x034368ac3291 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x03a25e6c5971 <JSArray[0]>
 - elements: 0x0118d1381119 <FixedArray[769]> [HOLEY_SMI_ELEMENTS]
 - length: 502
 - properties: 0x02f39dd81309 <FixedArray[0]>
 - All own properties (excluding elements): {
    000002F39DD84D59: [String] in ReadOnlySpace: #length: 0x01e9efe81189 <AccessorInfo> (const accessor descriptor)

5000 示例

根据扩容公式: 新容量 = (index + 1) + ((index + 1) >> 1) + 16

大概是 3321 附近的值,

带入值 推导 最终值
3321 (3321 + 1) + ((3321 + 1) >> 1) + 16 4999
3322 (3322 + 1) + ((3322 + 1) >> 1) + 16 5000
3323 (3323 + 1) + ((3323 + 1) >> 1) + 16 5002

索引值为3322。这里只是可以跳过新生代5000的限制,执行发生快转慢依旧是9倍那种判断的。

新最大索引值为3322, 扩容后小于等于 5000, 不发生快转慢。

javascript 复制代码
var array1 = new Array(3000);

// 不会发生快转慢
// 扩容后的值  (3322 + 1) + ((3322 + 1) >> 1) + 16 = 5000 <= 5000
array1[3322] = 3322;
%DebugPrint(array1);

DebugPrint: 000001D0AAD41119: [JSArray]
 - map: 0x0369b6743291 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x028f6f7c5971 <JSArray[0]>
 - elements: 0x01d0aad46f09 <FixedArray[5000]> [HOLEY_SMI_ELEMENTS]
 - length: 3323
 - properties: 0x027c40c81309 <FixedArray[0]>

新最大索引值为3323, 扩容后小于等于 5002, 发生快转慢, elements转为 NumberDictionary。 但是注意了,容量缺小了。

javascript 复制代码
var array1 = new Array(3000);

// 发生快转慢
array1[3323] = 3323;  // (3323 + 1) + ((3323 + 1) >> 1) + 16 = 5002  
%DebugPrint(array1);

DebugPrint: 000003AD718C1119: [JSArray]
 - map: 0x039647f32211 <Map(DICTIONARY_ELEMENTS)> [FastProperties]
 - prototype: 0x0071fcdc5971 <JSArray[0]>
 - elements: 0x03ad718c6f09 <NumberDictionary[16]> [DICTIONARY_ELEMENTS]
 - length: 3324
 - properties: 0x0345f1a41309 <FixedArray[0]>

这里是没有触发垃圾回收的,如果主动触发一次呢? 这里交给大家去尝试。

9倍:旧数组扩容后的容量 是 基于旧数组已使用容量计算而得的值 9倍及以上

9倍的计算逻辑源码位置: ShouldConvertToSlowElements(uint32_t used_elements, uint32_t new_capacity)

与之前的 ShouldConvertToSlowElements(JSObject object, uint32_t capacity,uint32_t index, uint32_t* new_capacity) 不一样,后者内部再调用了前者,也就是前面的各种场景完毕之后,才轮到本小节这个9倍。

其核心主流程代码如下:

  • 12 行: 新索引 - 数组长度 >= 1024
  • 19,20行:500 和 5000
  • 27行:9倍逻辑
javascript 复制代码
static inline bool ShouldConvertToSlowElements(JSObject object,
                                               uint32_t capacity,
                                               uint32_t index,
                                               uint32_t* new_capacity) {
  static_assert(JSObject::kMaxUncheckedOldFastElementsLength <=
                JSObject::kMaxUncheckedFastElementsLength);
  if (index < capacity) {
    *new_capacity = capacity;
    return false;
  }
  
  //(1)  新索引 - 数组长度  >=  1024
  if (index - capacity >= JSObject::kMaxGap) return true;

  // (index + 1) + ((index + 1) >> 1) + 16
  *new_capacity = JSObject::NewElementsCapacity(index + 1);
  DCHECK_LT(index, *new_capacity);

  // 新容积 <=  500 不转为慢数组
  // 或者 新容积 <= 5000 并且 在新生代
  if (*new_capacity <= JSObject::kMaxUncheckedOldFastElementsLength ||
      (*new_capacity <= JSObject::kMaxUncheckedFastElementsLength &&
       ObjectInYoungGeneration(object))) {
    return false;
  }
  
  return ShouldConvertToSlowElements(object.GetFastElementsUsage(),
                                     *new_capacity);
}

9倍核心代码和逻辑

javascript 复制代码
static inline bool ShouldConvertToSlowElements(uint32_t used_elements,
                                               uint32_t new_capacity) {
  uint32_t size_threshold = NumberDictionary::kPreferFastElementsSizeFactor *
                            NumberDictionary::ComputeCapacity(used_elements) *
                            NumberDictionary::kEntrySize;
  return size_threshold <= new_capacity;
}

// https://github.com/v8/v8/blob/10.7.116/src/objects/dictionary.h#L360
static const uint32_t kPreferFastElementsSizeFactor = 3;


// https://github.com/v8/v8/blob/10.7.116/src/objects/dictionary.h#L317
class NumberDictionaryShape : public NumberDictionaryBaseShape {
 public:
  static const int kPrefixSize = 1;
  static const int kEntrySize = 3;
};

公式

其中:

  • NumberDictionary::kPreferFastElementsSizeFactor = 3
  • NumberDictionary::kEntrySize = 3
  • used_elements 是一个数组实际使用的容量,比如长度为100, 只有一个非空元素,那么值为1。
  • NumberDictionary::ComputeCapacity(used_elements)
    基于旧数组已使用容量计算而得的值,简单说:就是算出不小 4, 且满足 used_elements + (used_elements >> 1) > 2**n的 最小 2**n 值。
    抽象把,举个例子:
    以 used_elements为 40为例, ComputeCapacity 最终计算的值为 64。
    1. 40 + (40 >> 1) = 60
    2. 最小大于60的 2**n 为 64
    3. kMinCapacity为4, Math.max(64, 4) 的结果为64
      那么上面的 size_threshold = 3 * 3 * ComputeCapacity(used_elements) = 384
  • new_capacity 是扩容后的容量, 计算公式: 旧容量+ (旧容量>> 1) + 16

把相关的代码放在一起,如下

javascript 复制代码
// If the fast-case backing storage takes up much more memory than a dictionary
// backing storage would, the object should have slow elements.
// static
static inline bool ShouldConvertToSlowElements(uint32_t used_elements,
                                               uint32_t new_capacity) {
  uint32_t size_threshold = NumberDictionary::kPreferFastElementsSizeFactor *
                            NumberDictionary::ComputeCapacity(used_elements) *
                            NumberDictionary::kEntrySize;
  return size_threshold <= new_capacity;
}

// https://github.com/v8/v8/blob/10.7.116/src/objects/dictionary.h#L360
static const uint32_t kPreferFastElementsSizeFactor = 3;

// https://github.com/v8/v8/blob/10.7.116/src/objects/dictionary.h#L266
class NumberDictionaryShape : public NumberDictionaryBaseShape {
 public:
  static const int kPrefixSize = 1;
  static const int kEntrySize = 3;
};

// https://github.com/v8/v8/blob/10.7.116/src/objects/js-objects.h#L577
static const uint32_t kMinAddedElementsCapacity = 16;

// https://github.com/v8/v8/blob/9.3.345/src/objects/js-objects.h#L558
static uint32_t NewElementsCapacity(uint32_t old_capacity) {
  // (old_capacity + 50%) + kMinAddedElementsCapacity (16)
  return old_capacity + (old_capacity >> 1) + kMinAddedElementsCapacity;
}

// https://github.com/v8/v8/blob/10.7.116/src/objects/hash-table-inl.h#L116
// static
int HashTableBase::ComputeCapacity(int at_least_space_for) {
  // Add 50% slack to make slot collisions sufficiently unlikely.
  // See matching computation in HashTable::HasSufficientCapacityToAdd().
  // Must be kept in sync with CodeStubAssembler::HashTableComputeCapacity().
  int raw_cap = at_least_space_for + (at_least_space_for >> 1);
  // 50 变成了 64, 100/120 变成了128, 200 变成了 256
  int capacity = base::bits::RoundUpToPowerOfTwo32(raw_cap);
  // kMinCapacity = 4  https://github.com/v8/v8/blob/10.7.116/src/objects/hash-table.h#L102
  return std::max({capacity, kMinCapacity});
}

// https://github.com/v8/v8/blob/10.7.116/src/objects/hash-table.h#L102
static const int kMinCapacity = 4;

示例

这里的9倍和一个数组非空元素是关联起来的,此例子

  1. 新建一个容量为499的数组
  2. 然后填充部分元素,制造 use_elements, 此例子 43是临界值,43发生快转慢,44不会发生。 空元素越少,越不容易发生快转慢。
  3. 垃圾回收
  4. 给索引499赋值,让数据扩容,触发快模式转换慢模式

以使用容量 used_elements 为43的:
size_threshold <= new_capacity => 576 <= 766 => true

触发扩容会发生快转慢,详细计算看代码备注:

javascript 复制代码
var array1 = new Array(499);

// 填充数量,制造 used_elements = 43
for (let i = 0; i < 43; i++) {
    array1[i] = i;
}

// 回收
global.gc();
console.log(array1.filter(v=> v!== null).length);


// https://github.com/v8/v8/blob/9.3.345/src/objects/js-objects-inl.h
// new_capacity: 500 + (500 << 1) + 16 = 766, 大于500
// NumberDictionary::ComputeCapacity(used_elements):  43 + (43 >> 1) => 64  =>  64
// size_threshold : 3 * 3 *  64  = 576
// size_threshold <= new_capacity =>  576 <= 766 => true

array1[499] = 499;
% DebugPrint(array1);

elements 转为为 NumberDictionary

used_elements 为44的时候:
size_threshold <= new_capacity => 1152 <= 766 => false

不会触发块转慢,详细计算看代码备注:

javascript 复制代码
var array1 = new Array(499);

// 填充数量, 制造 use_elements
for (let i = 0; i < 44; i++) {
    array1[i] = i;
}

// 回收
global.gc();
console.log(array1.filter(v=> v!== null).length);


// https://github.com/v8/v8/blob/9.3.345/src/objects/js-objects-inl.h
// new_capacity: (499 + 1) * 1.5 + 16 = 766, 大于500
// NumberDictionary::ComputeCapacity(used_elements):  44 + (44 >> 1) => 128  =>  128
// size_threshold : 3 * 3 *  128  = 1152
// size_threshold <= new_capacity =>  1152 <= 766 => false
array1[499] = 499;
% DebugPrint(array1);

//  node --allow-natives-syntax --expose-gc "4.0 Array\1.4 to Slow 9m-02.js"

// console.log('process.versions:', process.versions)

elements 依旧是 FixedArray,快模式。

模式转换发生在赋值过程,不是初始化过程

而且是新的索引大于当前数组的能提供最大索引的时候

如下,初始化一个超大的数组, 对不超过当前数组能提供的最大索引赋值,是不会发生变化的。

所以,没事别初始化超大的数组。

javascript 复制代码
var array1 = new Array(2 ** 20);
var a = 1;
var b = 2;
for (let i = 0; i < 1000; i++) {
    i + 1;
}
global.gc();
array1[2 ** 20 - 2] = 0;
%DebugPrint(array1);

DebugPrint: 0000000EBABD99E9: [JSArray] in OldSpace
 - map: 0x0369f2043291 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x026202645971 <JSArray[0]>
 - elements: 0x00df16881119 <FixedArray[1048576]> [HOLEY_SMI_ELEMENTS]
 - length: 1048576

慢转快

数组也是可以从慢数组转为快书数组的, 源码位置 github.com/v8/v8/blob/... ShouldConvertToFastElements方法,前面都是返回false, 主要看最后一行:

当慢数组容积 相比数组长度省小于等于50%,则转变成为快数组。

其意图也就是当一个数组的空元素由很多变到一定少的的时候,其会由慢模式转为快模式,想想也极其合理。

javascript 复制代码
static bool ShouldConvertToFastElements(JSObject object,
                                        NumberDictionary dictionary,
                                        uint32_t index,
                                        uint32_t* new_capacity) {
  // If properties with non-standard attributes or accessors were added, we
  // cannot go back to fast elements.
  if (dictionary.requires_slow_elements()) return false;

  // Adding a property with this index will require slow elements.
  if (index >= static_cast<uint32_t>(Smi::kMaxValue)) return false;

  if (object.IsJSArray()) {
    Object length = JSArray::cast(object).length();
    if (!length.IsSmi()) return false;
    *new_capacity = static_cast<uint32_t>(Smi::ToInt(length));
  } else if (object.IsJSArgumentsObject()) {
    return false;
  } else {
    *new_capacity = dictionary.max_number_key() + 1;
  }
  *new_capacity = std::max(index + 1, *new_capacity);

  uint32_t dictionary_size = static_cast<uint32_t>(dictionary.Capacity()) *
                             NumberDictionary::kEntrySize;

  // Turn fast if the dictionary only saves 50% space.
  return 2 * dictionary_size >= *new_capacity;
}

先造一个慢模式的数组:

依据: 快数组新增的索引与原容量(长度)的差值大于等于 1024,快数组会被转换会慢数组

javascript 复制代码
var array1 = [0, 1, 2];
// 1028     1027 - 3 = 1024  会导致数组转为慢模式
array1[1027] = 1027;

之后,开始从索引3开始填充数组,当数组里面的空元素数量越来越少的时候,其会有一个临界值,由慢模式再回恢复为快模式。

如下,分别会从索引3填充到83,84,85, 来验证。

javascript 复制代码
// 填充数组
for (let index = 3; index < array1.length; index++) {
    if(index === 83){
        console.log('index:before', index);
        %DebugPrint(array1);
        console.log('index:after', index);
        array1[index] = index;
        %DebugPrint(array1);
        break;
    }
    array1[index] = index;  
}

先一起看看 index 分别为 83, 84, 85 赋值前后输出情况:

可以看到index = 84 赋值之后,字典进行扩容,容积从388升级到了772。

index = 83 填充前后:

javascript 复制代码
index:before 83
DebugPrint: 0000026E925BFB39: [JSArray]
 - map: 0x014f37fb2211 <Map(DICTIONARY_ELEMENTS)> [FastProperties]
 - prototype: 0x03af0cc85971 <JSArray[0]>
 - elements: 0x02a079dc1a79 <NumberDictionary[388]> [DICTIONARY_ELEMENTS]
 - length: 1028
 - properties: 0x02bee9e81309 <FixedArray[0]>
 - All own properties (excluding elements): {
    000002BEE9E84D59: [String] in ReadOnlySpace: #length: 0x003b01381189 <AccessorInfo> (const accessor descriptor)


 index:after 83
DebugPrint: 0000026E925BFB39: [JSArray]
 - map: 0x014f37fb2211 <Map(DICTIONARY_ELEMENTS)> [FastProperties]
 - prototype: 0x03af0cc85971 <JSArray[0]>
 - elements: 0x02a079dc1a79 <NumberDictionary[388]> [DICTIONARY_ELEMENTS]
 - length: 1028
 - properties: 0x02bee9e81309 <FixedArray[0]>
 - All own properties (excluding elements): {
    000002BEE9E84D59: [String] in ReadOnlySpace: #length: 0x003b01381189 <AccessorInfo> (const accessor descriptor)     

index = 84 填充前后, 填充后elements的 NumberDictionary容积从388变为了772.

javascript 复制代码
index:before 84
DebugPrint: 000000513DC3FB39: [JSArray]
 - map: 0x016bd48b2211 <Map(DICTIONARY_ELEMENTS)> [FastProperties]
 - prototype: 0x026e4bac5971 <JSArray[0]>
 - elements: 0x03043df01a79 <NumberDictionary[388]> [DICTIONARY_ELEMENTS]
 - length: 1028
 - properties: 0x01dfc8cc1309 <FixedArray[0]>
 - All own properties (excluding elements): {
    000001DFC8CC4D59: [String] in ReadOnlySpace: #length: 0x035ef3b01189 <AccessorInfo> (const accessor descriptor


DebugPrint: 000000513DC3FB39: [JSArray]
 - map: 0x016bd48b2211 <Map(DICTIONARY_ELEMENTS)> [FastProperties]
 - prototype: 0x026e4bac5971 <JSArray[0]>
 - elements: 0x03043df17821 <NumberDictionary[772]> [DICTIONARY_ELEMENTS]
 - length: 1028
 - properties: 0x01dfc8cc1309 <FixedArray[0]>
 - All own properties (excluding elements): {
    000001DFC8CC4D59: [String] in ReadOnlySpace: #length: 0x035ef3b01189 <AccessorInfo> (const accessor descriptor)

index = 85 填充前后, 填充后elements 从NumberDictionary[772]变为了 FixedArray[1028]

javascript 复制代码
index:before 85
DebugPrint: 000000C4298FFB39: [JSArray]
 - map: 0x003207472211 <Map(DICTIONARY_ELEMENTS)> [FastProperties]
 - prototype: 0x001f05285971 <JSArray[0]>
 - elements: 0x0322b5a026a9 <NumberDictionary[772]> [DICTIONARY_ELEMENTS]
 - length: 1028
 - properties: 0x032a6c901309 <FixedArray[0]>
 - All own properties (excluding elements): {
    0000032A6C904D59: [String] in ReadOnlySpace: #length: 0x0247894c1189 <AccessorInfo> (const accessor descriptor)

index:after 85
DebugPrint: 000000C4298FFB39: [JSArray]
 - map: 0x003207477c11 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x001f05285971 <JSArray[0]>
 - elements: 0x0322b5a19051 <FixedArray[1028]> [HOLEY_SMI_ELEMENTS]
 - length: 1028
 - properties: 0x032a6c901309 <FixedArray[0]>
 - All own properties (excluding elements): {
    0000032A6C904D59: [String] in ReadOnlySpace: #length: 0x0247894c1189 <AccessorInfo> (const accessor descriptor)    

可以看到:

  1. index = 84 赋值之后,字典的容积从 388增大到了 772
  2. index = 85 赋值之后,elments从NumberDictionary[772]变为了 FixedArray[1028], 即从慢数组变为了快数组。

一起来看看这个临界的变化:

index = 85 时( 此时字典dictionary_size 是 772):

  • new_capacity = 1028
    *new_capacity = std::max(index + 1, *new_capacity);
    此时的 new_capacity 为数组长度1028, 当然比index + 1大,故值为1028.
  • dictionary_size = 772
    代入:2 * dictionary_size >= *new_capacity = 1544 >= 1028 , 值为true。 故会进行转换快数组。

index = 84 时( 此时字典dictionary_size 是 388):

  • new_capacity = 1028
    *new_capacity = std::max(index + 1, *new_capacity);
    此时的 *new_capacity 为数组长度1028, 当然比index + 1大,故值为1028.
  • dictionary_size = 388
    代入:2 * dictionary_size >= *new_capacity = 776 >= 1028 , 值为false。 故不会进行转换为快数组。

引用

Elements kinds in V8 | [译]V8中的数组类型

相关推荐
hao_04138 分钟前
elpis-core: 基于 Koa 实现 web 服务引擎架构设计解析
前端
码农黛兮_461 小时前
HTML、CSS 和 JavaScript 基础知识点
javascript·css·html
狂野小青年1 小时前
npm 报错 gyp verb `which` failed Error: not found: python2 解决方案
前端·npm·node.js
鲁鲁5171 小时前
Windows 环境下安装 Node 和 npm
前端·npm·node.js
跑调却靠谱1 小时前
elementUI调整滚动条高度后与固定列冲突问题解决
前端·vue.js·elementui
呵呵哒( ̄▽ ̄)"1 小时前
React - 编写选择礼物组件
前端·javascript·react.js
Coding的叶子2 小时前
React Flow 简介:构建交互式流程图的最佳工具
前端·react.js·流程图·fgai·react agent
apcipot_rain6 小时前
【应用密码学】实验五 公钥密码2——ECC
前端·数据库·python
油丶酸萝卜别吃7 小时前
OpenLayers 精确经过三个点的曲线绘制
javascript
ShallowLin7 小时前
vue3学习——组合式 API:生命周期钩子
前端·javascript·vue.js