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中的数组类型

相关推荐
高木的小天才4 分钟前
鸿蒙中的并发线程间通信、线程间通信对象
前端·华为·typescript·harmonyos
Danta1 小时前
百度网盘一面值得look:我有点难受🤧🤧
前端·javascript·面试
OpenTiny社区1 小时前
TinyVue v3.22.0 正式发布:深色模式上线!集成 UnoCSS 图标库!TypeScript 类型支持全面升级!
前端·vue.js·开源
dwqqw1 小时前
opencv图像库编程
前端·webpack·node.js
Captaincc2 小时前
为什么MCP火爆技术圈,普通用户却感觉不到?
前端·ai编程
海上彼尚2 小时前
使用Autocannon.js进行HTTP压测
开发语言·javascript·http
阿虎儿3 小时前
MCP
前端
layman05283 小时前
node.js 实战——(fs模块 知识点学习)
javascript·node.js
毕小宝3 小时前
编写一个网页版的音频播放器,AI 加持,So easy!
前端·javascript
万水千山走遍TML3 小时前
JavaScript性能优化
开发语言·前端·javascript·性能优化·js·js性能