Чи справді петлі швидше зворотні?


268

Я чув це вже декілька разів. Чи справді петлі JavaScript швидше, коли підраховувати відсталі? Якщо так, то чому? Я бачив кілька прикладів тестового набору, які показують, що обернені петлі швидші, але я не можу знайти жодного пояснення того, чому!

Я припускаю, що це тому, що цикл більше не повинен оцінювати властивість кожен раз, коли він перевіряє, чи закінчений він, і він просто перевіряє кінцеве числове значення.

Тобто

for (var i = count - 1; i >= 0; i--)
{
  // count is only evaluated once and then the comparison is always on 0.
}

6
хе-хе. це займе невизначений час. спробуй i--
StampedeXV

5
Зворотний forцикл швидший, тому що верхню межу (хе-хе, нижня межа) змінної керування циклом не потрібно визначати чи витягувати з об'єкта; це постійний нуль.
Джош Стодола

88
Там немає ніякої різниці . Конструкти нативного циклу завжди будуть дуже швидкими . Не турбуйтеся про їхню ефективність.
James Allardice

24
@Afshin: Для таких питань, будь ласка, покажіть нам статті, на які ви посилаєтесь.
Гонки легкості по орбіті

22
Існує різниця, головним чином важлива для пристроїв із низьким рівнем класу та акумуляторів. Відмінність полягає в тому, що з i-- ви порівнюєте з 0 для кінця циклу, тоді як з i ++ ви порівнюєте з числом> 0. Я вважаю, що різниця в продуктивності була приблизно як 20 наносекунд (щось на зразок cmp ax, 0 vs. cmp ax , bx) - це нічого, але якщо ви циклічаєте тисячі разів за секунду, чому б не отримати 20 наносекунд для кожного :)
Любен

Відповіді:


902

Це не так i--швидше, ніж i++. Насправді вони обоє однаково швидко.

Те, що вимагає часу у висхідних циклах, оцінює для кожного iрозмір масиву. У цьому циклі:

for(var i = array.length; i--;)

Ви оцінюєте .lengthлише один раз, коли заявляєте i, тоді як для цього циклу

for(var i = 1; i <= array.length; i++)

Ви оцінюєте .lengthкожен раз i, коли Ви збільшуєте , коли Ви перевіряєте, чи є i <= array.length.

У більшості випадків ви навіть не повинні турбуватися про подібну оптимізацію .


23
чи варто ввести змінну для array.length і використовувати її в голові для циклу?
ragatskynet

39
@ragatskynet: Ні, якщо ви не встановите орієнтир і не хочете зробити точку.
Джон

29
@ragatskynet Це залежить: чи буде швидше оцінити .lengthпевну кількість разів або оголосити та визначити нову змінну? У більшості випадків це передчасна (і неправильна) оптимізація, якщо тільки lengthце не дуже дорого оцінити.
алестаніс

10
@ Dr.Dredel: Це не порівняння - це оцінка. 0швидше оцінити, ніж array.length. Ну, нібито.
Гонки легкості по орбіті

22
Варто згадати, що ми говоримо про інтерпретовані мови, такі як Ruby, Python. Мови компіляції, наприклад, Java, мають оптимізацію на рівні компілятора, яка дозволить "згладити" ці відмінності до тієї міри, що не має значення .length, декларується for loopчи ні.
Харіс Крайна

198

Цей хлопець порівняв багато циклів у javascript, у багатьох браузерах. У нього також є тестовий набір, щоб ви могли запустити їх самостійно.

У всіх випадках (якщо я не пропустив жодного в читанні), найшвидший цикл був:

var i = arr.length; //or 10
while(i--)
{
  //...
}

Приємно :) Але немає тестування назад "for" -loop ... Але для циклів, згаданих peirix або searchlea, має бути майже таким же, як цикл "while" з "i--" як його умова. І це був найшвидший тестований цикл.
SvenFinke

11
Цікаво, але мені цікаво, чи попередня декрементація була б ще швидшою. Оскільки він не повинен буде зберігати проміжне значення i.
tvanfosson

Якщо я правильно пам'ятаю, мій професор з технічного курсу сказав, що тест на 0 чи ні 0 - це найшвидший "розрахунок". В той час, як (i--) тест - це завжди тест на 0. Можливо, саме тому це найшвидше?
StampedeXV

3
@tvanfosson, якщо ви попередньо скорочуєтесь, --iтоді вам доведеться використовувати var current = arr[i-1];всередині циклу, або він буде вимкнений одним ...
DaveAlger

2
Я чув, що i-- може бути швидше, ніж --i, тому що у другому випадку процесору потрібно зменшити, а потім перевірити нове значення (між інструкціями є залежність від даних), тоді як у першому випадку процесор може перевірити існуюче значення та зменшити значення через деякий час. Я не впевнений, чи це стосується JavaScript або лише дуже низького рівня C-коду.
marcus

128

Я намагаюся дати широку картину з цією відповіддю.

Наступні думки в дужках були моїми переконаннями, поки я нещодавно не перевірив це питання:

[[З точки зору мов низького рівня, таких як C / C ++ , код складається так, що процесор має спеціальну умовну команду стрибка, коли змінна дорівнює нулю (або не нулю).
Крім того, якщо вам важлива така велика оптимізація, ви можете піти ++iзамість цього i++, тому що ++iце одна команда процесора, тоді як i++означає j=i+1, i=j.]]

Дійсно швидкі петлі можна зробити, розгорнувши їх:

for(i=800000;i>0;--i)
    do_it(i);

Це може бути набагато повільніше, ніж

for(i=800000;i>0;i-=8)
{
    do_it(i); do_it(i-1); do_it(i-2); ... do_it(i-7);
}

але причини цього можуть бути досить складними (лише згадати, є проблеми попередньої обробки команд процесора та кеш-керування в грі).

Що стосується мов високого рівня , наприклад JavaScript, як ви запитували, ви можете оптимізувати речі, якщо покладатися на бібліотеки, вбудовані функції для циклічного циклу. Нехай вони вирішать, як це найкраще зробити.

Отже, у JavaScript я б запропонував використовувати щось подібне

array.forEach(function(i) {
    do_it(i);
});

Він також менш схильний до помилок, і у браузерів є можливість оптимізувати ваш код.

[ЗАПОМОГА: не лише браузери, але і у вас є простор для оптимізації, просто переозначте forEachфункцію (браузер залежно), щоб вона використовувала останню кращу хитрість! :) @AMK каже, що в особливих випадках варто скористатися array.popабо array.shift. Якщо ви це зробите, покладіть його за завісу. Все можливе надмірністю є додавання опції forEachдля вибору алгоритму зациклення.]

Більше того, для мов низького рівня найкращою практикою є використання певної функції інтелектуальної бібліотеки для складних петельних операцій, якщо це можливо.

Ці бібліотеки також можуть розміщувати речі (багатопотокові) за вашою спиною, а також спеціалізовані програмісти постійно оновлюють їх.

Я трохи проаналізував, і виявилося, що в C / C ++, навіть для 5e9 = (50 000x100 000) операцій, немає різниці між підйомом і зменшенням, якщо тестування проводиться на константі, як говорить @alestanis. (Результати JsPerf іноді суперечливі, але за великим рахунком говорять те саме: ви не можете зробити велику різницю.)
Отож, --iце трапляється досить "шикарною" річчю. Це лише робить вас схожим на кращого програміста. :)

З іншого боку, для того, щоб розгортатись у цій ситуації 5e9, це знизило мене з 12 до 2,5 сек, коли я пройшов 10 с, і до 2,1 сек, коли я пішов на 20 с. Це було без оптимізації, і оптимізація зводила речі до невимірного мало часу. :) (Розгортання може бути зроблено в моєму способі вище або за допомогою i++, але це не випереджає справи в JavaScript.)

Загалом: зберігайте i--/ i++та ++i/ i++відмінності на співбесіді з роботою, дотримуйтесь array.forEachчи інших складних функцій бібліотеки, коли вони доступні. ;)


18
Ключове слово - "може бути". Ваш розкручений цикл також може бути повільніше, ніж оригінальний. Під час оптимізації завжди вимірюйте, щоб ви точно знали, який вплив мали ваші зміни.
jalf

1
@jalf true дійсно, +1. Різні розкручені довжини (> = 1) відрізняються ефективністю. Ось чому зручніше, якщо це можливо, залишити цю роботу в бібліотеках, не кажучи вже про те, що браузери працюють у різних архітектурах, тому може бути краще, якщо вони вирішать, як це зробити array.each(...). Я не думаю, що вони будуть намагатися експериментувати з розкручуванням простої петлі.
Барні

1
@BarnabasSzabolcs Питання стосується конкретно JavaScript, а не C та інших мов. У JS є різниця, будь ласка, дивіться мою відповідь нижче. Хоча це не стосується питання, +1 хороша відповідь!
AMK

1
А там ви йдете - +1отримати вам Great AnswerЗначок. Дійсно чудова відповідь. :)
Rohit Jain

1
APL / A ++ також не є мовою. Люди використовують це, щоб висловити, що їх не цікавить мовна специфіка, а те, що ці мови поділяють.
Барні

33

i-- так само швидко i++

Цей код нижче такий же швидкий, як і ваш, але використовує додаткову змінну:

var up = Things.length;
for (var i = 0; i < up; i++) {
    Things[i]
};

Рекомендація НЕ оцінювати розмір масиву кожен раз. Для великих масивів можна побачити зниження продуктивності.


6
Ви тут абсолютно неправі. Тепер для керування циклом потрібна додаткова внутрішня змінна (i i up), і залежно від двигуна JavaScript це може обмежувати потенціал для оптимізації коду. JIT перекладе прості петлі на прямі машинні опкоди, але якщо циклів має занадто багато змінних, то JIT не зможе оптимізуватися як хороший. Як правило, він обмежений архітектурою або регістрами процесора, якими користується JIT. Ініціалізація одного разу та спуск просто найчистіше рішення, і саме тому його рекомендують повсюдно.
Павло П

Додаткова змінна буде усунена оптимізатором загального інтерпретатора / компілятора. Справа в "порівнянні з 0" - див. Мою відповідь для подальшого пояснення.
Х.-Дірк Шмітт

1
Краще поставити «вгору» всередині циклу:for (var i=0, up=Things.length; i<up; i++) {}
квітня

22

Оскільки вас цікавить тема, перегляньте публікацію веб-журналу Грега Реймера про орієнтир циклу JavaScript, який найшвидший спосіб кодування циклу в JavaScript? :

Я створив набір тестів для тестування циклу для різних способів кодування циклів у JavaScript. Тут уже є декілька таких, але я не знайшов жодного, який би визнав різницю між натурними масивами та колекціями HTML.

Ви також можете зробити перевірку працездатності в циклі , відкривши https://blogs.oracle.com/greimer/resource/loop-test.html(не працює, якщо JavaScript заблокований у браузері, наприклад, NoScript ).

Редагувати:

Більш свіжий орієнтир, створений Міланом Адамовським, може бути виконаний тут під час роботи для різних браузерів.

Для тестування Firefox 17.0 на Mac OS X 10.6 я отримав наступний цикл:

var i, result = 0;
for (i = steps - 1; i; i--) {
  result += i;
}

як найшвидший передує:

var result = 0;
for (var i = steps - 1; i >= 0; i--) {
  result += i;
}

Приклад порівняння.


5
Цій посаді зараз 4 роки. Зважаючи на швидкість (га!), З якою оновлено двигуни JavaScript, більшість порад, ймовірно, застаріли - стаття навіть не згадує Chrome, і це блискучий двигун JavaScript V8.
Yi Jiang

Так, я пропускаю цю деталь, дякую, що вказав на це. Подія на той час не мала великої різниці між --i або i ++ у циклі для.
dreamcrash

19

Це не --та ++, це операція порівняння. З ним --можна використовувати порівняння з 0, тоді як з ним ++потрібно порівнювати його за довжиною. У процесорі порівняння з нулем зазвичай доступне, тоді як порівняння з кінцевим цілим числом вимагає віднімання.

a++ < length

насправді складено як

a++
test (a-length)

Тому процесор займає більше часу, коли компілюється.


15

Коротка відповідь

Для звичайного коду, особливо на мові високого рівня, як JavaScript , різниця в продуктивності i++та i--.

Критеріями продуктивності є використання в forциклі та оператор порівняння .

Це стосується всіх мов високого рівня і здебільшого не залежить від використання JavaScript. Пояснення - отриманий код асемблера в нижньому рядку.

Детальне пояснення

У циклі може виникнути різниця в продуктивності. Фон полягає в тому, що на рівні коду асемблера ви бачите, що a compare with 0- це лише одне твердження, якому не потрібен додатковий реєстр.

Це порівняння видається при кожному проході циклу і може призвести до покращення показників, що покращуються.

for(var i = array.length; i--; )

буде оцінено на такий псевдокод :

 i=array.length
 :LOOP_START
 decrement i
 if [ i = 0 ] goto :LOOP_END
 ... BODY_CODE
 :LOOP_END

Зауважте, що 0 - це буквальне, або іншими словами, постійне значення.

for(var i = 0 ; i < array.length; i++ )

буде оцінено таким псевдокодом (передбачається нормальна оптимізація інтерпретатора):

 end=array.length
 i=0
 :LOOP_START
 if [ i < end ] goto :LOOP_END
 increment i
 ... BODY_CODE
 :LOOP_END

Зауважте, що end - це змінна, для якої потрібен регістр процесора . Це може викликати додатковий обмін регістром у коді та потребує більш дорогого порівняння у ifзаяві.

Всього мої 5 копійок

Для мови високого рівня читабельність, яка полегшує ремонтопридатність, важливіша як незначне підвищення продуктивності.

Зазвичай класична ітерація від початку до кінця масиву краще.

Більш швидка ітерація з кінця масиву для запуску призводить до можливої ​​небажаної зворотної послідовності.

Опублікувати scriptum

На запитання в коментарі: Різниця --iі i--полягає в оцінці iдо або після декрементації.

Найкраще пояснення - спробувати це ;-) Ось приклад Баша .

 % i=10; echo "$((--i)) --> $i"
 9 --> 9
 % i=10; echo "$((i--)) --> $i"
 10 --> 9

1
1+ Гарне пояснення. Просто питання трохи з області видимості, НЕ могли б ви пояснити різницю між --iі i--також?
Афшин Мехрабані

14

Я бачив таку ж рекомендацію в піднесеному тексті 2.

Як уже було сказано, головне вдосконалення полягає не в оцінці довжини масиву при кожній ітерації в циклі for. Це добре відома методика оптимізації та особливо ефективна в JavaScript, коли масив є частиною HTML-документа (робимо forдля всіх liелементів).

Наприклад,

for (var i = 0; i < document.getElementsByTagName('li').length; i++)

набагато повільніше, ніж

for (var i = 0, len = document.getElementsByTagName('li').length; i < len; i++)

Звідки я стою, головне вдосконалення форми у вашому запитанні полягає в тому, що він не оголошує зайву змінну ( lenна моєму прикладі)

Але якщо ви запитаєте мене, вся суть полягає не в оптимізації i++vs i--, а в тому, що не потрібно оцінювати довжину масиву при кожній ітерації (ви можете бачити тест на орієнтир на jsperf ).


1
Я маю взяти виняток із слова, що обчислює тут. Дивіться мій коментар до відповіді Павла . Специфікація ECMA зазначає, що довжина масиву не обчислюється, коли ви посилаєтесь на нього.
kojiro

Чи було б "оцінювання" кращим вибором? Цікавий факт btw, я цього не знав
BBog

Додаткова змінна буде усунена оптимізатором загального інтерпретатора / компілятора. Це ж стосується оцінки array.length. Справа в "порівнянні з 0" - див. Мою відповідь для подальшого пояснення.
Х.-Дірк Шмітт

@ H.-DirkSchmitt ваша відповідь здорового глузду вбік, протягом тривалого часу в історії JavaScript компілятор не оптимізував витрати на ефективність ланцюжка пошуку. AFAIK V8 першим спробував.
kojiro

@ Х.-ДіркШмітт так, як сказав Кадро, це настільки відомий і добре відомий трюк. Навіть якщо це більше не актуально в сучасних браузерах, це все ще не робить його застарілим. Окрім того, робити це або з введенням нової змінної за довжиною, або з хитрістю у питанні ОП, це все-таки найкращий спосіб, imo. Це просто розумна справа і хороша практика, я не думаю, що це має відношення до того, що компілятор був оптимізований піклуватися про те, що часто робиться погано в JS
BBog

13

Я не думаю, що має сенс говорити, що i--швидше, ніж i++у JavaScript.

Перш за все , це повністю залежить від реалізації двигуна JavaScript.

По-друге , за умови, що найпростіші конструкції JIT'ed і перекладені на рідні інструкції, тоді i++vs i--буде повністю залежати від процесора, який його виконує. Тобто, на ARM (мобільних телефонах) швидше опуститися до 0, оскільки декремент і порівняння з нулем виконані в одній інструкції.

Напевно, ви думали, що один є більше, ніж інший, тому що пропонується такий спосіб

for(var i = array.length; i--; )

але запропонований спосіб не тому, що один швидше, ніж інший, а просто тому, що якщо ти пишеш

for(var i = 0; i < array.length; i++)

тоді для кожної ітерації array.lengthпотрібно було оцінювати (розумніший JavaScript-механізм, можливо, може зрозуміти, що цикл не змінить довжину масиву). Хоча це виглядає як просте твердження, це насправді деяка функція, яку викликає двигун JavaScript під кришкою.

Інша причина, чому i--можна вважати "швидшою", полягає в тому, що двигуну JavaScript потрібно виділити лише одну внутрішню змінну для управління циклом (змінною на var i). Якщо ви порівнюєте з array.length або якусь іншу змінну, тоді для управління циклом повинно бути більше однієї внутрішньої змінної, а кількість внутрішніх змінних обмежена активність механізму JavaScript. Чим менше змінних буде використано в циклі, тим більше шансів у JIT для оптимізації. Ось чому i--можна вважати швидше ...


3
Напевно, варто ретельно сформулювати фразу щодо того, як array.lengthоцінюється. Довжина не обчислюється, коли ви посилаєтесь на неї. (Це просто властивість, яка встановлюється щоразу, коли індекс масиву створюється або змінюється ). Коли є додаткові витрати, це тому, що двигун JS не оптимізував ланцюжок пошуку для цієї назви.
якийro

Ну, не впевнений, що говорить специфікація Ecma, але знати про деякі внутрішні системи двигунів JavaScript - це не просто, getLength(){return m_length; }тому що тут займається деяким господарством. Але якщо ви спробуєте подумати назад: це було б досить геніально, щоб написати реалізацію масиву, де потрібно було б насправді обчислити довжину :)
Pavel P

специфікація ECMA вимагає, щоб властивість довжини вже було обчислено. lengthПовинен бути негайно оновлений щоразу , коли це властивість, що-це-ан-масив додається індекс або змінений.
якийro

Що я намагаюся сказати, це те, що досить важко порушити специфікацію, якщо ти спробуєш подумати над цим.
Павло П

1
x86 - це як ARM в цьому відношенні. dec/jnzпроти inc eax / cmp eax, edx / jne.
Пітер Кордес

13

Оскільки жодна з інших відповідей, здається, не відповідає вашому конкретному запитанню (більше половини з них показує приклади C та обговорюють мови нижчого рівня, ваше питання стосується JavaScript), я вирішив написати свій власний.

Отже, ось вам:

Проста відповідь: i-- як правило, швидше, тому що не потрібно виконувати порівняння з 0 кожного разу, коли результати, результати тестування на різних методах нижче:

Результати тесту: Як "доведено" цим jsPerf, arr.pop()насправді це найшвидший цикл на сьогоднішній день. Але, зосередивши увагу на --i, i--, i++і , ++iяк ви просили в вашому питанні, ось JSPerf (вони від безлічі JSPerf, дивіться , будь ласка , джерела нижче) ; результати підсумовані:

--iі i--вони однакові в Firefox, а i--швидше в Chrome.

У Chrome основного циклу ( for (var i = 0; i < arr.length; i++)) швидше , ніж i--та в --iтой час як в Firefox це повільніше.

І в Chrome, і в Firefox кешування arr.lengthзначно швидше, а Chrome випереджає приблизно 170 000 оп / сек.

Без суттєвої різниці, ++iшвидше, ніж i++у більшості браузерів, AFAIK, це ніколи не буває навпаки в будь-якому браузері.

Коротше резюме: arr.pop() це найшвидший цикл на сьогоднішній день; для конкретно згаданих циклів i--- це найшвидший цикл.

Джерела: http://jsperf.com/fastest-array-loops-in-javascript/15 , http://jsperf.com/ipp-vs-ppi-2

Я сподіваюся, що це відповість на ваше запитання.


1
Здається, ваш popтест здається лише таким швидким, оскільки він зменшує розмір масиву до 0 для більшості циклу - наскільки я можу сказати. Однак, щоб переконатися, що справи справедливі, я створив цей jsperf так, щоб створити масив однаково з кожним тестом - який, здається, показує .shift()перемогу для моїх кількох браузерів - не те, що я б очікував, хоча :) jsperf.com / порівняти-різні-типи
петельки

Запропонований тим, що згадує єдине ++i: D
jaggedsoft

12

Це залежить від розміщення масиву в пам’яті та коефіцієнта звернення сторінок пам’яті під час доступу до цього масиву.

У деяких випадках доступ до членів масиву в порядку стовпців відбувається швидше, ніж порядок рядків через збільшення коефіцієнта звернень.


1
Якби тільки ОП запитала, чому проїзд однієї і тієї ж матриці в різних порядках може зайняти різну кількість часу ..
dcow

1
Враховуючи те, що в операційних системах управління їх пам’яттю базується на пейджинговому режимі, коли процес потребує даних, які не перебувають у кешованих сторінках, виникає помилка сторінки в ОС, і вона повинна принести цільову сторінку в кеш процесора і замінити на іншу сторінку, отже, викликає накладні витрати на обробку, що займає більше часу, ніж коли цільова сторінка знаходиться в кеші процесора. Припустимо, що ми визначили великий масив, що кожен рядок у ньому більший за розмір ОС на сторінці, і ми отримуємо доступ до нього в порядку рядків, в цьому випадку коефіцієнт помилок сторінки збільшується і результат стає повільнішим, ніж доступ порядку замовлення стовпців до цього масиву.
Акбар Баяті

Помилки сторінки - це не те саме, що пропускає кеш. Помилка сторінки лише в тому випадку, якщо пам’ять вийшла на сторінку з диска. Доступ до багатовимірних масивів у послідовному порядку того, як вони зберігаються в пам'яті, є швидшим через локальність кешу (при використанні всіх байтів рядка кеша), а не через помилки сторінки. (якщо тільки ваш робочий набір не надто великий для комп'ютера, який ви використовуєте.)
Пітер Кордес,

10

Востаннє я хвилювався з цього при написанні збірки 6502 (так, так!). Великий приріст полягає в тому, що більшість арифметичних операцій (особливо декрементів) оновили набір прапорів, один з них - Zпоказник "досягнутого нуля".

Отже, наприкінці циклу ви просто виконали дві інструкції: DEC(декремент) та JNZ(стрибок, якщо не нуль), порівняння не потрібно!


У випадку JavaScript, очевидно, це не застосовується (оскільки він працює на процесорах, у яких немає таких оп-кодів). Найімовірніше, що справжня причина по відношенню до i--vs i++полягає в тому, що з першим ви не вводите додаткові контрольні змінні в область циклу. Дивіться мою відповідь нижче ...
Павло П

1
правильно, це дійсно не застосовується; але це дуже поширений стиль С, і він виглядає чистіше тих, хто звик до цього. :-)
Хав'єр

x86 і ARM в цьому відношенні обидва як 6502. dec/jnzзамість inc/cmp/jneвипадку x86. Ви не побачите швидше запущений порожній цикл (обидва будуть насичувати пропускну здатність гілки), але відлік часу трохи зменшує накладну петлю. Нинішні попередньо завантажувачі Intel HW також не турбуються моделями доступу до пам'яті, що йдуть у порядку зменшення. Старіші процесори, на мою думку, можуть відслідковувати як 4 потоки назад, так і 6 або 10 потоків вперед, IIRC.
Пітер Кордес

7

Це можна пояснити JavaScript (і всі мови), з часом перетворюючись на опкоди для роботи на процесорі. Процесори завжди мають єдину інструкцію для порівняння з нулем, що проклято швидко.

Як осторонь, якщо ви можете гарантувати countзавжди >= 0, ви можете спростити:

for (var i = count; i--;)
{
  // whatever
}

1
Коротший вихідний код не обов'язково означає, що він буде швидшим. Ви це виміряли?
Грег Х'югілл

Ой, я пропустив цей. Цвях на голові є хлопець.
Роберт

1
Я хотів би бачити джерела збірки, де порівняння проти 0 відрізняється. Минуло кілька років, але свого часу я зробив тону кодування збірки, і я не можу за все життя придумати спосіб порівняння / тестування проти 0 таким чином, який не може бути однаково швидким для будь-якого іншого цілого числа. Тим не менш, те, що ви говорите, відповідає дійсності. Розчарований, що не можу зрозуміти, чому!
Брайан Ноблеуч

7
@Brian Knoblauch: Якщо ви використовуєте таку інструкцію, як "dec eax" (код x86), тоді ця інструкція автоматично встановлює прапор Z (нуль), який ви можете негайно перевірити, не використовуючи між собою іншу інструкцію порівняння.
Грег Хьюгілл

1
Інтерпретуючи Javascript, я сумніваюся, що опкоди будуть вузьким місцем. Більш імовірно, що менше маркерів означає, що інтерпретатор може швидше обробити вихідний код.
Тодд Оуен

6

Це не залежить від знаку --або ++знаку, але це залежить від умов, які ви застосовуєте в циклі.

Наприклад: ваш цикл швидше, якщо змінна має статичне значення, ніж якщо ваш цикл перевіряє умови кожного разу, як довжина масиву чи інші умови.

Але не хвилюйтеся з приводу цієї оптимізації, оскільки цього разу її ефект вимірюється в наносекундах.


кілька наносекундних циклів можуть стати секундами ... ніколи не погана ідея для оптимізації, коли у вас є час
Buksy

6

for(var i = array.length; i--; )не набагато швидше. Але при заміні array.lengthз super_puper_function(), що може бути значно швидше (так як це називається в кожній ітерації). Ось і різниця.

Якщо ви збираєтесь змінити це в 2014 році, вам не потрібно думати про оптимізацію. Якщо ви збираєтеся змінити його на "Пошук і заміна", вам не потрібно думати про оптимізацію. Якщо у вас немає часу, вам не потрібно думати про оптимізацію. Але тепер у вас є час подумати над цим.

PS: i--не швидше, ніж i++.


6

Якщо коротко сказати: це абсолютно не має різниці в JavaScript.

Перш за все, ви можете перевірити його самостійно:

Ви можете не лише протестувати і запускати будь-який скрипт у будь-якій бібліотеці JavaScript, але і у вас є доступ до цілого ряду раніше написаних сценаріїв, а також можливість бачити відмінності між часом виконання у різних браузерах на різних платформах.

Тож, наскільки ви бачите, різниці між продуктивністю в будь-якому середовищі немає.

Якщо ви хочете покращити ефективність свого сценарію, ви можете спробувати зробити:

  1. Майте var a = array.length;заяву, щоб ви не обчислювали її значення кожного разу в циклі
  2. Робіть розмотування циклу http://en.wikipedia.org/wiki/Loop_unwinding

Але ви повинні розуміти, що поліпшення, яке ви можете отримати, буде настільки незначним, що здебільшого про це вам навіть не варто піклуватися.

Моя власна думка, чому таке помилкове уявлення (Dec vs Inc) з'явилося

Давно-давно існувала загальна машинна інструкція, DSZ (Decrement and Skip on Zero). Люди, які запрограмовані на мові збірки, використовували цю інструкцію для реалізації циклів для збереження реєстру. Зараз ці давні факти застаріли, і я впевнений, що ви не отримаєте жодного покращення продуктивності будь-якої мови, використовуючи це псевдоудосконалення.

Я думаю, що єдиний спосіб, коли такі знання можуть поширюватися в наш час, - це коли ти читаєш чужий код людини. Подивіться таку конструкцію і запитайте, чому вона була реалізована, і тут відповідь: "вона покращує продуктивність, оскільки порівнює з нулем". Ви збентежилися вищими знаннями свого колеги і думаєте використовувати його, щоб бути набагато розумнішим :-)


Цікаво, але для ваших тестів, що працюють під Firefox 16.0.2 на win7, цикл зменшення був на 29% повільніший ... EDIT: Не зважайте на це. Повторні тести виявились непереконливими, в результатах моїх тестів з’являється дивовижна кількість шуму для наступних пробіжок. Не зовсім впевнений, чому.

Так, я спробував це пояснити, закривши все інше і просто запустивши тести. Все-таки отримали хиткі результати. Дуже дивно.

Я думаю, ви пропустили реальну точку, чому перехід до нуля вважається кращим у JavaScript. Це здебільшого тому, що таким чином лише одна змінна керує виконанням циклу, наприклад, таким чином оптимізатор / JITer має більше можливостей для вдосконалення. Використання array.lengthне обов'язково несе штрафну ефективність, просто тому, що віртуальна машина JS досить розумна, щоб зрозуміти, чи масив не змінений тілом циклу. Дивіться мою відповідь нижче.
Павло П

1
нитчікінг: цей давній факт (оптимізація мови складання) не застарілий, просто таємничий. як у вас не потрібно знати, якщо ви справді цього не робите. :-)
Хав’єр

6

Я зробив порівняння на jsbench .

Як зазначав алестані, одна річ, яка потребує часу у висхідних циклах, - це оцінити для кожної ітерації розмір вашого масиву. У цьому циклі:

for ( var i = 1; i <= array.length; i++ )

Ви оцінюєте .lengthкожен раз, коли Ви збільшуєтеся i. У цьому:

for ( var i = 1, l = array.length; i <= l; i++ )

ви оцінюєте .lengthлише один раз, коли заявляєте i. У цьому:

for ( var i = array.length; i--; )

порівняння неявне, воно відбувається безпосередньо перед декрементацією i, і код дуже читабельний. Однак те, що може зробити приголомшливе значення, - це те, що ви ставите всередину циклу.

Цикл із викликом до функції (визначено в іншому місці):

for (i = values.length; i-- ;) {
  add( values[i] );
}

Петля з вбудованим кодом:

var sum = 0;
for ( i = values.length; i-- ;) {
  sum += values[i];
}

Якщо ви можете вбудувати свій код, замість того, щоб викликати функцію, не приносячи шкоди розбірливості, ви можете мати цикл на порядок швидше!


Примітка . Оскільки браузер стає хорошим у створенні простих функцій, це дійсно залежить від того, наскільки складний ваш код. Отже, профіль перед оптимізацією, тому що

  1. Вузьке місце може бути в іншому місці (ajax, reflow, ...)
  2. Ви можете вибрати кращий алгоритм
  3. Ви можете вибрати кращу структуру даних

Але пам’ятайте:

Код написаний для того, щоб люди читали, і лише випадково для машин для виконання.


+1 для цієї відповіді та для еталону. Я додав тести forEach і переробив орієнтир на окремий файл, який можна запустити в браузері, а також у вузлі. jsfiddle.net/hta69may/2 . Для вузла "Зворотний цикл, неявне порівняння, вбудований код" є найшвидшим. Але тестування на FF 50 показало цікаві результати: не тільки таймінги були майже в 10 разів менше (!), Але й обидва тести "forEach" були такими ж швидкими, як і "зворотний цикл". Може, хлопці з Вузла повинні використовувати двигун JS Mozilla замість V8? :)
Fr0sT

4

Те, що ви робите це зараз, не швидше (крім того, що це невизначений цикл, я думаю, ви це мали намір зробити i--.

Якщо ви хочете зробити це швидше, зробіть:

for (i = 10; i--;) {
    //super fast loop
}

звичайно, ви б не помітили цього на такій маленькій петлі. Причина, що це швидше, полягає в тому, що ви декрементуєте i, перевіряючи, що це "правда" (він оцінюється як "хибний", коли він досягає 0)


1
Вам не вистачає напівколонки? (i = 10; i--;)
searchlea

Так, я виправив помилку. І якщо ми будемо прискіпливі, я зазначу, що ви забули свою напівколонку після свого i--! Хе.
djdd87

Чому це було б швидше? Коротше джерело не означає, що воно буде швидше. Ви це виміряли?
Грег Х'югілл

4
Так, я виміряв це, і я не кажу, що коротше джерело робить його швидшим, але менша кількість операцій робить його швидшим.
peirix

4

Іноді внесення деяких дуже незначних змін у спосіб написання нашого коду може мати велике значення в тому, наскільки швидко насправді працює наш код. Одна з областей, де незначна зміна коду може істотно змінити час виконання, - це те, що у нас є цикл for, який обробляє масив. Там, де масив містить елементи на веб-сторінці (наприклад, кнопки перемикання), найбільший ефект має зміни, але все ж варто застосувати цю зміну навіть там, де масив є внутрішнім кодом Javascript.

Звичайний спосіб кодування циклу for для обробки масиву lis таким:

for (var i = 0; i < myArray.length; i++) {...

Проблема в цьому полягає в тому, що для оцінки довжини масиву за допомогою myArray.length потрібен час, а спосіб, який ми закодували в циклі, означає, що цю оцінку потрібно виконувати кожен раз навколо циклу. Якщо масив містить 1000 елементів, то довжина масиву буде оцінена в 1001 раз. Якщо ми дивилися на радіо кнопки і мали myForm.myButtons.length, то це знадобиться ще довше, тому що відповідна група кнопок у зазначеній формі спочатку повинна бути розташована, перш ніж довжина може бути оцінена кожен раз навколо циклу.

Очевидно, ми не очікуємо, що довжина масиву буде змінюватися під час його обробки, тому всі ці перерахунки довжини просто непотрібно додають час обробки. (Звичайно, якщо у вас є код у циклі, який додає або видаляє записи масиву, то розмір масиву може змінюватися між ітераціями, і тому ми не можемо змінити код, який перевіряє його)

Що ми можемо зробити, щоб виправити це для циклу, де фіксується розмір, - це оцінити довжину один раз на початку циклу та зберегти її у змінній. Потім ми можемо протестувати змінну, щоб вирішити, коли закінчити цикл. Це набагато швидше, ніж щоразу оцінювати довжину масиву, особливо коли масив містить більше ніж кілька записів або є частиною веб-сторінки.

Код для цього:

for (var i = 0, var j = myArray.length; i < j; i++) {...

Тож тепер ми лише один раз оцінюємо розмір масиву і перевіряємо лічильник циклу проти змінної, яка щоразу утримує це значення навколо циклу. Доступ до цієї додаткової змінної можна отримати набагато швидше, ніж оцінка розміру масиву, і тому наш код буде працювати набагато швидше, ніж раніше. У нас просто одна додаткова змінна в нашому сценарії.

Часто не має значення, яким порядком ми обробляємо масив, доки всі записи в масиві обробляються. У цьому випадку ми можемо зробити наш код трохи швидшим, усуваючи зайву змінну, яку ми щойно додали, та обробляючи масив у зворотному порядку.

Остаточний код, який обробляє наш масив найефективнішим можливим способом, це:

for (var i = myArray.length-1; i > -1; i--) {...

Цей код все ще оцінює розмір масиву лише один раз на початку, але замість порівняння лічильника циклу зі змінною ми порівнюємо його з константою. Оскільки константа є навіть більш ефективною для доступу, ніж до змінної, і оскільки у нас є одна менша заява про призначення, ніж раніше, то наша третя версія коду тепер трохи ефективніша, ніж друга версія та набагато ефективніша, ніж перша.


4

++vs. --не має значення, оскільки JavaScript - це інтерпретована мова, а не компільована мова. Кожна інструкція перекладається на більш ніж одну машинну мову, і вам не слід дбати про деталі горіння.

Люди, які говорять про використання --(або ++) ефективного використання інструкцій по збірці, помиляються. Ця інструкція стосується цілої арифметики, і в JavaScript немає цілих чисел, а лише числа .

Вам слід написати читабельний код.


3

У багатьох випадках це по суті не має нічого спільного з тим, що процесори можуть порівняти нуль швидше, ніж інші порівняння.

Це тому, що лише деякі двигуни Javascript (ті, які є у списку JIT) насправді генерують машинний код мови.

Більшість двигунів Javascript будують внутрішнє представлення вихідного коду, яке вони потім інтерпретують (щоб зрозуміти, що це таке, подивіться внизу цієї сторінки на SpiderMonkey Firefox ). Як правило, якщо фрагмент коду робить практично те саме, але призводить до більш простого внутрішнього представлення, він запуститься швидше.

Майте на увазі, що при таких простих завданнях, як додавання / віднімання однієї зі змінної або порівняння змінної з чимось, накладні витрати інтерпретатора, що рухаються від однієї внутрішньої "інструкції" до іншої, досить великі, тому менше "інструкцій", які є використовується внутрішньо двигуном JS, тим краще.


3

Ну, я не знаю про JavaScript, це дійсно має бути лише питанням довжини масиву для повторної оцінки і, можливо, щось пов’язане з асоціативними масивами (якщо ви лише декремент, навряд чи потрібно буде виділити нові записи - якщо масив щільний, тобто хтось може для цього оптимізувати).

У низькорівневій збірці існує циклічна інструкція, що називається DJNZ (декремент і стрибок, якщо не нульовий). Таким чином, декремент і стрибок - це все в одній інструкції, що робить його, можливо, дещо швидшим, ніж INC і JL / JB (приріст, стрибок, якщо менше / стрибок, якщо нижче). Також порівняння проти нуля простіше, ніж порівняння з іншим числом. Але все це насправді маргінально, а також залежить від цільової архітектури (може змінитися, наприклад, від Arm у смартфоні).

Я б не очікував, що ці низькі відмінності матимуть такий великий вплив на інтерпретовані мови, я просто не бачив DJNZ серед відповідей, тому подумав, що поділюсь цікавою думкою.


Для запису DJNZ- це інструкція ISA 8051 (z80). x86 має dec/jnzзамість inc/cmp/jne, і, мабуть, рука має щось подібне. Я впевнений, що це не причина різниці Javascript; це від того, що потрібно більше оцінювати в циклі.
Пітер Кордес

3

Він використовується , щоб сказати , що --i був швидше (в C ++) , тому що є тільки один результат, зменшене значення. i-- потрібно зберігати зменшене значення назад до i, а також зберігати початкове значення як результат (j = i--;). У більшості компіляторів це використовувало два регістри, а не один, який може викликати необхідність запису іншої змінної в пам'ять, а не збереження як змінну реєстру.

Я погоджуюся з тими, хто сказав, що це не має значення в ці дні.


3

Дуже простими словами

"i-- і i ++. Насправді вони обоє займають один і той же час".

але в цьому випадку, коли у вас поступова робота .. процесор оцінює .length кожен раз, коли змінна збільшується на 1, а в разі зменшення .. особливо в цьому випадку, вона оцінить .length лише один раз, поки ми не отримаємо 0.


3

По-перше, i++і i--займайте рівно стільки ж часу на будь-якій мові програмування, включаючи JavaScript.

Наступний код займає набагато інший час.

Швидкий:

for (var i = 0, len = Things.length - 1; i <= len; i++) { Things[i] };

Повільний:

for (var i = 0; i <= Things.length - 1; i++) { Things[i] };

Тому наступний код теж займає різний час.

Швидкий:

for (var i = Things.length - 1; i >= 0; i--) { Things[i] };

Повільний:

for (var i = 0; i <= Things.length - 1; i++) { Things[i] };

PS Slow повільно лише для декількох мов (двигуни JavaScript) через оптимізацію компілятора. Найкращий спосіб - використовувати замість '<' замість '<=' (або '=') і '--i' замість 'i--' .


3

Не багато часу витрачається на i-- або i ++. Якщо ви заглибитесь в архітектуру процесора, то ++це швидше, ніж операція --, оскільки --операція буде доповнювати 2, але вона відбудеться всередині апаратного забезпечення, так що це зробить її швидкою, і немає великих відмінностей між, ++а --також ці операції вважаються найменш витрачений час в процесорі.

Цикл for працює так:

  • Ініціалізуйте змінну один раз на початку.
  • Перевірте обмеження у другому операнд петлі, <, >, <=і т.д.
  • Потім нанесіть петлю.
  • Збільшення циклу і циклу знову кидають ці процеси знову.

Так,

for (var i = Things.length - 1; i >= 0; i--) {
    Things[i]
}; 

обчислить довжину масиву лише один раз на старті, і це не багато часу, але

for(var i = array.length; i--; ) 

буде обчислювати довжину в кожній петлі, тому це забиратиме багато часу.


var i = Things.length - 1; i >= 0; i--обчислить довжину теж 1 раз.
Дмитро

1
Я не впевнений, що --означає " операція, що доповнить 2", але я припускаю, що це означає, що це щось зведе нанівець. Ні, це нічого не заперечує в будь-якій архітектурі. Віднімання 1 так само просто, як додавання 1, ви просто складете схему, яка запозичує, а не несе.
Олате

1

Найкращий підхід до відповіді на подібне питання - це насправді спробувати його. Налаштуйте цикл, який налічує мільйон ітерацій чи що завгодно, і зробіть це обома способами. Розмістіть обидві петлі та порівняйте результати.

Відповідь, ймовірно, залежатиме від того, який браузер ви використовуєте. Деякі матимуть інші результати, ніж інші.


4
Це все ще не відповість на його запитання про те, чому це швидше. Він просто отримав орієнтир, не знаючи, чому саме так ...
peirix

3
Це правда. Однак, не знаючи точної реалізації кожного механізму Javascript у кожному браузері, майже неможливо відповісти на частину "чому". Багато відповідей тут містять анекдотичні рекомендації, такі як "використовувати попереднє замість замість постдекременту" (хитрість оптимізації C ++) та "порівняти проти нуля", що може бути правдою в рідних мовах, складених кодом, але Javascript досить далекий від чистого металу ЦП.
Грег Хьюгілл

4
-1 Я повністю не згоден, що найкращий підхід - це спробувати. Кілька прикладів не замінюють колективні знання, і в цьому вся суть форумів на кшталт цього.
Крістоф

1

Любіть це, багато оцінок, але відповіді немає: D

Простіше кажучи порівняння проти нуля - це завжди найшвидше порівняння

Тож (a == 0) насправді швидше повертати True, ніж (a == 5)

Вона невелика і незначна, зі 100 мільйонами рядків у колекції вона вимірюється.

тобто на циклі вгору ви можете сказати, де i <= array.length і збільшувати i

на циклі вниз ви можете сказати, де i> = 0, і замість цього будете декрементувати i.

Порівняння швидше. Не напрямок циклу.


Немає відповіді на вказане запитання, оскільки у двигунах Javascript всі різні, і відповідь залежить від того, який саме браузер ви вимірюєте.
Грег Хьюгілл

Ні, це принципово прийняті порівняння проти нуля - найшвидші. Хоча ваші твердження є правильними, але золоте правило порівняння проти нуля є абсолютним.
Роберт

4
Це вірно лише в тому випадку, якщо компілятор вирішив зробити оптимізацію, що, безумовно, не гарантується. Компілятор може генерувати абсолютно той самий код для (a == 0) і (a == 5), за винятком значення константи. Процесор не буде порівнювати проти 0 швидше, ніж з будь-яким іншим значенням, якщо обидві сторони порівняння є змінними (з точки зору ЦП). Як правило, лише компілятори нативного коду мають можливість оптимізуватися на цьому рівні.
Грег Хьюгілл

1

ДОПОМОГАЙТЕ ДРУГИМИ ВІДБУДИТИ ГОЛОВНУ --- ГОЛОСУВАТИ ЦЕ ВГО !!!

Найпопулярніша відповідь на цій сторінці не працює для Firefox 14 і не передає jsLinter. Цикли "while" потребують оператора порівняння, а не призначення. Це працює на хром, сафарі, і навіть. Але вмирає у вогняного світла.

ЦЕ ПОЛОЖЕНО!

var i = arr.length; //or 10
while(i--)
{
  //...
}

ЦЕ РОБОТАЄ! (працює на firefox, передає jsLinter)

var i = arr.length; //or 10
while(i>-1)
{
  //...
  i = i - 1;
}

2
Я просто спробував це на Firefox 14, і він спрацював чудово. Я бачив приклади виробничих бібліотек, які використовують while (i--)і які працюють у багатьох браузерах. Чи могло бути щось дивне у вашому тесті? Чи використовували ви бета-версію Firefox 14?
Метт Браун

1

Це лише здогадка, але, можливо, це тому, що процесору простіше порівняти щось з 0 (i> = 0), а не з іншим значенням (i <Things.length).


3
+1 Не відмовляйся з іншими. Хоча багаторазове оцінювання .length набагато більше проблеми, ніж фактичний inc / decrement, перевірка кінця циклу може бути важливою. Мій компілятор C дає певні застереження щодо циклів на зразок: "зауваження №1544-D: (ULP 13.1) Виявлений цикл підрахунку. Рекомендуйте відлік циклів, оскільки виявити нулі простіше"
harper
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.