Чи пошкоджує незмінність ефективність роботи JavaScript?


88

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

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

Але тоді бібліотеки Immutable.js і Redux.js написані розумними людьми , ніж я, і , здається, є сильне занепокоєння за продуктивністю, так що це змушує мене задатися питанням, якщо моє розуміння сміття (і його вплив на продуктивність) є неправильним.

Чи існують переваги від непорушності, які мені не вистачають, і чи переважають вони недоліки створення стільки сміття?


8
Вони частково сильно заклопотані ефективністю, оскільки незмінність (іноді) є вартістю продуктивності, і вони хочуть максимально звести до мінімуму ці показники. Незмінність, сама по собі, має лише переваги від продуктивності в тому сенсі, що полегшує запис багатопотокового коду.
Роберт Харві

8
На мій досвід, ефективність стосується лише двох сценаріїв - одного, коли дію виконується 30+ разів за одну секунду, і два - коли її ефекти збільшуються з кожним виконанням (Windows XP одного разу знайшов помилку, де зайняв час оновлення Windows O(pow(n, 2))для кожного оновлення в його історії .) Більшість інших кодів - це негайна відповідь на подію; клацання, запит API або подібне, і поки час виконання є постійним, очищення будь-якої кількості об'єктів навряд чи має значення.
Katana314

4
Також врахуйте, що існують ефективні реалізації незмінних структур даних. Можливо, вони не такі ефективні, як мінливі, але, ймовірно, все ж більш ефективні, ніж наївна реалізація. Див., Наприклад, Чисто функціональні структури даних Кріса Окасакі
Джорджіо

1
@ Katana314: 30+ разів для мене все одно не вистачить, щоб виправдати занепокоєння щодо продуктивності. Я переніс невеликий емулятор процесора, який я написав на node.js, і вузол виконав віртуальний процесор приблизно в 20 МГц (це 20 мільйонів разів за секунду). Тож я б хвилювався про продуктивність, якби я робив щось 1000+ разів за секунду (навіть тоді я б не переживав, поки не роблю 1000000 операцій в секунду, тому що знаю, що з комфортом можу робити більше 10 з них за один раз) .
slebetman

2
@RobertHarvey "Незмінність, сама по собі, має лише переваги від продуктивності в тому сенсі, що полегшує запис багатопотокового коду". Це не зовсім вірно, незмінність дозволяє дуже широко розповсюджуватися без реальних наслідків. Це дуже небезпечно в умовах, що змінюються. Це дає вам думає , як O(1)масив нарізки і O(log n)вставок в бінарне дерево, все ще будучи в змозі використати стару вільно, а ще один приклад , tailsякий бере на себе всі хвости в списку tails [1, 2] = [[1, 2], [2], []]займає O(n)час і простір, але O(n^2)в елементі підраховувати
точку з коми

Відповіді:


59

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

Без незмінності вам, можливо, доведеться передати об'єкт між різними областями, і ви заздалегідь не знаєте, чи і коли об’єкт буде змінено. Щоб уникнути небажаних побічних ефектів, ви починаєте створювати повну копію об'єкта «про всяк випадок» і передаєте цю копію навколо, навіть якщо виявляється, що властивість взагалі не повинна змінюватися. Це залишить набагато більше сміття, ніж у вашому випадку.

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

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

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

Моє особисте правило у цій ситуації:

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

Для ситуацій між цими двома крайностями використовуйте своє судження. Але YMMV.


8
That will leave a lot more garbage than in your case.і що ще гірше, ваш час виконання, ймовірно, не зможе виявити безглузде дублювання, і, таким чином (на відміну від незмінного об'єкта, який втрачає чинність, ніхто не використовує) він навіть не буде придатний для збору.
Якоб Райхле

37

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

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

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

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


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

1
"перешкоджає створенню змінних" Чи не справедливо це лише для мов, де поведінка за замовчуванням є копією при призначенні / неявній конструкції? У JavaScript змінна - просто ідентифікатор; це не сам об’єкт. Він все ще десь займає простір, але це мізерно (особливо, як на більшості реалізацій JavaScript, наскільки мені відомо, все ще використовується стек для викликів функцій, тобто, якщо у вас є багато рекурсії, ви просто в кінцевому підсумку використовуєте той самий простір стека для більшості тимчасові змінні). Незмінність не має відношення до цього аспекту.
JAB

33

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

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

Повільніше / швидше

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

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

Трансформація змінних бітів і байтів

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

введіть тут опис зображення

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

Ми в кінцевому підсумку закидаємо ключ у тому процесі, якщо зміна цієї пам'яті закінчиться, бажаючи створити цілий новий блок пам'яті збоку, наприклад:

введіть тут опис зображення

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

Однак існують способи пом'якшити це, використовуючи резервний пул попередньо виділеної пам’яті, який уже торкнувся.

Великі агрегати

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

Щоб уникнути занадто складної діаграми, давайте уявимо, що цей простий блок пам'яті був якось дорогим (можливо, символи UTF-32 на неймовірно обмеженому обладнанні).

введіть тут опис зображення

У цьому випадку, якби ми хотіли замінити "HELP" на "KILL", і цей блок пам'яті був непорушним, нам довелося б створити цілий новий блок в цілому, щоб зробити унікальний новий об'єкт, навіть якщо змінилися лише його частини :

введіть тут опис зображення

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

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

Один із способів пом'якшити цю зайву роботу з копіювання - це перетворити ці блоки пам'яті в набір покажчиків (або посилань) на символи, як-от:

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

Синій колір позначає неглибоко скопійовані дані.

введіть тут опис зображення

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

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

введіть тут опис зображення

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

Збивання даних

Щоб пом'якшити проблему вище, ми можемо застосувати ту саму базову стратегію, але на більш грубому рівні з 8 символів, наприклад

введіть тут опис зображення

Для цього потрібно завантажити 4 рядки кеш-пам'яті (1 для 3 покажчиків і 3 для символів), щоб перейти для проходження цього рядка, що лише на 1 короткий від теоретичного оптимуму.

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

Швидкість

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

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

Зберігання нових та старих даних

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

Приклад: Скасувати систему

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

Інтерфейси високого рівня

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

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

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

Структури даних

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

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

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

Продуктивність

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

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

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


11

ImmutableJS насправді досить ефективний. Якщо ми візьмемо приклад:

var x = {
    Foo: 1,
    Bar: { Baz: 2 }
    Qux: { AnotherVal: 3 }
}

Якщо вищезазначений об'єкт буде непорушним, ви змінюєте значення властивості 'Baz', що ви отримаєте:

var y = x.setIn('/Bar/Baz', 3);
y !== x; // Different object instance
y.Bar !== x.Bar // As the Baz property was changed, the Bar object is a diff instance
y.Qux === y.Qux // Qux is the same object instance

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

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


4

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

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


4

Щоб додати до цього (вже чудово відповіді) питання:

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


Однак довга відповідь насправді не є .

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

Для мене найбільше підвищення продуктивності не в продуктивності, а в продуктивності розробника . Одне з перших речей, про які я дізнався, працюючи над програмами Real World (tm), - це те, що змінність є дійсно небезпечною та заплутаною. Я втратив багато годин, переслідуючи нитку (не тип одночасності) виконання, намагаючись розібратися, що викликає незрозумілу помилку, коли виявиться мутацією з іншого боку проклятої програми!

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

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


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


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

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