Производительность 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>