Измерване на производителността на функциите в JavaScript

функциите

API за време с висока разделителна способност има функция now(), която връща обект DOMHighResTimeStamp. Това е число с плаваща запетая, представляващо текущото време в милисекунди, с точност до хилядна от милисекунда. Само по себе си това число няма голяма стойност за нас, но разликата между двете измерени стойности описва колко време е минало.

Освен че е по-точен от вградения обект Date, този инструмент е и "монотонен". Ако по прост начин: не се влияе от корекцията на системното време. Тоест, като създадем две копия на Date и изчислим разликата между тях, няма да получим точна, представителна представа за това колко време е минало.

От гледна точка на математиката монотонната функция или само нараства, или само намалява. Друг пример, за по-добро разбиране: преминаването към лятно или зимно часово време, когато всички часовници в страната се преместват с час или час напред. Ако сравним стойностите на две копия на Date - преди и след смяната на часовника, ще получим например разликата "1 час 3 секунди и 123 милисекунди". И когато използвате две копия на performance.now() - "3 секунди 123 милисекунди 456 789 хилядни от милисекундата." Тук няма да анализираме подробно този API, желаещите могат да се обърнат към статията Discovering the High Resolution Time API.

И така, сега знаем какво представлява API за време с висока разделителна способност и как да го използваме. Нека сега да разгледаме някои възможни грешки, но първо нека напишем функцията makeHash(), която ще бъде използвана по-нататък в текста.

Ефективността на такива функции може да бъде измерена по следния начин:

Ако изпълните този код в браузър, резултатът ще изглежда така:

Грешка №1: Случайно измерване на ненужни неща

В примера по-горе може би сте забелязали товамежду две се използва функцията performance.now() makeHash(), чиято стойност се присвоява на резултатната променлива. Така че изчисляваме колко време е отнело изпълнението на тази функция и нищо повече. Можете също да го измерите по следния начин:

Но в този случай ще измерваме колко време е отнело на функциите makeHash('Peter'),ида изпратят и отпечатат резултата на конзолата. Не знаем колко време отнема всяка от тези операции, знаем само общата им продължителност. В допълнение, скоростта на изпращане на данни и изход към конзолата е силно зависима от браузъра и дори от това какво друго прави в момента. Вероятно си мислите, че console.log е непредвидимо бавен. Но във всеки случай е грешка да се изпълняват повече от една функция, дори ако всяка от функциите не включва никакви I/O операции. Например:

Отново, не знаем коя операция е отнела най-много време: присвояване на стойност на променлива, извикване на toLowerCase() или извикване на toString().

Грешка №2: Едно измерване

Много правят само едно измерване, събират общото време и правят дълбоки заключения. Но ситуацията може да се промени всеки път, тъй като скоростта на изпълнение зависи силно от фактори като:

  • време за компилиране на кода в байт код (време за загряване на компилатора),
  • заетост на основния процес с изпълнение на други задачи,
  • Използване на процесора от нещо, което забавя целия браузър.
Затова е по-добре да извършвате не едно измерване, а няколко:

Грешка №3: ​​Прекомерно разчитане на средните стойности

Затова е препоръчително да направите серия от измервания, за да оцените по-точно изпълнението на определена функция. Но как да се определи производителността на функция, ако за различни входове тя се изпълнява сразлична скорост? Нека първо експериментираме и измерим времето за изпълнение десет пъти с едни и същи входове. Резултатите ще изглеждат по следния начин:

Забележете как първата стойност се различава от останалите. Най-вероятно причината е само субоптимизацията и необходимостта от „загряване“ на компилатора. Малко можете да направите, за да избегнете това, но можете да се предпазите от грешни заключения.

Например, можете да изключите първата стойност и да изчислите средната аритметична стойност на останалите девет. Но е по-добре да вземем всички резултати и да изчислим медианата. Резултатите се сортират по ред и се избира средната стойност. Това е мястото, където performance.now() е много полезно, защото получавате стойност, с която можете да правите каквото искате.

Така че нека измерим отново, но този път използвайки медианата на извадката:

Грешка #4: Сравняване на функции в предвидим ред

Сега знаем, че винаги е по-добре да се направят няколко измервания и да се вземе средната стойност. Освен това, последният пример предполага, че в идеалния случай трябва да се вземе медианата вместо средната стойност.

Измерването на времето за изпълнение е добро за избор на най-бързата функция. Да кажем, че имаме две функции, които използват един и същ вход и произвеждат едни и същи резултати, но работят по различен начин. Да приемем, че трябва да изберем функция, която връща true или false, ако намери определен низ в масив, и не е чувствителна към главни и малки букви. В този случай не можем да използваме Array.prototype.indexOf.

Този код може да бъде подобрен, тъй като цикълът haystack.forEach ще обхожда всички елементи, дори ако бързо сме намерили съвпадение. Нека използваме доброто старо за:

А сега да видим коя опция е по-бърза. Нека изпълним всяка функциядесет пъти и изчислете "правилните" резултати:

Получаваме следния резултат:

Какво означава? Първата функция бешетри пъти по-бърза. Просто не може да бъде! Обяснението е просто, но не очевидно. Първата функция, използваща haystack.forEach, се възползва от оптимизации на ниско ниво на ниво JS двигател на браузъра, което не се прави при използване на индекс на масив. Така че няма да разберете, докато не го измерите!

Друга причина, поради която не можем да знаем предварително кой вариант ще бъде по-бърз, е, че всичко зависи от ситуацията. В последния пример търсихме съвпадение между 26 стойности, независимо от регистъра на буквите. Но ако търсим сред 100 000 стойности, тогава изборът на функция може да се окаже различен.