Найкращі практики зменшення активності збирача сміття у Javascript


94

У мене є досить складний додаток Javascript, який має основний цикл, який викликається 60 разів на секунду. Здається, відбувається багато збору сміття (на основі виведення «пилоподібного» з часової шкали пам'яті в інструментах розробника Chrome) - і це часто впливає на продуктивність програми.

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

Додаток побудований у "класах" за зразком простої спадщини JavaScript Джона Резіга .

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

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

Які прийоми я можу використати, щоб зменшити обсяг роботи, яку повинен робити сміттєзбірник?

І, можливо, також - за допомогою яких методів можна визначити, які об’єкти найбільше збирають сміття? (Це набагато велика кодова база, тому порівняння знімків купи не було дуже плідним)


2
У вас є приклад вашого коду, який ви могли б нам показати? Тоді на це питання буде легше відповісти (але також потенційно менш загальне, тому я тут не впевнений)
Джон Дворжак,

2
Як щодо припинення запуску функцій тисяч разів на секунду? Це справді єдиний спосіб підійти до цього? Це питання здається проблемою XY. Ви описуєте X, але те, що ви насправді шукаєте, - це рішення Y.
Тревіс Дж.

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

1
@Bergi - "деякі функції можна викликати тисячі разів на секунду". Це один раз на мілісекунду (можливо гірше!). Це взагалі не часто. 60 разів на секунду не повинно бути проблемою. Це питання є надто розмитим і буде лише давати думки чи здогади.
Тревіс Дж.

4
@TravisJ - Це зовсім не рідкість у ігрових рамках.
UpTheCreek

Відповіді:


127

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

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

  1. Коли ви створюєте об'єкт за допомогою newабо через буквальний синтаксис [...], або {}.
  2. При об'єднанні рядків.
  3. Коли ви вводите область, яка містить оголошення про функції.
  4. Коли ви виконуєте дію, яка викликає виняток.
  5. Коли ви оцінюєте вираз функції: (function (...) { ... }).
  6. Коли ви виконуєте операцію, яка примушує об'єкт, як Object(myNumber)абоNumber.prototype.toString.call(42)
  7. Коли ви телефонуєте вбудованому, який робить будь-яке з них під капотом, наприклад Array.prototype.slice.
  8. Коли ви використовуєте argumentsдля відображення над списком параметрів.
  9. Коли ви розділяєте рядок або збігаєтесь із регулярним виразом.

Уникайте робити це, а об’єднуйте та повторно використовуйте об’єкти, де це можливо.

Зокрема, зверніть увагу на можливості:

  1. Витягніть внутрішні функції, які не мають або мають мало залежностей від закритого стану, у вищу, більш тривалу область дії. (Деякі мініфайнери коду, такі як компілятор Closure, можуть вбудовувати внутрішні функції та можуть покращити продуктивність GC.)
  2. Уникайте використання рядків для представлення структурованих даних або для динамічної адресації. Особливо уникайте багаторазового синтаксичного аналізу за допомогою splitзбігів або регулярних виразів, оскільки кожен вимагає декількох розподілів об’єктів. Це часто трапляється з ключами в таблицях пошуку та ідентифікаторами динамічних вузлів DOM. Наприклад, lookupTable['foo-' + x]і те , і document.getElementById('foo-' + x)інше передбачає розподіл, оскільки існує конкатенація рядків. Часто ви можете прикріпити ключі до довгожителів замість повторного об’єднання. Залежно від браузерів, які вам потрібно підтримати, ви можете використовувати Mapдля безпосереднього використання об’єктів як ключів.
  3. Уникайте лову винятків на звичайних кодових шляхах. Замість try { op(x) } catch (e) { ... }, робіть if (!opCouldFailOn(x)) { op(x); } else { ... }.
  4. Коли ви не можете уникнути створення рядків, наприклад, щоб передати повідомлення серверу, використовуйте вбудований модуль, JSON.stringifyякий використовує внутрішній власний буфер для накопичення вмісту, замість того, щоб виділяти кілька об'єктів.
  5. Уникайте використання зворотних викликів для високочастотних подій, і там, де це можливо, передайте як зворотний виклик довговічну функцію (див. 1), яка відтворює стан із вмісту повідомлення.
  6. Уникайте використання, argumentsоскільки функції, які використовують для створення масивоподібного об'єкта при виклику.

Я запропонував використовувати JSON.stringifyдля створення вихідних мережевих повідомлень. Синтаксичний аналіз вхідних повідомлень, JSON.parseочевидно, передбачає розподіл, і багато його для великих повідомлень. Якщо ви можете представити свої вхідні повідомлення як масиви примітивів, тоді ви можете заощадити багато виділень. Єдиним іншим вбудованим процесом, навколо якого можна створити парсер, який не виділяє, є String.prototype.charCodeAt. Синтаксичний аналізатор для складного формату, який використовує лише той, який буде пекельно читати.


Вам не здається, що JSON.parseоб'єкти d виділяють менше (або рівне) місця, ніж рядок повідомлення?
Бергі,

@Bergi, Це залежить від того, чи вимагають імена властивостей окремі розподіли, але парсер, який генерує події замість дерева синтаксичного аналізу, не робить сторонніх розподілів.
Mike Samuel

Фантастична відповідь, дякую! Багато вибачень за те, що закінчується нагорода - я в той час подорожував, і з якихось причин я не міг увійти до SO за допомогою свого облікового запису gmail на своєму телефоні ....: /
UpTheCreek

Щоб компенсувати свій поганий час нагородою, я додав ще один, щоб поповнити його (200 було мінімальним, що я міг дати;) - Хоча з якихось причин мені потрібно почекати 24 години, перш ніж я його нагороджу (хоча Я вибрав "винагородити існуючу відповідь"). Буде вашим завтра ...
UpTheCreek

@UpTheCreek, не хвилюйся. Я рада, що Ви знайшли це корисним.
Mike Samuel

13

Інструменти розробника Chrome мають дуже приємну функцію для відстеження розподілу пам'яті. Це називається Timeline Memory. У цій статті описано деякі подробиці. Я гадаю, це те, про що ви говорите про "пилоподібний"? Це нормальна поведінка для більшості середовищ виконання GC. Розподіл триває, поки не буде досягнуто межі використання, що ініціює збір. Зазвичай існують різні види колекцій з різними порогами.

Хронологія пам'яті в Chrome

Збір сміття включається до списку подій, пов’язаних із трасуванням, разом із їх тривалістю. На моєму досить старому ноутбуці ефемерні колекції займають близько 4 Мб і займають 30 мс. Це 2 з ваших ітерацій циклу 60 Гц. Якщо це анімація, колекції 30ms, ймовірно, заїкаються. Ви повинні почати тут, щоб побачити, що відбувається у вашому середовищі: де поріг збору та скільки часу триває ваша колекція. Це дає вам орієнтир для оцінки оптимізацій. Але ви, мабуть, не зробите краще, ніж зменшити частоту заїкання, уповільнюючи швидкість розподілу, подовжуючи інтервал між колекціями.

Наступним кроком є ​​використання профілів | Функція розподілу купи записів створює каталог розподілів за типом записів. Це швидко покаже, які типи об’єктів споживають найбільше пам’яті протягом періоду трасування, що еквівалентно швидкості розподілу. Зосередьтеся на них у порядку зменшення курсу.

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

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


Правильно, це я маю на увазі під пилкою. Я знаю, що завжди буде якийсь пилкоподібний зразок, але мене турбує те, що з моїм додатком частота пилоподібних та „скелі” досить висока. Цікаво, що GC події не показують на мій графік - тільки події , які з'являються на панелі «записів» (в середині) є: request animation frame, animation frame fired, і composite layers. Я не уявляю, чому я не бачу, GC Eventяк ти (це на останній версії chrome, а також канарці).
UpTheCreek

4
Я спробував використовувати профайлер із `` розподілом кучі записів '', але поки що це не дуже корисно. Можливо, це тому, що я не знаю, як ним правильно користуватися. Здається, тут повно посилань, які для мене нічого не означають, таких як @342342і code relocation info.
UpTheCreek

9

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

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

var options = {var1: value1, var2: value2, ChangingVariable: value3};
function loopfunc()
{
    //do something
}

while(true)
{
    $.each(listofthings, loopfunc);

    options.ChangingVariable = newvalue;
    someOtherFunction(options);
}

буде працювати набагато швидше, ніж це:

while(true)
{
    $.each(listofthings, function(){
        //do something on the list
    });

    someOtherFunction({
        var1: value1,
        var2: value2,
        ChangingVariable: newvalue
    });
}

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

Вибачте, якщо все це трохи дріб’язково порівняно з тим, що ви вже пробували і думали.


Це. Плюс також функції, згадані в інших функціях (які не є IIFE), - це також поширене зловживання, яке спалює багато пам’яті і легко пропустити.
Esailija

Дякую Крісе! У мене немає простоїв, на жаль: /
UpTheCreek

4

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

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

PS Це може зробити цю конкретну частину коду трохи менш ремонтопридатною.


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