Статьи

Перевод: Николай Мациевский aka sunnybear
Опубликована: 1 апреля 2008

Оптимизируем JavaScript: насколько ресурсоемки цепочки вызовов?

Примечание: ниже перевод статьи "JavaScript optimization, are chained calls expensive?". В ней автор тестирует, насколько медленнее производятся цепочки вызовов функций по сравнению с их кешированными аналогами. В конце приведены результаты моих тестов производительности.

Sree Kotay в своем блоге оставил небольшую заметку о JavaScript. (Если вас интересуют технические подробности, я рекомендую ознакомиться с презентацией Simon Willison "A (Re)-Introduction to JavaScript".) В своем блоге Sree пишет следующее:

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

for (i=0; i < 100; i++) a.b.c.d(v);

...ЗНАЧИТЕЛЬНО медленнее, по крайней мере, в JavaScript, чем:

var f=a.b.c.d;
for (i=0; i < 100; i++) f(v);

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

Конечно, я это интуитивно понимаю и согласен с Sree, что последний вариант должен быть быстрее, но насколько точно быстрее? Действительно ли символические ссылки в современных JavaScript-движках настолько медленные? Не пользуются ли JavaScript интерпретаторы преимуществами JIT компиляции байткода и оптимизации байткода, которая предполагается, что если у вас написан первый вариант кода, он преобразуется внутри движка во второй вариант? (Я не знаю, насколько это возможно исходя из статического анализа, я даже не уверен в этом — это просто мое предположение.)

Предполагая, что у нас нет возможности оптимизировать неэффективные вызовы, осуществленные в первой форме ... какую потерю в производительности нам ожидать? 1%? 10%? Является ли это настолько весомым различием, чтобы мы преобразовали наш код соответствующим образом, чтобы избежать вызовов в первой форме? Будучи слишком ленив, я провел следующее тестирование только в Firefox 1.5.0.1 здесь на моем 2,2 Ггц Dell C840:

<script language="JavaScript">
var v = "Привет, медвед!";
var a = {
    b: {
	c: {
	    d: function(arg) { return arg; }
	}
    }
};

document.write("<p>a.b.c.d(\"Превед, медвед!\") 
    = " + a.b.c.d("Превед, медвед!") + "</p>");

var start = new Date();

for (var i = 0; i < 1000000; i++) {
    a.b.c.d(v);
}

var now = new Date();

document.write("<p>Время выполнения: " + (now - start) + "</p>");

start = new Date();

var f = a.b.c.d;
for (var i = 0; i < 1000000; i++) {
    f(v);
}

now = new Date();

document.write("<p>Время выполнения: " + (now - start) + "</p>");
</script>

Вывод:

a.b.c.d("Превед, медвед!") = Превед, медвед!

Время выполнения: 2374

Время выполнения: 1652

Так как JavaScript-объект Date выдает время в миллисекундах, мы проводим 1 миллион итераций в 2374 миллисекунды или 2,4 микросекунды на итерацию для первого случая. А во втором 1 миллион итераций осуществляется за 1652 миллисекунды или 1,7 микросекунды на итерацию по второй форме. Мы получаем разницу в 0,7 микросекунды на итерацию или 29%. (Да, мои математические способности, на самом деле, не являются такими выдающимися, поэтому я могу и ошибаться. Пожалуйста, перепроверьте мои выкладки и напишите, пожалуйста, если я где-то ошибся.)

Итак, выигрыш в 29% действительно впечатляет, но он дает всего лишь 0,7 микросекунды на итерацию, что не стоит того, чтобы это оптимизировать. И я полагаю, что есть куча другого кода, в которой можно применить оптимизационные техники с гораздо большей эффективностью. Другими словами, эти издержки в 29% при выполнении скрипта, по-видимому, не находятся в тех критических 90% времени выполнения скрипта, на которые нужно направить свои усилия при оптимизации.

Примечание: далее следует мои результаты для разных браузеров и несколько комментариев. Первое число соответствует первому случая, второе — второму.

  • IE 6.0: 3187 / 3297
  • IE 7.0: 3875 / 3734
  • Firefox 2: 1781 / 5063
  • Opera 9.5: 1375 / 1094
  • Safari 3: 515 / 329

Internet Explorer ведет себя примерно одинаково в обоих случаях. Скорее всего, этот шаблон в нем уже оптимизирован. Firefox 2 неприятно удивил. Вполне возможно, что сохранение цепочки вызовов в переменной, на самом деле, не уменьшают эту цепочку, а только увеличивают ее (сохраняется ссылка на цепочку вызовов). А может быть, этому есть и другое объяснение. Opera 9.5/Safari 3 ведут себя, как и ожидалось. Но их выигрыш и доля использования слишком малы, чтобы на них ориентироваться.

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

UPD: в комментариях посмотреть профиль AKS предложил ссылку на вариант, который работает быстрее и в Firefox:

for (var i = 0, d = a.b.c; i < 1000000; i++) {
    d.d();
}

Результаты: IE6 (2875), IE7 (3453), Firefox (1687), Opera 9.5 (1266), Safari 3 (468). Opera и Safari немного проигрывают, но для них можно сделать, например, специальное ветвление.

Читать дальше

Все комментарии (habrahabr.ru)