JavaScript

индекс
246,46

Архитектура YASS. Часть 3: проблема выбора

Это третья статья из цикла, посвященного разбору практических методов, заложенных в основу YASS. Первая статья была про модульное построение, вторая — про логику выбора CSS-селектора и организацию циклов.

Условное ветвление



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

var a = 1,
	b = 2,
	c = 3;
if(a == 1) {
	if (b == 2) {
		if (c == 3) {
			...
		}
	}
}


Это, самое интересное, работает так же быстро, как и совмещенный if:

if(a == 1 && b == 2 && c == 3){
	...
}


Однако последний немного меньше по размеру. Если не стоит задача минимального размера кода, то для улучшения читаемости стоит использовать первый вариант. Если же мы минимизируем все, то можно рассмотреть возможность использования if-then-else выражения. Но нужно иметь в виду, что производительность таких конструкций:

var v = a == 1 ? b == 2 ? c == 3 ? 1 : 0 : 0 : 0;


примерно на 10-50% меньше, чем у обычного ветвления, рассмотренного чуть выше.

В том случае, когда все переменные у нас числовые, то проверка равенства их суммы заданной будет выполняться на 5–10% быстрее:

if (a + b + c == 6) {
	...
}


Если же нам нужно проверить просто существование переменных и их неотрицательность (т.е. то, что переменные не undefined, не NaN, не null, не '' и не 0), то следующий вариант будет работать еще на 5–10% быстрее, чем предыдущий случай (и на 10–20% быстрее, чем самый первый пример):

if (a && b && c) {
	...
}


Очень часто нам нужно проверить что-то сложнее, чем просто число. Например, совпадение строки с заданной или равенство объектов. В этом случае нам просто необходимо следующее сравнение:

var a = 1,
	b = 2,
	c = '3';
if (a == 1 && b == 2 && c === '3') {
	...
}


Здесь мы используем сравнение без приведения типов ===, которое в случае нечисловых переменных работает быстрее обычного сравнения на 10–20%.

Выбор в зависимости от строки



Достаточно часто нам нужно выбрать одну из условных ветвей, основываясь на заданной строке. Обычно для этого используется либо методы объекта RegExp (exec, test), либо строковые методы (match, search, indexOf). Если нам нужно просто проверить соответствие строки какому-то регулярному выражению, то лучше всего для этого подойдет именно test:

var str = 'abc',
	regexp = new RegExp('abc');
if (regexp.test(str)) {
	...
}


Такая конструкция отработает на 40% быстрее, чем аналогичный exec:

if (regexp.exec(str)[1]) {
	...
}


Строковый метод match аналогичен методу exec у создаваемого объекта RegExp, но работает на 10–15% быстрее в случае простых выражений. Однако метод search работает чуть медленнее (5–10%), чем test, потому что последний не возвращает найденную подстроку.

В том случае, если регулярное выражение требуется «на один раз», то подойдет более быстрая (примерно на 10% относительно варианта с инициализацией нового объекта) запись:

if (/abc/.test(str)) {
	...
}


Если же, наконец, нам нужно проверить просто нахождение подстроки в заданной строке, то тут бесспорным лидером будет именно indexOf, который работает в 2 раза быстрее разбора регулярных выражений:

if (str.indexOf('abc') != -1) {
	...
}


Точное соответствие и хэши



Давайте теперь рассмотрим следующий вариант регулярного выражения: /a|b|c/. В этом случае нам нужно проверить в заданной строке наличие одного из возможных вариантов (или равенство строки этому варианту). В случае точного соответствия быстрее регулярного выражения (на 50%) будет проверка строки как ключа какого-либо хэша:

var hash = {'a':1, 'b':1},
	str = 'a';
if (h[str]) {
	...
}


Быстрее (на 20%) такого хэша будет только точная проверка строки на определенные значения:

if (str === 'a' || str === 'b'){
	...
}


Если рассмотреть 3 конструкции: вложенный if, switch с соответствующими значениями и проверка значений в хэше, — то стоит отметить следующую интересную особенность. При небольшом уровне вложенности if (если всего значений немного, или мы очень часто выходим по первому-второму значению), конструкции if и switch обгоняют по производительности хэш примерно на 10%. Если же у нас значений много, и они все примерно равновероятны, то хэш отрабатывает в общем случае быстрее уже на 20%. Это в равной степени относится как к установлению значений переменных, так и к вызову функций. Т, е. для создания ветвления с вызовом функций лучше всего использовать именно хэш.

Возвращаясь к YASS. При анализе CSS-селектора можно выделить несколько подзадач, описываемых как «проблема выбора»:

  • Ветвление для простого случая выполнено при помощи проверки входной строки через test:
    if (/^[\w[:#.][\w\]*^|=!]*$/.test(selector)) {
    	...
    } else {
    	...
    }
  • Ветвление для простейшего случая (когда нам нужно выбрать по идентификатору или по классу). Поскольку всего значений у нас 5, и 3 из них относительно равновероятны (выбор по классу, по идентификатору или по тегу), то используется switch:
    switch (firstLetter) {
    	case '#':
    		...
    		break;
    	case '.':
    		...
    		break;
    	case ':'
    		...
    		break;
    	case '[':
    		...
    		break;
    	default:
    		...
    		break;
    }
  • Абсолютно аналогичную картину мы наблюдаем для выбора правильного отношения «родитель-ребенок» (>, +, ~, ): тут тоже только switch:
    switch (ancestor) {
    	case ' ':
    		...
    		break;
    	case '~':
    		...
    		break;
    	case '+':
    		...
    		break;
    	case '>':
    		...
    		break;
    }
  • Наконец, выбор соответствующей проверочной функции для child-модификаторов (first-child, last-child, nth-child, и т.д.) и выбор проверочной функции для атрибутов (~=, *=, = и т.д.) осуществляется уже через специальные хэши:
    _.attr = {'': ... , '=': ... , '&=': ... , '^=': ... ... }



Итоговая таблица



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

Проверка числового значения Обычное сравнение (==)
Проверка нескольких числовых значений Сравнение их суммы
Проверка, что число не нуль, или проверка на существование Проверка отрицания к заданной переменной (!)
Разбор строки и выделение частей в массив String.match(RegExp) или RegExp.exec(String)
Проверка строки на соответствие регулярному выражению RegExp.test(String)
Проверка строки на наличие подстроки String.indexOf(String)
Проверка строки на точное соответствие (либо соответствие одному из набора значений) if без приведения типов (===)
Выбор в зависимости от точного значения (значений 1–2) Условная конструкция if
Выбор в зависимости от точного значения (значений 3–8) switch
Выбор в зависимости от точного значения (значений больше 8) Хэш с ключами, соответствующим значениям


Наверное, данную таблицу можно дополнить еще некоторыми случаями или же обратиться к статье, посвященной производительности простых конструкций в JavaScript и сделать соответствующие выводы.

Продолжение следует...
+23
25 января 2009, 19:26
19

комментарии (33)

+2
tenshi #
хэши лучше проверять так: hash[ name ] === 1
иначе можно напороться на одно из полей, унаследованных из глобального прототипа.
+2
Zeroglif #
>Если же нам нужно проверить просто существование переменных и их неотрицательность.
Несуществующая переменная конечно же не сможет выбросить ошибку в вашем славном примере.

>Здесь мы используем сравнение без приведения типов ===, которое в случае нечисловых переменных работает быстрее обычного сравнения на 10–20%.
Типы в примере совпадают, конвертации значений нет, в этом случае что строгое, что нестрогое сравнение работает одинаково. При разных типах строгое сравнение тут же выкинет false, чего тут скорость сравнивать-то.

>Такая конструкция отработает на 40% быстрее, чем аналогичный exec
test — эквивалент RegExp.prototype.exec(string) != null, ой, не верю в 40%… В целом, итоговой таблице сильно не хватает столбца со ссылками на тесты в разных движках, чтоб своими глазами увидеть эти 10-20-30-40-50 процентов… ;)

0
galexey #
.test не запоминает найденные подстроки в круглых скобках (переменные RexExp.$1..n), отсюда и экономия, тоже самое в .match
К тому же вообще не запоминает найденную строку.
Для ускорения регулярок можно «не запоминающие скобки» делать /(?:\d|-)*/ вместо «запоминающих» /(\d|-)*/
Это лишь пример, понятно что регулярка глупая.

Судя по всему .match любые скобки не запоминяет в отличии от .exec? что объясняет разницу производительности.
0
Zeroglif #
При работе 'test' свойства у RegExp, и у RegExp instance создаются/обновляются также, как и при работе 'exec', вспомните часто встречающуюся ошибку «test работает через раз», где речь идёт об обновляемом свойстве 'lastIndex'. Это касается и RegExp.$n и проч.

Проигрыш в производительности 'exec' связан скорее всего с необходимостью при нахождении совпадений вернуть определённый массив плюс заодно обвешать его свойствами ('input', 'index' etc). Если совпадений нет, то оба метода работают одинаково, возвращая null или false (6-ой пункт 15.10.6.2 ECMA-262).
0
sunnybear #
1. Можно подробнее?
2. Если значения разные, то === отработает быстрее
www.rockstarapps.com/samples/performance/
3. В статье приведены все базовые примеры. Можете поделиться ссылкой, где написано, что
test — эквивалент RegExp.prototype.exec(string) != null

У меня сомнения, что в IE так

Цифры можно получить из примеров в статье. Это совсем не сложно. Потом еще есть
dromaeo.com/?dromaeo
0
sunnybear #
+1
Zeroglif #
1. if (a && b) {} — если 'a' — не существует, то чего будет?
2. разницы нет в алгоритме для значений одного типа; по ссылке не нашёл нужного.
3. ECMA-262 пункт 15.10.6.3.

В общем, тестов нет, жаль, половина из написанного была бы опровергнута, а другая половина вышла бы разнонаправленной. ;)
НЛО прилетело и опубликовало эту надпись здесь
0
sunnybear #
2. под IE прогоните — разница есть
3. IE так сильно придерживается ECMA? хмм, лол :)
Что стоит прогнать предложенные варианты кода в цикле по 50000 раз и просто убедиться?
0
Zeroglif #
2. Мило, от обобщений перевели стрелки на один движок, какой IE? 6-ой, 8-ой? что прогнать? статья льёт воду — там на 20% медленнее, здесь на 10-40%, где там? в каком движке? где тесты? я со всем готов согласиться, если пройдусь по вашим тестам и получу похожее.

3.IE близко придерживается экмы, не сильно меньше других, есть разночтения у всех, есть собственная оптимизация (никакого к экме не имеющая отношения), есть много чего…

что стоит прогнать
Прогоню с удовольствием, выкладывайте.
0
sunnybear #
я конкретно запускал под IE6/7. 8 меня не очень волнует — пока еще он долю наберет.

Как только я соберу окружение с матрицей всех вышеуказанных тестов — дам ссылку. Прежде, чем делать какие-либо выводы каждый случае прогонял в Firebug (иногда и в IE7).
0
sunnybear #
большинство тестов приведено здесь
webo.in/tests/yass-conditions-tests/
0
Zeroglif #
Спасибо. Теперь очевиднее. Проверил в IE6/FF3.0.1/Op9.63/Chr0.2. Между строгим и нестрогим сравнением ожидаемо разницы нет(туда-сюда летающие 1,2,3,4 процента на таких больших оборотах во всех случаях опускаю), та же самая картина со всеми условиями — простыми и вложенными (за исключением Оперы, у которой '&&' медленнее на 30%). C чем вы точно попали в точку, так это с RegExp, метод 'exec' везде кроме Хрома работает существенно медленнее, у IE дополнительно можно отметить быстрый 'search'.
–1
sunnybear #
неужели небеса разверзлись? :)

Стоит провести замеры в ряд тестов 10-15 — тогда разница становится очевиднее, и выплывают указанные 5-20%. Просто у браузеров большой разброс — ясными бывают только значительные разрывы в производительности.
0
Zeroglif #
ОК, каждый может сам сделать свои выводы, проверив в вышеуказанных версиях браузеров, я сделал для себя такие:

1. строгое сравнение значений одного типа по стандарту ничем (буква в букву) не отличается от нестрогого сравнения, приведённый тест это лишний раз подтверждает;

2. наглядный 'if-else' не уступает в скорости ни '&&', ни ":?", соответственно, использовать последние ради одной только скорости, а не ради игры на 'expression vs. statement' или ради сокращения кода — глупо;

3. метод 'test' несмотря на стандарт очевидно оптимизирован, метод 'exec', наоборот, не оптимизирован (кроме хитрого Хрома), 'exec' и 'test' работают одинаково (по стандарту) при отсутствии совпадений (выброс null/false), при их наличии производительность 'exec' падает (видимо) за счёт возвращаемого значения.

И общий вывод: как правило, ссылка непосредственно на сам тест в разы полезнее, чем проза в стиле «нужно иметь в виду, что производительность таких конструкций бла-бла-бла примерно на 10-50% меньше, чем бла-бла-бла». Когда примерно туда-сюда, когда большие разбросы, когда не указаны движки (версии), когда нет обоснований/предположений, а еcть одни обобщения на базе тестов в Firebug!!!, то это вообще ни о чём. Начитается эдакого доверчивая душа и ради гонок откажется от 'if-else', от нестрогого сравнения, начнёт делать всякие глупости вроде «быстрого» сложения значений или проверки на существование в стиле 'if(x)'…

p.s. за тест спасибо ;)
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
0
sunnybear #
еще один хороший комментарий. Не помню, зачем кэшировался этот вызов. Но сейчас создавать такую переменную нет нужды — у нас всего одно обращение к string.charAt(0)
НЛО прилетело и опубликовало эту надпись здесь
0
sunnybear #
проверка тега оказалась на 40% быстрее даже в IE
НЛО прилетело и опубликовало эту надпись здесь
+1
whoozle #
Сумма говорите? :) a + b + c == 6 для 2 2 2, например. :)
0
sunnybear #
это было актуально для нескольких indexOf :)
+1
Zibx #
Улучшение читаемости стопки ифов перед одним со сложным условием — крайне спорный вердикт.
0
sunnybear #
попробуйте записать в строчку десяток условий :). Например,
if ((!id || item.id === id) && (!klass || klass.test(item.className)) && (!attr || (_.attr[eql] && (_.attr[eql](item, attr, single[6]) || (attr === 'class' && _.attr[eql](item, 'className', single[6]))))) && !item.yeasss && !(_.modificators[modificator] ? _.modificators[modificator](item, ind) : modificator))
0
Zibx #
Пробовал. А вот разворачивать вот это вот в большущее дерево даже бы ради эксперимента над нервами не стал.
0
sunnybear #
:) кому как удобнее. Главное, что работает одинаково быстро
0
pepelsbey #
Ищу в подстроке обычно вот так: if (str.indexOf('abc')+1).
Если вернётся что угодно, то true, если -1 — то выйдет 0, т.е. false.

Как у такого варианта с производительностью по сравнению с !=-1?
0
sunnybear #
было одинаково, когда я проверял месяца два назад. Но кода чуть меньше :)
0
DenVdmj #
в теории — медленнее, так как 0 !== false
смешно все это ))
+1
DenVdmj #
Раз уж вы «такты» считаете, можно убрать регулярку для ие в поиске по имениКласса.
то есть заменить вот это:

     klass = new RegExp('(^| +)' + klass + '($| +)');
     var nodes = root.getElementsByTagName('*'),
         i = 0,
         node;
     while (node = nodes[i++]) {
         if (klass.test(node.className)) {
             sets[idx++] = node;
         }
     }

вот на такое:

     klass = ' ' + klass + ' ';
     var nodes = root.getElementsByTagName('*'),
         i = 0,
         node;
     while (node = nodes[i++]) {
         if ((' ' + node.className + ' ').indexOf(klass)) {
             sets[idx++] = node;
         }
     }
0
sunnybear #
о спасибо, долго думал, как избежать регулярки :)
0
DenVdmj #
Кстати, я как то в порядке эксперимента пробовал устанавливать временные styleSheets для поиска по css-селектору, в лоб это работало очень медленно, но может это будет хорошим способом узнать актуальность выборки из кэша:

(function (){

    var css = CSS.add(), // закроссбраузенный элемент коллекции document.styleSheets
        defaultView = document.defaultView;

    var isActualNodeBySelector = function (selectorFromCache, nodeFromCache) {
        css.insertRule('*{cursor:default}', 0);
        css.insertRule(selectorFromCache + '{cursor:crosshair}', 0);
        if (( defaultView ?
                defaultView.getComputedStyle(nodeFromCache, null) :
                node.currentStyle
            )['cursor'] == 'crosshair'
        ) {
            // .......... выборка из кэша актуальна
        }
        css.deleteRule(0);
        css.deleteRule(1);
    }
})()

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.