Christoph
Christoph

Reputation: 51201

Performance of jQuery.grep vs. Array.filter

In a question it was discussed on how jQuery and native JS would perform against each other.

While of course the vanilla solution performs a lot faster because it does not process the whole array I proposed the usage of Array.filter which I was pretty confident would be at least faster than $.grep.

Surprisingly after adding it to the test I was taught a lesson: Testsuite

Edgecases of course have a different outcome.

Anyone having an idea why $.grep is supposed to be over 3 times faster than the native method Arrray.filter?

Edit: I modified the test to use the filter shim from MDN and the results are pretty interesting:

and finally a result like i was hoping it to see in

Upvotes: 39

Views: 23092

Answers (5)

Riek Steenwegen
Riek Steenwegen

Reputation: 21

Isn't your script wrong?

For array.filter you are doing the measurement 1000 times and present it by taken the sum divided by 1000

For JQuery.grep you are doing the measurement 1 time and present it by taken the sum divided by 1000.

That would mean your grep is actually 1000 times slower than the value you use for comparison.

Quick test in firefox gives:

Machine 1:
average filter - 3.864
average grep - 4.472

Machine2:
average filter - 1.086
average grep - 1.314

Quick test in chrome gives:

Machine 1:
average filter - 69.095
average grep - 34.077

Machine2:
average filter - 18.726
average grep - 9.163

Conclusion in firefox (50.0) is a lot faster for your code path, and filter is about 10-15% faster than jquery.grep.

Chrome is extremely slow for your code path, but grep seems to be 50% faster than array.filter here making it 900% slower than the firefox run.

Upvotes: 2

Matas Vaitkevicius
Matas Vaitkevicius

Reputation: 61401

TLDR; Grep is faster by a magnitude... (hint on why can be found here)

It looks to me like .filter forces it's this to Object, checks the callback IsCallable and sets this in it as well as checking for existence of property in each iteration whereas .grep assumes and skips these steps, meaning there is slightly less going on.

Here is the script I used for testing:

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>

Upvotes: -1

MarcoK
MarcoK

Reputation: 6110

As found on this blog post (which also does the same kind of tests):

If you read the documentation for filter, you will see why it's so much slower.

  1. It ignores deleted values and gaps in the array
  2. It optionally sets the execution context of the predicate function
  3. It prevents the predicate function from mutating the data

Upvotes: 17

nicojs
nicojs

Reputation: 2055

I found out something interesting. As explained by MarcoK, $.grep is just a simple implementation with a for loop. Filter is slower in most cases, so the implementation must be different. I think i found the answer:

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)

The native 'filter' is much faster in this case. So i think it iterates the properties rather than the array index.

Now lets get back to 'big' problems ;).

Upvotes: 3

Mathias Bynens
Mathias Bynens

Reputation: 149564

Section 15.4.4.20 of the ECMAScript 5.1 spec defines Array.prototype.filter(callbackfn, thisArg) as follows:

callbackfn should be a function that accepts three arguments and returns a value that is coercible to the Boolean value true or false. filter calls callbackfn once for each element in the array, in ascending order, and constructs a new array of all the values for which callbackfn returns true. callbackfn is called only for elements of the array which actually exist; it is not called for missing elements of the array.

If a thisArg parameter is provided, it will be used as the this value for each invocation of callbackfn. If it is not provided, undefined is used instead.

callbackfn is called with three arguments: the value of the element, the index of the element, and the object being traversed.

filter does not directly mutate the object on which it is called but the object may be mutated by the calls to callbackfn.

The range of elements processed by filter is set before the first call to callbackfn. Elements which are appended to the array after the call to filter begins will not be visited by callbackfn. If existing elements of the array are changed their value as passed to callbackfn will be the value at the time filter visits them; elements that are deleted after the call to filter begins and before being visited are not visited.

That in itself is already a lot of work; a lot of steps that the ECMAScript engine needs to perform.

Then it goes on to say the following:

When the filter method is called with one or two arguments, the following steps are taken:

Let O be the result of calling ToObject passing the this value as the argument. Let lenValue be the result of calling the [[Get]] internal method of O with the argument length. Let len be ToUint32(lenValue). If IsCallable(callbackfn) is false, throw a TypeError exception. If thisArg was supplied, let T be thisArg; else let T be undefined. Let A be a new array created as if by the expression new Array() where Array is the standard built-in constructor with that name. Let k be 0. Let to be 0. Repeat, while k < len Let Pk be ToString(k). Let kPresent be the result of calling the [[HasProperty]] internal method of O with argument Pk. If kPresent is true, then Let kValue be the result of calling the [[Get]] internal method of O with argument Pk. Let selected be the result of calling the [[Call]] internal method of callbackfn with T as the this value and argument list containing kValue, k, and O. If ToBoolean(selected) is true, then Call the [[DefineOwnProperty]] internal method of A with arguments ToString(to), Property Descriptor {[[Value]]: kValue, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true}, and false. Increase to by 1. Increase k by 1. Return A.

The length property of the filter method is 1.

NOTE The filter function is intentionally generic; it does not require that its this value be an Array object. Therefore it can be transferred to other kinds of objects for use as a method. Whether the filter function can be applied successfully to a host object is implementation-dependent.

Some things to note about this algorithm:

  • It prevents the predicate function from mutating the data
  • It optionally sets the execution context of the predicate function
  • It ignores deleted values and gaps in the array

In a lot of cases, none of these things are needed. So, when writing a filter method of your own, most of the time you wouldn’t even bother to perform these steps.

Every ES5.1-compliant JavaScript engine must conform to that algorithm, and must thus perform all those steps every time you use Array#filter.

It should be no surprise that any custom-written method that only performs a part of those steps will be faster :)

If you write your own filter function, chances are it’s not gonna be as complex as the above algorithm. Perhaps you won’t be converting the array to an object at all, as depending on the use case it may not be needed just to filter the array.

Upvotes: 8

Related Questions