【问题标题】:Performance of jQuery.grep vs. Array.filterjQuery.grep 与 Array.filter 的性能对比
【发布时间】:2013-01-16 20:01:45
【问题描述】:

question 中讨论了 jQuery 和原生 JS 如何相互对抗。

当然,香草解决方案的执行速度要快得多,因为它不处理整个数组,但我建议使用 Array.filter,我非常有信心至少会比 $.grep 快。

令人惊讶的是,在将其添加到测试中后,我学到了一课:Testsuite

Edgecases当然会有不同的结果。

有人知道为什么 $.grep 应该比原生方法 Arrray.filter 快 3 倍以上?

编辑:我修改了测试以使用filter shim from MDN,结果非常有趣:

  • Chrome:即使是 MDN shim 也比原生方法快,jQuery 遥遥领先
  • Firefox:shim 比原生方法慢一点,jQuery 领先

最后是我希望看到的结果

  • Internet Explorer: native 方法是最快的,然后是 jQuery,shim 是最慢的(也许这只是 IE 比较弱的 JS 引擎的结果......)

【问题讨论】:

    标签: javascript jquery performance


    【解决方案1】:

    如在 this blog post 上发现的(它也进行相同类型的测试):

    如果您阅读了filter 的文档,您就会明白为什么它这么慢。

    1. 它会忽略数组中已删除的值和间隙
    2. 它可以选择设置谓词函数的执行上下文
    3. 它可以防止谓词函数改变数据

    【讨论】:

    • 这不是Array.filter 实现,它只是一个后备。 Array.filter 是在 JS 引擎(C++)中实现的原生方法。
    • 仍然:为什么原生过滤方法明显比jQuery.grep慢?
    • 更新了我的答案(很抱歉造成混淆)
    • 也更新了我的答案...... JS-shim 比原生方法表现更好......为什么???
    • 在 FF18 中,native 的性能略好于 shim,但 jQuery 仍然快 3 倍。
    【解决方案2】:

    Section 15.4.4.20 of the ECMAScript 5.1 spec 定义Array.prototype.filter(callbackfn, thisArg) 如下:

    callbackfn 应该是一个接受三个参数和 返回一个可强制转换为布尔值 true 的值或 falsefilter 为每个元素调用一次 callbackfn 数组,按升序排列,并构造一个包含所有 callbackfn 返回 true 的值。 callbackfn 被调用 仅适用于实际存在的数组元素;它不叫 用于数组的缺失元素。

    如果提供了thisArg 参数,它将用作this 每次调用 callbackfn 的值。如果没有提供, 改为使用undefined

    callbackfn 使用三个参数调用:元素的值, 元素的索引,以及被遍历的对象。

    filter 不会直接改变调用它的对象,但是 该对象可能会因调用callbackfn而发生变异。

    filter处理的元素范围是在第一次调用之前设置的 到callbackfn。后追加到数组中的元素 callbackfn 不会访问过滤器开始的调用。如果存在 数组的元素在传递给时更改了它们的值 callbackfn 将是过滤器访问它们时的值; 在调用过滤器开始之后和之前删除的元素 被访问的未被访问。

    这本身就已经是很多工作了; ECMAScript 引擎需要执行的许多步骤。

    然后它继续说:

    当使用一个或两个参数调用过滤器方法时, 采取以下步骤:

    O 成为调用ToObject 的结果,传递this 值作为 争论。 让lenValue 成为调用[[Get]] 内部的结果 O 的方法,参数为 length。 让len 成为ToUint32(lenValue)。 如果 IsCallable(callbackfn) 为 false,则抛出 TypeError 异常。如果 thisArg 已提供,设 T 为 thisArg;否则令 T 未定义。让一个 是一个新数组,就好像由表达式 new Array() where Array 是具有该名称的标准内置构造函数。让 k 为 0。让 为 0。重复,而 k

    长度属性 过滤方式为1。

    注意过滤器函数是故意通用的;它不需要 它的 this 值是一个 Array 对象。因此可以 转移到其他种类的对象作为方法使用。是否 过滤器功能可以成功地应用于宿主对象是 依赖于实现。

    关于这个算法的一些注意事项:

    • 它可以防止谓词函数改变数据
    • 它可以选择设置谓词函数的执行上下文
    • 它会忽略数组中已删除的值和间隙

    在很多情况下,这些东西都不需要。因此,在编写自己的 filter 方法时,大多数时候您甚至都不会费心执行这些步骤。

    每个符合 ES5.1 的 JavaScript 引擎都必须符合该算法,因此每次使用 Array#filter 时都必须执行所有这些步骤。

    毫不奇怪,任何只执行部分步骤的自定义编写方法都会更快:)

    如果您编写自己的 filter 函数,它可能不会像上述算法那么复杂。也许您根本不会将数组转换为对象,因为根据用例,可能不需要仅仅过滤数组。

    【讨论】:

    • 好吧,在 chrome 中,即使是完全符合 ES5-spec 的 js-shim 也比原生函数快...
    • @Christoph 它并没有确认完全符合 ES 规范。我只看了几秒钟的代码,就发现它的作用与 ES 规范中的定义不同。第 4 步:“如果 IsCallable(callbackfn)false,则抛出 TypeError 异常。”垫片通过使用typeof 来检查这一点,这与规范中的IsCallable 算法不同。最终结果可能相同,但它是一个完全不同的代码路径,性能成本可能不同。
    【解决方案3】:

    我发现了一些有趣的东西。正如 MarcoK 所解释的,$.grep 只是一个带有 for 循环的简单实现。过滤器在大多数情况下速度较慢,因此实现必须不同。我想我找到了答案:

    function seak (e) { return e === 3; }
    
    var array = [1,2,3,4,5,6,7,8,9,0], i, before;
    array[10000] = 20; // This makes it slow, $.grep now has to iterate 10000 times.
    before = new Date();
    
    // Perform natively a couple of times.
    for(i=0;i<10000;i++){
        array.filter(seak);
    }
    
    document.write('<div>took: ' + (new Date() - before) + '</div>'); // took: 8515 ms (8s)
    
    before = new Date();
    
    // Perform with JQuery a couple of times
    for(i=0;i<10000;i++){
        $.grep(array, seak);
    }
    document.write('<div>took: ' + (new Date() - before) + '</div>'); // took: 51790 ms  (51s)
    

    在这种情况下,本机“过滤器”要快得多。所以我认为它迭代属性而不是数组索引。

    现在让我们回到“大”问题;)。

    【讨论】:

    • 这绝不是一个答案,而是一个模糊的假设。当然,数组索引只是数组对象的普通属性,但您必须通过括号表示法访问它们。因此,谈论属性或索引是没有意义的——它是一样的。唯一的区别是Array.prototype.filter 忽略了数组中的间隙,因此只调用了 11 次过滤函数,而grep 调用了 10001 次。
    • 我也无法确认你的时间,grep 很慢,但只能达到原生版本的 50% ...jsperf.com/grep-vs-pure-js/7
    【解决方案4】:

    TLDR;Grep 的速度要快一个数量级...(提示为什么 can be found here

    在我看来,.filter 强制它是对象,检查 回调 IsCallable 并在其中设置它以及检查 每次迭代中都存在属性,而 .grep 假设和 跳过这些步骤,这意味着发生的事情略少。

    这是我用于测试的脚本:

    function test(){
    var array = [];
    for(var i = 0; i<1000000; i++)
    {
    array.push(i);
    }
    
    var filterResult = []
    for (var i = 0; i < 1000; i++){
    var stime = new Date();
    var filter = array.filter(o => o == 99999);
    filterResult.push(new Date() - stime);
    }
    
    var grepResult = [];
    var stime = new Date();
    var grep = $.grep(array,function(i,o){
    return o == 99999;
    });
    grepResult.push(new Date() - stime);
    
    $('p').text('average filter - '+(filterResult.reduce((pv,cv)=>{ return pv +cv},0)/1000))
    $('div').text('average grep - '+(grepResult.reduce((pv,cv)=>{ return pv + cv},0)/1000))
    }
    test();
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    <p></p>
    <div></div>

    【讨论】:

    • 毫无疑问,它更快,但主要问题仍未得到解答 - 为什么? ;)
    【解决方案5】:

    你的脚本是不是错了?

    对于array.filter,您进行了 1000 次测量,并通过总和除以 1000 来呈现它

    对于JQuery.grep,您进行了 1 次测量,并以总和除以 1000 的方式呈现。

    这意味着您的 grep 实际上比您用于比较的值慢 1000 倍。

    Firefox 中的快速测试给出:

    Machine 1:
    average filter - 3.864
    average grep - 4.472
    
    Machine2:
    average filter - 1.086
    average grep - 1.314
    

    chrome 中的快速测试给出:

    Machine 1:
    average filter - 69.095
    average grep - 34.077
    
    Machine2:
    average filter - 18.726
    average grep - 9.163
    

    Firefox (50.0) 中的结论对于您的代码路径来说要快得多,并且过滤器比 jquery.grep 快 10-15%。

    Chrome 对于您的代码路径来说非常慢,但 grep 似乎比 array.filter 快 50%,这使得它比 firefox 运行慢 900%。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-03-14
      • 2012-03-06
      相关资源
      最近更新 更多