为什么array.prototype.slice()在子类数组上这么慢? [英] Why is array.prototype.slice() so slow on sub-classed arrays?

查看:77
本文介绍了为什么array.prototype.slice()在子类数组上这么慢?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在节点v14.3.0中,我发现(在对非常大的数组进行一些编码工作时),对数组进行子类化可能导致 .slice()的速度降低20倍.虽然,我可以想象可能会对非子类化数组进行一些编译器优化,但我根本不了解的是 .slice()如何比手动复制元素慢2倍以上从一个数组到另一个数组这对我完全没有意义.有人有主意吗?这是一个错误还是有某些方面可以/可以解释呢?

In node v14.3.0, I discovered (while doing some coding work with very large arrays) that sub-classing an array can cause .slice() to slow down by a factor 20x. While, I could imagine that there might be some compiler optimizations around a non-subclassed array, what I do not understand at all is how .slice() can be more than 2x slower than just manually copying elements from one array to another. That does not make sense to me at all. Anyone have any ideas? Is this a bug or is there some aspect to this that would/could explain it?

为了进行测试,我创建了一个100,000,000个单位数组,其中填充了越来越多的数字.我使用 .slice()制作了该数组的副本,然后通过遍历该数组并将值分配给新数组来手动制作了副本.然后,我针对 Array 和我自己的空子类 ArraySub 运行了这两个测试.这是数字:

For the test, I created a 100,000,000 unit array filled with increasing numbers. I made a copy of the array with .slice() and I made a copy manually by then iterating over the array and assigning values to a new array. I then ran those two tests for both an Array and my own empty subclass ArraySub. Here are the numbers:

Running with Array(100,000,000)
sliceTest: 436.766ms
copyTest: 4.821s

Running with ArraySub(100,000,000)
sliceTest: 11.298s
copyTest: 4.845s

手动复制在两种方式上几乎相同. .slice()副本在子类上慢26倍,比手动副本慢2倍以上.为什么会这样?

The manual copy is about the same both ways. The .slice() copy is 26x slower on the sub-class and more than 2x slower than the manual copy. Why would that be?

而且,这是代码:

// empty subclass for testing purposes
class ArraySub extends Array {

}

function test(num, cls) {
    let name = cls === Array ? "Array" : "ArraySub";
    console.log(`--------------------------------\nRunning with ${name}(${num})`);
    // create array filled with unique numbers
    let source = new cls(num);
    for (let i = 0; i < num; i++) {
        source[i] = i;
    }

    // now make a copy with .slice()
    console.time("sliceTest");
    let copy = source.slice();
    console.timeEnd("sliceTest");

    console.time("copyTest");
    // these next 4 lines are a lot faster than this.slice()
    const manualCopy = new cls(num);
    for (let [i, item] of source.entries()) {
        manualCopy[i] = item;
    }
    console.timeEnd("copyTest");
}

[Array, ArraySub].forEach(cls => {
    test(100_000_000, cls);
});

仅供参考,运行时 jsperf.com测试也有类似的结果在Chrome浏览器中.在Firefox中运行jsperf表现出相似的趋势,但与Chrome相比并没有太大差异.

FYI, there's a similar result in this jsperf.com test when run in the Chrome browser. Running the jsperf in Firefox shows a similar trend, but not as much of a difference as in Chrome.

推荐答案

V8开发人员在此处.您所看到的是相当典型的:

V8 developer here. What you're seeing is fairly typical:

针对常规数组的内置 .slice()函数经过了充分优化,采用了各种快捷方式和特化技术(甚至甚至可以使用 memcpy 只包含数字的数组,因此使用CPU的向量寄存器一次复制多个元素!).这使其成为最快的选择.

The built-in .slice() function for regular arrays is heavily optimized, taking all sorts of shortcuts and specializations (it even goes as far as using memcpy for arrays containing only numbers, hence copying more than one element at a time using your CPU's vector registers!). That makes it the fastest option.

在自定义对象(如子类数组,或只是 let obj = {length:100_000_000,foo:"bar",...} <)上调用自定义对象上的 Array.prototype.slice /code>)不符合快速路径的限制,因此由内置的 .slice 的通用实现来处理,该实现虽然慢得多,但可以处理您扔给它的任何东西.这不是JavaScript代码,因此它不会收集类型反馈,也无法动态优化.好处是,无论如何,它每次都会为您提供相同的性能.这种性能实际上并不差劲,与其他选择相比,它只是苍白无力.

Calling Array.prototype.slice on a custom object (like a subclassed array, or just let obj = {length: 100_000_000, foo: "bar", ...}) doesn't fit the restrictions of the fast path, so it's handled by a generic implementation of the .slice builtin, which is much slower, but can handle anything you throw at it. This is not JavaScript code, so it doesn't collect type feedback and can't get optimized dynamically. The upside is that it gives you the same performance every time, no matter what. This performance is not actually bad, it just pales in comparison to the optimizations you get with the alternatives.

您自己的实现(如所有JavaScript函数一样)获得了动态优化的好处,因此尽管自然不会立即内置任何奇特的快捷方式,但它可以适应当前的情况(例如对象的类型)它正在运行).这就解释了为什么它比通用内置速度更快,以及为什么它在两个测试用例中都能提供一致的性能.也就是说,如果您的方案更加复杂,则可能会污染此函数的类型反馈,使其变得比通用内置函数慢.

Your own implementation, like all JavaScript functions, gets the benefit of dynamic optimization, so while it naturally can't have any fancy shortcuts built into it right away, it can adapt to the situation at hand (like the type of object it's operating on). That explains why it's faster than the generic builtin, and also why it provides consistent performance in both of your test cases. That said, if your scenario were more complicated, you could probably pollute this function's type feedback to the point where it becomes slower than the generic builtin.

使用source.entries() [i,item]方法,您可以非常简洁地接近 .slice()的规范行为.一些开销;一个普通的旧 for(让i = 0; i< source.length; i ++){...} 循环大约快一倍,即使您添加 if(i在源代码中)检查,以在每次迭代中反映出 .slice()的"HasElement"检查.

With the [i, item] of source.entries() approach you're coming close to the spec behavior of .slice() very concisely at the cost of some overhead; a plain old for (let i = 0; i < source.length; i++) {...} loop would be about twice as fast, even if you add an if (i in source) check to reflect .slice()'s "HasElement" check on every iteration.

更笼统地说:您可能会看到许多其他JS内建函数具有相同的通用模式-这是在动态语言的优化引擎上运行的自然结果.尽管我们希望一切都变得快速,但有两种原因导致这种情况不会发生:

More generally: you'll probably see the same general pattern for many other JS builtins -- it's a natural consequence of running on an optimizing engine for a dynamic language. As much as we'd love to just make everything fast, there are two reasons why that won't happen:

(1)实现快速路径是有代价的:开发(和调试)它们需要花费更多的工程时间;JS规范更改时,需要花费更多时间来更新它们;它造成了大量的代码复杂性,这些复杂性很快变得难以管理,从而导致进一步的开发速度下降和/或功能错误和/或安全错误;将它们运送到我们的用户需要更多的二进制文件大小,并且需要更多的内存来加载此类二进制文件;在开始任何实际工作之前,要花费更多的CPU时间来决定采用哪个路径;等等.由于这些资源都不是无限的,因此我们总是必须选择将其用于何处,而不是在何处.

(1) Implementing fast paths comes at a cost: it takes more engineering time to develop (and debug) them; it takes more time to update them when the JS spec changes; it creates an amount of code complexity that quickly becomes unmanageable leading to further development slowdown and/or functionality bugs and/or security bugs; it takes more binary size to ship them to our users and more memory to load such binaries; it takes more CPU time to decide which path to take before any of the actual work can start; etc. Since none of those resources are infinite, we'll always have to choose where to spend them, and where not.

(2)速度从根本上讲与灵活性不符.快速路径之所以快速,是因为它们会做出限制性假设.尽可能多地扩展快速路径,以便将其应用于尽可能多的情况是我们所做的一部分,但是对于用户代码而言,构造一种情况总是很容易的,使得无法采用快速实现的快捷方式快速路径.

(2) Speed is fundamentally at odds with flexibility. Fast paths are fast because they get to make restrictive assumptions. Extending fast paths as much as possible so that they apply to as many cases as possible is part of what we do, but it'll always be easy for user code to construct a situation that makes it impossible to take the shortcuts that make a fast path fast.

这篇关于为什么array.prototype.slice()在子类数组上这么慢?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆