【问题标题】:How to append contents of one array onto another without intermediate copy of all the data?如何在没有所有数据的中间副本的情况下将一个数组的内容附加到另一个数组中?
【发布时间】:2020-09-09 22:01:11
【问题描述】:

是否有任何干净有效的方法可以将一个数组的内容直接添加到另一个数组,而无需制作所有数据的中间/临时副本?

例如,您可以使用.push() 将一个数组的内容直接添加到另一个数组中,如下所示:

// imagine these are a both large arrays
let base = [1,2,3];
let data = [4,5,6]
base.push(...data);

但是,这似乎复制了data 中的所有项目,因为它使它们成为.push() 的参数。 .splice() 也是如此:

// imagine these are a both large arrays
let base = [1,2,3];
let data = [4,5,6]
base.splice(base.length, 0, ...data);

从内存的角度(所有数据的额外副本)和执行的角度(数据被迭代两次)的角度来看,这两种方法似乎都是低效的。

.concat() 之类的方法不会将一个数组的内容添加到另一个数组,而是使用组合内容创建一个新数组(复制两个数组的内容)。

我有一些包含大量操作的大型数组,我试图通过避免不必要的中间副本来减轻 CPU/垃圾收集器的负担,我很奇怪我还没有找到这样的内置-在操作中。到目前为止,我避免不必要的副本的最佳选择是:

// imagine these are a both large arrays
let base = [1,2,3];
let data = [4,5,6];
for (let item of data) {
    base.push(item);
}

如果它是一个操作,显然它的执行效率可能不如它可能是一个操作,而且当你希望它是一行时,它显然是多行代码。

【问题讨论】:

  • AFAIK 如果你想得到一个数组,它永远不会是一个操作。如果您想在合并中进行一项操作,则可以使用自定义数据结构。像 2d 数组,但对其进行迭代会很复杂。即使拼接和展开运算符也不是“一次操作”,即使它们使用了这么多额外空间
  • 浏览器可能会优化base.push(...data)(现在或将来的版本)。参见例如 dev.to/uilicious/…stackoverflow.com/questions/5080028/…dmitripavlutin.com/…
  • @SebastianB。 - 问题是,如果它是一个大数组,并且您尝试在未优化它的环境中运行它,则会出现堆栈溢出。因此,“可能会得到优化”并不能很好地工作。有趣的是,它可以得到优化。我只需要一种始终优化的方法。您的第二个参考在其中一个答案中提到了堆栈溢出问题。所以,我不能以一种可能做到这一点的方式编码。
  • 我明白了,这是一个有趣的问题。但我担心在这种高度解释的语言中手动优化也是危险的。如果你把它推到极限,今天有效的东西明天可能就没有那么高效了。使用typed arrays 是一种选择吗?或者那部分的 WebAssembly (developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…)。只是在没有自己使用这些 API 的情况下进行头脑风暴......
  • @SebastianB。 - 我了解在移动目标上调整优化的危险。但是,我必须通过将一个巨大的数组推到堆栈上来避免可能导致堆栈爆炸的结构,所以我不能真正使用它,除非我的代码只针对一个非常特定的 JS 引擎,该引擎已知对我的特定条件是安全的,比如 nodejs V12+ 然后它假设 nodejs 中的 JS 引擎永远不会倒退。这就是我当前的实现在我的问题中使用for 循环的原因。至少它在所有引擎中都是安全的。我对 webAssembly 一无所知 - 也许是时候学习了。

标签: javascript arrays append


【解决方案1】:

根据 Sebastian 的帮助 cmets,理想的情况是 fast-path optimization 代码如下:

array.push(...data)

V8 现在部署在它检测到 data 为已知类型的可迭代对象的位置,然后采用优化的快捷方式来增长目标数组一次,然后复制数据而不在堆栈上制作中间副本。

但是,显然当前的 V8 并未对这种特定情况应用这样的优化。当我在节点 v14.3 中尝试此操作时:

const targetCnt = 100_000;
const sourceCnt = 100_000_000;

// create initial arrays
let target = new Array(targetCnt);
let source = new Array(sourceCnt);

target.fill(1);
source.fill(2);

let b1 = new Bench().markBegin();
target.push(...source);
b1.markEnd();
console.log(`.push(...source): ${b1.formatNs()}`);

我收到了这个错误:

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

而且,当我将 sourceCnt 减少到 1_000_000 时,我得到了这个错误:

RangeError: Maximum call stack size exceeded

所以,我想优化只适用于文章中提到的其他情况,不适用于这种情况。

因此,似乎直到快速路径优化在所有可能的目标中被认为无处不在并且您确切知道它适用于您的代码中的哪些情况(我想知道它是否会被编入规范中?)并且没有在您的代码中传递一个无法获得如此优惠待遇的未知迭代的危险,也许最好的选择是将您自己的优化迷你版作为一个函数:

 function appendToArray(targetArray, sourceArray) {
     // grow the target
     let targetIndex = targetArray.length;
     let sourceLen = sourceArray.length;
     targetArray.length = targetIndex + sourceLen;
     // copy the data
     for (let sourceIndex = 0; sourceIndex < sourceLen; sourceIndex++, targetIndex++) {
         targetArray[targetIndex] = sourceArray[sourceIndex];
     }
     return targetArray;
 }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-12-27
    • 2022-11-25
    • 2020-10-06
    • 2018-11-25
    • 1970-01-01
    • 2021-02-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多