Производительность jQuery.grep и Array.filter

В вопросе обсуждалось, как jQuery и native JS будут выполняться друг против друга.

В то время как решение ванили выполняется намного быстрее, потому что оно не обрабатывает весь массив, я предложил использовать Array.filter, который был довольно уверенным, был бы, по крайней мере, быстрее, чем $.grep.

Удивительно, добавив его к тесту, я получил урок: Testsuite

Edgecases, конечно, имеют другой результат.

Любой, у кого есть идея, почему $.grep предполагается более чем в 3 раза быстрее, чем собственный метод Arrray.filter?

Изменить: я изменил тест, чтобы использовать фильтрацию из MDN, и результаты довольно интересны:

  • Chrome: даже прошивка MDN быстрее, чем собственный метод, путь jQuery
  • Firefox: проложить немного медленнее, чем собственный метод, jQuery путь вперед

и, наконец, результат, как я надеялся увидеть его в

  • Internet Explorer: собственный метод является самым быстрым, тогда jQuery, прокладка медленнее (возможно, это только результат IE, а слабый JS-движок...)

Ответ 1

Как найдено на этом сообщении в блоге (который также выполняет те же самые тесты):

Если вы прочитали документацию для filter, вы увидите, почему она так медленнее.

  • Он игнорирует удаленные значения и пробелы в массиве
  • Он может задавать контекст выполнения функции предиката
  • Это предотвращает изменение функции предиката

Ответ 2

Раздел 15.4.4.20 спецификации ECMAScript 5.1 определяет Array.prototype.filter(callbackfn, thisArg) следующим образом:

callbackfn должна быть функцией, которая принимает три аргумента и возвращает значение, которое является принудительным для логического значения true или false. filter вызывает callbackfn один раз для каждого элемента в массив, в порядке возрастания и строит новый массив всех значения, для которых callbackfn возвращает true. callbackfn называется только для элементов существующего массива; он не называется для отсутствующих элементов массива.

Если указан параметр thisArg, он будет использоваться как thisзначение для каждого вызова callbackfn. Если это не предусмотрено, Вместо этого используется undefined.

callbackfn вызывается с тремя аргументами: значением элемента, индекс элемента и пройденный объект.

filter не напрямую мутирует объект, на который он вызывается, но объект может быть мутирован вызовами callbackfn.

Диапазон элементов, обработанных фильтром, устанавливается перед первым вызовом до callbackfn. Элементы, которые добавляются к массиву после вызов для начала фильтра не будет посещаться callbackfn. Если существующие элементы массива меняют свое значение, переданное в callbackfn будет значением в момент, когда фильтр посещает их; элементы, которые удаляются после начала вызова фильтра, и до посещения не посещаются.

Это само по себе уже много работы; много шагов, которые должен выполнить движок ECMAScript.

Затем он говорит следующее:

Когда метод фильтра вызывается с одним или двумя аргументами, выполняются следующие шаги:

Пусть O является результатом вызова ToObject, передающего значение this в качестве аргумент. Пусть lenValue является результатом вызова [[Get]] внутреннего метод O с аргументом length. Пусть len be ToUint32(lenValue). Если IsCallable (callbackfn) является ложным, введите исключение TypeError. Если thisArg было предоставлено, пусть T будет thisArg; иначе пусть T будет undefined. Пусть A быть новым массивом, созданным, как будто выражением new Array(), где Array является стандартным встроенным конструктором с таким именем. Пусть k равно 0. Пусть равным 0. Повторите, в то время как k < len Пусть Pk - ToString (k). Пусть kPresent результат вызова внутреннего метода O [[HasProperty]] аргумент Pk. Если kPresent истинно, то пусть kValue будет результатом вызывая внутренний метод [[Get]] O с аргументом Pk. Позволять выбранным является результатом вызова внутреннего метода [[Call]] callbackfn с T как это значение и список аргументов, содержащий kValue, k и O. Если ToBoolean (выбрано) истинно, то Call [[DefineOwnProperty]] внутренний метод A с аргументами ToString (to), дескриптор свойства {[[Value]]: kValue, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true} и false. Увеличить на 1. Увеличить k на 1. Вернуть A.

Свойство длины метод фильтра равен 1.

ПРИМЕЧАНИЕ. Функция фильтра преднамеренно является общей; он не требует что его значением является объект Array. Поэтому это может быть переносится на другие виды объектов для использования в качестве метода. Является ли функция фильтра может быть успешно применена к объекту хоста зависит от реализации.

Некоторые примечания об этом алгоритме:

  • Это предотвращает изменение функции предиката
  • Он может задавать контекст выполнения функции предиката
  • Он игнорирует удаленные значения и пробелы в массиве

Во многих случаях ни одна из этих вещей не требуется. Таким образом, при написании собственного метода filter, большую часть времени вы даже не потрудились выполнять эти шаги.

Каждый механизм, совместимый с ES5.1, должен соответствовать этому алгоритму и должен выполнять все эти шаги каждый раз, когда вы используете Array#filter.

Неудивительно, что любой пользовательский метод, который выполняет только часть этих шагов, будет быстрее:)

Если вы пишете свою собственную функцию filter, скорее всего, она не будет такой сложной, как описанный выше алгоритм. Возможно, вы не будете преобразовывать массив в объект вообще, так как в зависимости от варианта использования он может не понадобиться только для фильтрации массива.

Ответ 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)

В этом случае собственный "фильтр" намного быстрее. Поэтому я думаю, что он выполняет итерацию свойств, а не индекс массива.

Теперь вернемся к "большим" проблемам;).

Ответ 4

Разве ваш script не прав?

Для 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

Быстрый тест в хроме дает:

Machine 1:
average filter - 69.095
average grep - 34.077

Machine2:
average filter - 18.726
average grep - 9.163

Заключение в firefox (50.0) намного быстрее для вашего кода, а фильтр примерно на 10-15% быстрее, чем jquery.grep.

Chrome очень медленный для вашего кода, но grep, кажется, на 50% быстрее, чем array.filter, делая это на 900% медленнее, чем запуск firefox.

Ответ 5

TL;DR; Grep быстрее на величину... (подсказка о том, почему можно найти здесь)

Мне кажется, что .filter заставляет его это Object, проверяет callback IsCallable и устанавливает это в нем, а также проверяет существование свойства в каждой итерации, тогда как .grep предполагает и пропустит эти шаги, что означает, что происходит немного меньше.

Вот script, который я использовал для тестирования:

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>