【问题标题】:How are the JavaScript Arrays internally resizing?JavaScript 数组如何在内部调整大小?
【发布时间】:2017-10-06 22:36:10
【问题描述】:

我一直在尝试在具有一些自定义功能的 JavaScript 中实现类的集合类型(类似于 C# 中的 List)。我还希望对其进行一些优化(我已经阅读了一些关于如何正确使用 JavaScript 数组的文章)。

我对自己说“如果我们没有为数组定义初始大小并且我们不断向其中添加对象,那么在内部它必须为每次插入分配一个新大小,这一定很慢。我可以避免这种情况通过自己分配一个新的大小(更改数组长度),有点类似于在 C# 中的做法,只要达到最大容量,大小就会翻倍(我知道这不是微不足道的,而是一个开始)”。

我尝试实现这个想法,发现它慢了很多(大约慢了 10 倍):

// This simplified approach of my implementation is faster...
var array = [];
var counter = 0;
function addItem(newItem) {
    array[++counter] = newItem;
}

// ...then this version that resizes the array when a limit is reached
var array = [];
array.length = INITIAL_SIZE;
/*
 Alternatively
 var array = new Array(INITIAL_SIZE);
*/
var counter = 0;
function addItem(newItem) {
    if( CheckCapacity(counter + 1) ) { // Function that checks if the maximum size is reached and if it is, change the array.length to the new size
        array[++counter] = newItem;
    }
}

在测试这个之前,我想,“因为当我调用 CheckCapacity(counter + 1) 时数组有了新的大小,所以在内部它 (JavaScript Array) 与第一个函数,因为我确保有可用空间,超过必要”,即第二个函数上的 array[++counter] = newItem 行应该比同一个函数更快第一个函数。

我什至使用了不同的数组,其中包含预先计算的存放物品的大小;还是比较慢。

回到我的问题,JavaScript Array 的实现如何分配必要的大小?我是否正确地假设不能做太多事情来加快这个过程?对我来说,每次添加新项目时动态分配更多内存的对象(JavaScript 数组)的缺点是速度的损失(除非它实现了非常好的算法,但我没有'不知道,因此我的问题)。

【问题讨论】:

  • 阵列大小和存储已经在现代用户代理中进行了相当优化。为什么要重新发明轮子?
  • 我不确定您是否将[INITIAL_SIZE] 用作更复杂事物的简写,但var array = [INITIAL_SIZE]; 将创建一个单元素数组,其中第一个元素的值为INITIAL_SIZE。跨度>
  • 不想重新发明轮子。由于我可以优化许多 Array 方法,如 Splice、Push、Pop 等,我认为我可以对大小管理做一些事情。 JsPerf 上有很多与此相关的测试用例,例如 Array(N) vs [].length vs [] vs {}。我只是想再测试一下。
  • 没有“容量”。 JavaScript 数组不存储在连续内存中,因此,它们可以按需增长/缩小,而无需额外开销。更多地将其视为一种链表方法,而不是传统的数组结构。 stackoverflow.com/a/20323491/4987197
  • @mobileDev07 Array(N), [N], [], {} 都做了完全不同的事情...

标签: javascript arrays performance


【解决方案1】:

在 JavaScript 中,数组是一种抽象。它是如何实现的(以及何时执行分配和调整大小)由 JavaScript 引擎决定——ECMAScript 规范并没有规定如何完成。所以基本上没有确切的方法可以知道

在实践中,JavaScript 引擎非常聪明地分配内存并确保不会分配太多。在我看来,它们比 C# 的 List 复杂得多——因为 JavaScript 引擎可以根据情况动态更改底层数据结构。算法各不相同,但大多数会考虑您的数组中是否有任何“漏洞”:

var array = [];
array[0] = "foo"          // Is a resizable array
array[1] = "bar"          // Is a resizable array
array[2] = "baz"          // Is a resizable array
array[1000000] = "hello"; // Is now a hash table
console.log(array[1000000]) // "hello"

如果您正常使用数组并使用从零开始的连续键,则没有“漏洞”,大多数 JavaScript 引擎将使用可调整大小的数组数据结构来表示 JavaScript 数组。现在考虑第四个任务,我创建了一个大约一百万大小的所谓“洞”(洞跨越插槽 3-999999)。事实证明,JavaScript 引擎足够聪明,不会为这个巨大的漏洞分配大约 100 万个内存槽。它检测到我们有一个洞,它现在将使用类似字典/哈希表的数据结构(它使用对键进行哈希处理的二叉搜索树)来表示 JavaScript 数组以节省空间。它不会为孔存储空间,只有四个映射:(0, "foo")(1, "bar")(2, "baz")(1000000, "hello")

不幸的是,引擎现在访问数组的速度变慢了,因为它现在必须计算哈希并遍历树。当没有空洞时,我们使用一个可调整大小的数组并且我们有更快的访问时间,但是当我们有空洞时,数组的性能会变慢。常见的术语是说一个数组是一个密集数组,当它没有任何孔(它使用一个可调整大小的数组=更好的性能)时,一个数组是一个稀疏数组,当它一个或多个孔时(它使用哈希表 = 性能较慢)。一般来说,为了获得最佳性能,请尝试使用密集数组。

现在结束,让我告诉你以下是一个坏主意:

var array = new Array(1000000);
array[0] = "foo";               // Is a hash table

上面的数组有一个大小约为 100 万的孔(就像这样:["foo", undefined, undefined, ... undefined]),因此,它使用哈希表作为底层数据结构。因此,自己实施调整大小是一个坏主意 - 它会造成一个漏洞并导致最差的性能而不是更好的性能。你只是在混淆 JavaScript 引擎。

这就是你的代码所做的,你的数组总是有一个洞,因此使用哈希表作为底层数据结构;与没有任何漏洞的数组(也就是您的代码的第一个版本)相比,性能较慢。

我是否正确地假设不能做太多事情来加快这个过程?

是的,在预先分配空间方面,用户方面几乎没有什么可做的。一般来说,要加速 JavaScript 数组,您需要避免创建稀疏数组(避免创建空洞):

  1. 不要使用new Array(size) 进行预分配。而是“随心所欲地成长”。引擎将计算出底层可调整大小数组的大小本身
  2. 使用从 0 开始的连续整数键。不要从大整数开始。不要添加非整数的键(例如,不要使用字符串作为键)。
  3. 尽量不要删除数组中间的键(不要从填充了索引 0-9 的数组中删除索引 5 处的元素)。
  4. 不要在密集和稀疏数组之间进行转换(即不要重复添加和删除孔)。引擎在可调整大小的数组与哈希表表示之间进行转换会产生开销。

[JavaScript 数组优于 C# 列表的缺点是它们] 每次添加新项目时都会动态分配更多内存

不,不一定。当 JavaScript 数组没有漏洞时,C# 列表和 JavaScript 数组基本相同。两者都是可调整大小的数组。不同的是:

  1. C# 列表让用户可以更好地控制可调整大小数组的行为。在 JavaScript 中,您无法控制它——它在引擎内部。
  2. C# 列表允许用户预先分配内存以获得更好的性能,而在 JavaScript 中,您应该让引擎自动计算如何在底层可调整大小的数组中预先分配内存以获得更好的性能。

【讨论】:

  • 优秀的答案。
猜你喜欢
  • 2012-01-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-28
  • 1970-01-01
  • 2011-11-10
相关资源
最近更新 更多