чому остання функція на 10% швидша, хоча вона повинна створювати змінні знову і знову?


14
var toSizeString = (function() {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

  return function(size) {
    var gbSize = size / GB,
        gbMod  = size % GB,
        mbSize = gbMod / MB,
        mbMod  = gbMod % MB,
        kbSize = mbMod / KB;

    if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
    } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
    } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
    } else {
      return size + 'B';
    }
  };
})();

І швидша функція: (зауважте, що вона завжди повинна знову обчислювати однакові змінні kb / mb / gb). Де вона отримує продуктивність?

function toSizeString (size) {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

 var gbSize = size / GB,
     gbMod  = size % GB,
     mbSize = gbMod / MB,
     mbMod  = gbMod % MB,
     kbSize = mbMod / KB;

 if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
 } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
 } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
 } else {
      return size + 'B';
 }
};

3
У будь-якій мові, набраній статично, "змінні" будуть складені як константи. Можливо, сучасні двигуни JS здатні зробити таку ж оптимізацію. Здається, це не працює, якщо змінні є частиною закриття.
usr

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

ви використовуєте слово "обчислити" стосовно постійних значень; там насправді немає чого обчислити в тому, що ви посилаєтесь. Арифметика постійних значень є одним з найбільш простих і очевидних оптимізаторів компіляторів, тому будь-коли ви побачите вираз, який має лише постійні значення, ви можете просто припустити, що весь вираз оптимізований до одного постійного значення.
Джиммі Хоффа

@JimmyHoffa це правда, але, з іншого боку, йому потрібно створити 3 постійних змінних для кожної функції виклику ...
Tomy

@Томі константи не є змінними. Вони не змінюються, тому їх не потрібно відтворювати після компіляції. Константа, як правило, розміщується в пам’яті, і кожне майбутнє досягнення цієї константи спрямоване саме на те саме місце, її відтворювати не потрібно, тому що значення ніколи не змінюватиметься , тому це не є змінною. Компілятори зазвичай не випромінюють код, який створює константи, компілятор робить створення, і він спрямовує всі посилання коду на те, що він зробив.
Джиммі Хоффа

Відповіді:


23

Сучасні двигуни JavaScript роблять компіляцію вчасно. Ви не можете робити жодних припущень щодо того, що це "повинно створюватися знову і знову". Такий розрахунок порівняно легко оптимізувати в будь-якому випадку.

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

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


Я підозрюю, що це обхід області для змінної роздільної здатності викликає втрати, як ви згадуєте. Здається розумним, але хто по-справжньому знає, яке безумство криється в двигуні JavaScript JIT ...
Джиммі Хоффа

1
Можливе розширення цієї відповіді: причина, через яку JIT ігнорує оптимізацію, просту для офлайн-компілятора, полягає в тому, що продуктивність всього компілятора важливіша, ніж для незвичних випадків.
Левшенко

12

Змінні дешеві. Контексти та ланцюги виконання дорого коштують.

Існують різні відповіді , які в основному зводяться до «тому затворів», і ті , по суті вірно, але проблема не конкретно з закриттям, це той факт , що у вас є функція , що посилається змінні в іншій області. У вас була б така ж проблема, якби це були глобальні змінні windowоб’єкта, на відміну від локальних змінних всередині IIFE. Спробуйте і подивіться.

Тож у вашій першій функції, коли двигун бачить це твердження:

var gbSize = size / GB;

Він повинен зробити наступні кроки:

  1. Пошук змінної sizeв поточному діапазоні. (Знайшов це.)
  2. Пошук змінної GBв поточному діапазоні. (Не знайдено.)
  3. Пошук змінної GBв батьківській області. (Знайшов це.)
  4. Зробіть обчислення і призначте gbSize.

Крок 3 значно дорожчий, ніж просто виділення змінної. Більше того, ви робите це п’ять разів , включаючи двічі і для, GBі для MB. Я підозрюю, що якби ви їх псевдоніміли на початку функції (наприклад var gb = GB) і замість цього посилалися на псевдоніми, це фактично призведе до невеликого прискорення, хоча також можливо, що деякі двигуни JS вже виконують цю оптимізацію. І звичайно, найефективніший спосіб прискорити виконання - це взагалі не переходити по ланцюжку областей.

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

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


І навіть якщо «оптимізація» не впливає на продуктивність негативно, то майже напевно це буде впливати на читаність коду негативно. Що, якщо ви не робите якісь божевільні обчислювальні роботи, найчастіше це поганий компроміс (мабуть, без постійної посилання на якір; на жаль, шукайте "2009-02-17 11:41"). Як додається резюме: "Вибирайте чіткість над швидкістю, якщо швидкість абсолютно не потрібна".
CVn

Навіть при написанні дуже базового інтерпретатора для динамічних мов змінний доступ під час виконання часу є операцією O (1), а обхід області (n) не потрібен навіть під час початкової компіляції. У кожному діапазоні кожній щойно оголошеній змінній присвоюється номер, тому, маючи на увазі, var a, b, cми можемо отримати доступ bяк scope[1]. Всі діапазони пронумеровані, і якщо цей обсяг вкладений в п’ять діапазонів глибиною, то bвін повністю вирішується, за допомогою env[5][1]якого відомо під час розбору. У власному коді області застосування відповідають сегментам стеку. Закриття є складнішим, оскільки вони повинні створити резервну копію та замінитиenv
амон

@amon: Це, можливо, ви б хотіли, щоб це працювало, але це не так, як це насправді працює. Люди, набагато більш обізнані та досвідчені, ніж я писав про це книги; зокрема, я хотів би вказати на високоефективний JavaScript Ніколаса К. Закаса. Ось фрагмент , і він також поговорив із орієнтирами, щоб підкріпити його. Звичайно, він, звичайно, не єдиний, просто найвідоміший. JavaScript має лексичне визначення, тому закриття насправді не таке вже й особливе - по суті, все це закриття.
Aaronaught

@Aaronaught Цікаво. Оскільки цій книзі вже 5 років, мені було цікаво, як поточний двигун JS обробляє змінну пошукових запитів і дивився на хіднебек x64 двигуна V8. Під час статичного аналізу більшість змінних вирішуються статично і присвоюється зміщення пам’яті в їх області застосування. Області функцій представлені у вигляді пов'язаних списків, а складання випромінюється у вигляді розкрученого циклу, щоб досягти правильної області застосування. Тут ми отримаємо еквівалент коду С *(scope->outer + variable_offset)для доступу; кожен додатковий рівень функції функції коштує одного додаткового затримки покажчика. Здається, ми обидва мали рацію :)
amon

2

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

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

function add(vars, y) {
  vars.x += y;
}

function getSum(vars) {
  return vars.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(adder, 2);
console.log(adder.getSum(adder));  //=> 42

Зауважте, що closure.apply(closure, ...realArgs)цього вимагає незручна умова виклику

Підтримка вбудованого об’єкта JavaScript дозволяє опустити явний varsаргумент і дозволяє використовувати this:

function add(y) {
  this.x += y;
}

function getSum() {
  return this.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

Ці приклади прирівнюються до цього коду, фактично використовуючи закриття:

function makeAdder(x) {
  return {
    add: function (y) { x += y },
    getSum: function () { return x },
  };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

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

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

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