Чому <= повільніше, ніж <використовувати цей фрагмент коду у V8?


166

Я читаю слайди Порушення обмеження швидкості Javascript з V8 , і є такий приклад, як код нижче. Я не можу зрозуміти, чому <=це повільніше, ніж <у цьому випадку, хтось може це пояснити? Будь-які коментарі вдячні.

Повільний:

this.isPrimeDivisible = function(candidate) {
    for (var i = 1; i <= this.prime_count; ++i) {
        if (candidate % this.primes[i] == 0) return true;
    }
    return false;
} 

(Підказка: primes - це масив довжини prime_count)

Швидше:

this.isPrimeDivisible = function(candidate) {
    for (var i = 1; i < this.prime_count; ++i) {
        if (candidate % this.primes[i] == 0) return true;
    }
    return false;
} 

[Докладніше] Поліпшення швидкості є значним, в моєму тесті на місцевому середовищі результати такі:

V8 version 7.3.0 (candidate) 

Повільний:

 time d8 prime.js
 287107
 12.71 user 
 0.05 system 
 0:12.84 elapsed 

Швидше:

time d8 prime.js
287107
1.82 user 
0.01 system 
0:01.84 elapsed

10
@DacreDenny Обчислювальна складність <=та <однакова, як теоретично, так і фактично впроваджена у всіх сучасних процесорах (і перекладачах).
TypeIA

1
Я прочитав документ, є mainкод, який викликає цю функцію в циклі, який працює в 25000рази, тому ви робите набагато менше ітерацій в цілому, роблячи цю зміну. Крім того, якщо масив має довжину 5, спроба отримати array[5]буде виходити за межу, даючи undefinedзначення, оскільки масиви починають індексувати 0.
Шидерш

1
Було б корисно, якби це питання пояснило, яка частина покращення швидкості досягається (наприклад, у 5 разів швидше), щоб людей не відкидало додаткова ітерація. Я намагався знайти швидкість на слайдах, але було багато, і у мене виникли проблеми з її пошуку, інакше я б сам це відредагував.
Капітан Людина

@CaptainMan Ви маєте рацію, точно покращити швидкість важко відстежувати зі слайдів, оскільки вони охоплюють відразу декілька різних проблем. Але в моїй розмові з доповідачем після цієї розмови він підтвердив, що це не просто невелика частка відсотка, як можна було очікувати від однієї додаткової ітерації в цьому тестовому випадку, а велика різниця: в кілька разів швидше, можливо, замовлення за величиною або більше. І причиною цього є те, що V8 повертається назад (або впав назад у ті часи) до оптимізованого формату масиву, коли ви намагаєтесь читати поза межами масиву.
Майкл Гірі

3
Це може бути корисно порівняти версію, яка використовує, <=але в іншому випадку діє ідентично <версії, виконуючи це i <= this.prime_count - 1. Це вирішує як питання "додаткової ітерації", так і питання "один минулий кінець масиву".
TheHansans

Відповіді:


132

Я працюю над V8 в Google, і хотів би дати деяке додаткове розуміння поверх існуючих відповідей та коментарів.

Для довідки, ось приклад повного коду зі слайдів :

var iterations = 25000;

function Primes() {
  this.prime_count = 0;
  this.primes = new Array(iterations);
  this.getPrimeCount = function() { return this.prime_count; }
  this.getPrime = function(i) { return this.primes[i]; }
  this.addPrime = function(i) {
    this.primes[this.prime_count++] = i;
  }
  this.isPrimeDivisible = function(candidate) {
    for (var i = 1; i <= this.prime_count; ++i) {
      if ((candidate % this.primes[i]) == 0) return true;
    }
    return false;
  }
};

function main() {
  var p = new Primes();
  var c = 1;
  while (p.getPrimeCount() < iterations) {
    if (!p.isPrimeDivisible(c)) {
      p.addPrime(c);
    }
    c++;
  }
  console.log(p.getPrime(p.getPrimeCount() - 1));
}

main();

В першу чергу, різниця в продуктивності не має нічого спільного з <і <=операторами безпосередньо. Тому, будь ласка, не стрибайте через обручі просто, щоб уникнути <=свого коду, оскільки ви читали на стеку переповнення, що це повільно --- це не так!


По-друге, люди зазначили, що масив - "дірявий". Це не було зрозуміло з фрагмента коду в дописі ОП, але зрозуміло, коли ви дивитесь на код, який ініціалізується this.primes:

this.primes = new Array(iterations);

Це призводить до масиву з через HOLEYелементи виду в V8, навіть якщо масив закінчується повністю заповнений / упаковано / суміжними. Взагалі, операції на маточних масивах проходять повільніше, ніж операції з упакованими масивами, але в цьому випадку різниця незначна: вона становить 1 додатковий чек Smi ( мале ціле число ) (для захисту від дірок) кожного разу, коли ми потрапляємо this.primes[i]в цикл всередині isPrimeDivisible. Немає нічого!

TL; DR Тут HOLEYне проблема.


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

Поза межами читання приводить до this.primes[i]того, що перебуваєте undefinedна цій лінії:

if ((candidate % this.primes[i]) == 0) return true;

І це підводить нас до справжньої проблеми : %зараз оператор використовується з не цілими операндами!

  • integer % someOtherIntegerможна обчислити дуже ефективно; Двигуни JavaScript можуть виробляти високооптимізований код машини для цього випадку.

  • integer % undefinedз іншого боку, означає спосіб менш ефективний Float64Mod, оскільки undefinedпредставлений як подвійний.

Фрагмент коду дійсно може бути поліпшена за рахунок зміни <=INTO <на цій лінії:

for (var i = 1; i <= this.prime_count; ++i) {

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


1
Коментарі не для розширеного обговорення; ця розмова була переміщена до чату .
Самуель Liew

1
Щоб бути на 100% завершеним, зафіксований IC завантаження для цього.primes [i] в ​​isPrimeDivisible несподівано переходить у мегаморфний V8. Це здається помилкою: bugs.chromium.org/p/v8/isissue/detail?id=8561
Mathias Bynens

226

Інші відповіді та коментарі зазначають, що різниця між двома петлями полягає в тому, що перша виконує ще одну ітерацію, ніж друга. Це правда, але в масиві, який виростає до 25 000 елементів, одна ітерація більш-менш змінила б лише незначну різницю. Як можна припустити, якщо припустити, що середня довжина в міру зростання становить 12 500, то різниця, яку ми могли б очікувати, повинна становити близько 1/12 500, або лише 0,008%.

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

this.primes це суміжний масив (кожен елемент має значення), а елементи - це всі числа.

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

Однією умовою було б, якщо деякі елементи масиву відсутні. Наприклад:

let array = [];
a[0] = 10;
a[2] = 20;

Тепер у чому цінність a[1]? Це не має значення . (Неправильно сказати, що у нього є значення undefined- елемент масиву, що містить undefinedзначення, відрізняється від елемента масиву, який повністю відсутній.)

Не існує способу представити це лише номерами, тому двигун JavaScript змушений використовувати менш оптимізований формат. Якщо він a[1]містив числове значення, як і інші два елементи, масив потенційно може бути оптимізований лише у масив чисел.

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

Перший цикл із <=спробами прочитати елемент повз кінець масиву. Алгоритм все ще працює правильно, оскільки в останній додатковій ітерації:

  • this.primes[i]оцінює, undefinedтому що iминув кінець масиву.
  • candidate % undefined(для будь-якого значення candidate) оцінює до NaN.
  • NaN == 0оцінює до false.
  • Отже, return trueне виконується.

Отже, ніби зайвої ітерації ніколи не бувало - це не впливає на іншу логіку. Код дає такий же результат, як і без додаткової ітерації.

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

Другий цикл <зчитує лише елементи, які існують у масиві, тому він дозволяє оптимізувати масив та код.

Проблема описується на сторінках 90-91 бесіди з відповідним обговоренням на сторінках до і після цього.

Я випадково відвідував цю саму презентацію Google I / O і згодом спілкувався зі спікером (одним із авторів V8). Я використовував техніку у власному коді, яка передбачала читання минулого кінця масиву як хибну (заднім числом) спробу оптимізувати одну конкретну ситуацію. Він підтвердив, що якщо ви спробуєте навіть прочитати минулий кінець масиву, це не дозволить використовувати простий оптимізований формат.

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

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


1
Я майже впевнений, що масив все ще суміжний - немає ніяких причин змінювати компонування пам’яті. Що важливо, це те, що перевірка вихідних меж в доступі до ресурсу не може бути оптимізована, і код інколи подається undefinedзамість числа, що призводить до іншого обчислення.
Бергі

1
@Bergi Я не експерт JS / V8, але об'єкти в мовах GC майже завжди посилаються на фактичні об'єкти. Ці фактичні об'єкти мають незалежне розподілення, навіть якщо посилання є суміжними, оскільки термін експлуатації об'єктів GC не пов'язаний між собою. Оптимізатори можуть упакувати ці незалежні асигнування до сусідніх, але (a) пам'ять використовує підвісні ракети та (b) у вас є два суміжні блоки, які повторюються (посилання та згадані дані) замість одного. Я припускаю, що божевільний оптимізатор міг би переплутати посилання та дані, на які посилаються, і мати масив, що володіє смугами пам'яті ...
Як - Адам Невраумон

1
@Bergi Масив все ще може бути суміжним у неоптимізованому випадку, але елементи масиву не є тим самим типом, як у оптимізованому випадку. Оптимізована версія - це простий масив чисел без додаткового пуху. Неоптимізована версія - це масив об'єктів (внутрішній формат об'єкта, а не JavaScript Object), оскільки він повинен підтримувати будь-яку суміш типів даних у масиві. Як я вже згадував вище, код у циклі, що подається undefined, не впливає на правильність алгоритму - він зовсім не змінює обчислення (це як би додаткова ітерація ніколи не бувала).
Майкл Гірі

3
@Bergi Автор V8, який читав цю розмову, сказав, що спроба читання поза межами масиву спричиняє обробку масиву як би поєднання типів: замість оптимізованого формату, призначеного лише для числа, він деоптимізує масив назад до загальний формат. В оптимізованому випадку це простий масив чисел, який ви можете використовувати в програмі C. У деоптимізованому випадку це масив Valueоб'єктів, який може містити посилання на значення будь-якого типу. (Я склав ім'я Value, але справа в тому, що елементи масиву - це не просто прості числа, а об'єкти, які обертають числа чи інші типи.)
Майкл Гірі

3
Я працюю над V8. Масив, про який йдеться, буде позначено як те, HOLEYщо він створений за допомогою new Array(n)(хоча ця частина коду не була видно в ОП). HOLEYмасиви HOLEYназавжди залишаються у V8 , навіть коли вони згодом заповнюються. Однак це означає, що масив, який є холі, не є причиною проблеми в цьому випадку; це просто означає, що ми повинні зробити додаткову перевірку Smi на кожну ітерацію (щоб захистити від дірок), що не має нічого особливого.
Матіас Байненс

19

TL; DR Більш повільний цикл пов'язаний з доступом до масиву "поза межами", який або змушує двигун перекомпілювати функцію з меншими або навіть відсутніми оптимізаціями АБО не скомпілювати функцію з будь-якої з цих оптимізацій для початку ( якщо компілятор (JIT-) виявив / запідозрив цей стан до першої "версії" компіляції), читайте нижче чому;


Хто - то повинен сказати , що це (зовсім здивований вже ніхто не робив):
Там раніше був час , коли фрагмент коду в OP був би де-факто приклад НАЧИНАЮЩИХ програмування книги , призначені для начерків / підкреслити , що «масиви» в JavaScript індексуються Відправною в 0, а не 1, і як такий можна використовувати як приклад поширеної «помилки початківців» (не любите ви, як я уникнув фразу «помилка програмування» ;)): поза межами масиву доступу .

Приклад 1:
a Dense Array(будучи суміжним (означає відсутність проміжків між індексами) ТА фактично елементом у кожному індексі) з 5 елементів, використовуючи індексацію на основі 0 (завжди в ES262).

var arr_five_char=['a', 'b', 'c', 'd', 'e']; // arr_five_char.length === 5
//  indexes are:    0 ,  1 ,  2 ,  3 ,  4    // there is NO index number 5



Таким чином, ми насправді не говоримо про різницю продуктивності між <vs <=(або «однією додатковою ітерацією»), але ми говоримо:
«чому правильний фрагмент (b) працює швидше, ніж помилковий фрагмент (a)»?

Відповідь є дворазовою (хоча з точки зору реалізатора мови ES262 обидва є формами оптимізації):

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

Пункт 1 достатньо (і правильно IMHO) пояснюється прийнятою відповіддю , але це витрачає лише 2 слова ("код") на пункт 2: складання .

Точніше: JIT-компіляція і ще важливіше JIT- RE -компіляція!

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

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

Уявіть таку просту функцію:

function sum(arr){
  var r=0, i=0;
  for(;i<arr.length;) r+=arr[i++];
  return r;
}

Ідеально зрозуміло, правда? Ніякого додаткового уточнення не потрібно, правда? Тип повернення є Number, правда?
Ну .. ні, ні і ні ... Це залежить від того, який аргумент ви передаєте для названого параметра функції arr...

sum('abcde');   // String('0abcde')
sum([1,2,3]);   // Number(6)
sum([1,,3]);    // Number(NaN)
sum(['1',,3]);  // String('01undefined3')
sum([1,,'3']);  // String('NaN3')
sum([1,2,{valueOf:function(){return this.val}, val:6}]);  // Number(9)
var val=5; sum([1,2,{valueOf:function(){return val}}]);   // Number(8)

Бачите проблему? Тоді вважайте, що це ледве вискоблювання можливих масивних перестановок ... Ми навіть не знаємо, який тип ВИДАЄТЬСЯ функцію, поки ми не виконаємо ...

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

Таким чином, якщо ви збирали функцію sumJUST ONCE, то єдиний спосіб, який завжди повертає визначений специфікацією результат для будь-якого і всіх типів введення, тоді, очевидно, лише виконуючи ВСІ призначені специфікою основні І під кроки, може гарантувати відповідність специфікації результатам (як-от неназваний веб-переглядач pre-y2k). Не залишається жодних оптимізацій (бо немає припущень) і мертвої повільної інтерпретованої мови сценаріїв.

JIT-компіляція (JIT як в Just In Time) - це поточне популярне рішення.

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

На все це потрібен час!

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

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

Зважаючи на це, говорити про продуктивність є важливим, Але ТАКОЖ мінним полем (і через вказане мінне поле я дуже хочу закінчити вказівкою на (і цитуванням) якогось відповідного матеріалу:

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

...

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

Джерело:
"JITProf: Pinpointing JIT-непривітний JavaScript-код"
Публікація в Берклі, 2014 р., Лян Гонг, Майкл Прадель, сенатор Кушик
http://software-lab.org/publications/jitprof_tr_aug3_2014.pdf

ASM.JS (також не подобається вихід із обмеженого масиву):

Попередня компіляція

Оскільки asm.js є суворим набором JavaScript, ця специфікація визначає лише логіку перевірки - семантика виконання - просто те, що JavaScript. Однак перевірений asm.js піддається попередній компіляції (AOT). Більше того, код, сформований компілятором AOT, може бути досить ефективним, включаючи:

  • незмінені подання цілих чисел і чисел з плаваючою комою;
  • відсутність перевірок типу виконання;
  • відсутність збору сміття; і
  • ефективні нагромадження купи та сховища (стратегії впровадження залежать від платформи).

Код, який не вдалося перевірити, повинен повернутися до виконання традиційними засобами, наприклад, інтерпретацією та / або складанням щойно вчасно (JIT).

http://asmjs.org/spec/latest/

і нарешті https://blogs.windows.com/msedgedev/2015/05/07/bringing-asm-js-to-chakra-microsoft-edge/
чи був невеликий підрозділ про внутрішні покращення працездатності двигуна при усуненні меж- чек (у той час як лише підняття межі перевірки за межі циклу вже покращило 40%).



EDIT:
зауважте, що багато джерел говорять про різні рівні JIT-рекомпіляції аж до інтерпретації.

Теоретичний приклад, що ґрунтується на вищенаведеній інформації, щодо фрагменту роботи ОП:

  • Дзвінок на isPrimeDivisible
  • Компілювати isPrimeDivisible, використовуючи загальні припущення (як, наприклад, відсутність доступу поза межами)
  • Робити роботу
  • BAM, раптом масив має доступ за межі (прямо в кінці).
  • Лайно, каже двигун, давайте перекомпілюємо isPrimeDivisible, використовуючи різні (менші) припущення, і цей приклад двигуна не намагається зрозуміти, чи може він використовувати повторно поточний частковий результат,
  • Перерахуйте всю роботу, використовуючи більш повільну функцію (сподіваємось, вона закінчиться, інакше повторіть і на цей раз просто інтерпретуйте код).
  • Повернення результату

Отже, час був:
Перший запуск (не вдалося в кінці) + виконувати всю роботу знову, використовуючи повільніший машинний код для кожної ітерації + перекомпіляція тощо. В цьому теоретичному прикладі явно потрібно> 2 рази довше !



EDIT 2: (відмова: домисли, засновані на фактах, наведених нижче)
Чим більше я думаю про це, тим більше я думаю, що ця відповідь може насправді пояснити більш домінуючу причину цього "штрафу" за помилковий фрагмент a (або бонус за ефективність на фрагменті b , залежно від того, як ви це думаєте), саме тому я прихильний називати це (фрагмент а) помилкою програмування:

Дуже привабливо вважати, що this.primesце чистий числовий масив, чистий чисельний

  • Жорстко закодований літерал у вихідному коді (відомий відмінний кандидат стати "справжнім" масивом, оскільки все вже відомо компілятору до часу компіляції) АБО
  • швидше за все, генерується за допомогою числової функції, що заповнює попередньо розмір ( new Array(/*size value*/)) у порядку зростання (інший давно відомий кандидат стати "справжнім" масивом).

Ми також знаємо, що primesдовжина масиву кешована як prime_count! (із зазначенням його наміру та фіксованого розміру).

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

Тому доцільно припустити, що Array primes- це, швидше за все, вже оптимізований масив, який не змінюється після створення (просто знати для компілятора, якщо немає коду, що модифікує масив після створення), а тому вже є (якщо застосовно до двигун) зберігається оптимізованим способом, майже так, як ніби це було Typed Array.

Як я намагався зрозуміти на sumприкладі своєї функції, аргументи (аргументи), які передаються, сильно впливають на те, що насправді має відбутися, і як такий, як саме цей код збирається в машинний код. Перехід Stringдо sumфункції не повинен змінювати рядок, а змінювати те, як функція компілюється JIT! При передачі масиву sumслід скласти іншу (можливо, навіть додаткову для цього типу або "форму", як вони називають, версію машинного коду).

Як здається, злегка бонус перетворити масив, схожий на Typed_Array, primesна льоту, в something_else, тоді як компілятор знає, що ця функція навіть не збирається її змінювати!

Згідно з цими припущеннями, що залишає 2 варіанти:

  1. Скомпілюйте як число-розбивач, припускаючи, що немає вихідних меж, в кінці виникають проблеми поза межами, перекомпілюйте та повторіть роботу (як зазначено в теоретичному прикладі в редакції 1 вище)
  2. Компілятор вже виявив (або підозрює?) Поза обмеженими доступом передовий доступ, а функція була JIT-Compiled так, ніби переданий аргумент був розрідженим об'єктом, що призводить до уповільнення функціонального машинного коду (оскільки це матиме більше перевірок / перетворень / примусів тощо). Іншими словами: функція ніколи не піддавалася певним оптимізаціям, вона була складена так, ніби вона отримала аргумент "розріджений масив" (- як).

Мені зараз справді цікаво, хто з цих 2 це!


2
Хороша дискусія щодо деяких основних питань - проте ви ледве не пояснюєте відповідь (в останньому реченні). Можливо, додати до самого верху tl; dr? наприклад, "Більш повільний цикл пов'язаний із перевищенням масиву меж, що змушує двигун переоцінювати цикл без оптимізацій. Читайте далі, щоб дізнатися, чому."
бричін

@brichins: спасибі і дякую за пропозицію, яку я трохи переробив у світлі своєї другої додаткової редакції, тому що чим більше я зараз про це думаю, тим твердження вгорі здається і правильним
GitaarLAB

6

Щоб додати до цього трохи науковості, ось jsperf

https://jsperf.com/ints-values-in-out-of-array-bounds

Він перевіряє контрольний випадок масиву, заповненого ints та циклічно, виконуючи модульну арифметику, залишаючись у межах. У ньому є 5 тестових випадків:

  • 1. Розкручування поза межами
  • 2. Масиви холів
  • 3. Модульна арифметика проти NaN
  • 4. Повністю невизначені значення
  • 5. Використання a new Array()

Це показує, що перші 4 випадки справді погані для роботи. Викручування за межі трохи краще, ніж інші 3, але всі 4 приблизно на 98% повільніше, ніж найкращий випадок.
Справа new Array()майже така ж хороша, як і необроблений масив, лише на кілька відсотків повільніше.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.