Пізно до цього питання з уже чудовими відповідями, але я хотів втрутитися як іноземець, звик дивитись на речі з нижчого рівня бітів і байтів у пам'яті.
Я дуже схвильований незмінними конструкціями, навіть виходячи з точки зору С та з точки зору пошуку нових способів ефективного програмування цього чудового обладнання, яке ми маємо в наші дні.
Повільніше / швидше
Щодо питання, чи робить це все повільніше, відповідь буде робототехнічною yes
. На такому дуже технічному концептуальному рівні незмінність могла лише зробити все повільніше. Обладнання робить найкраще, коли воно не спорадично розподіляє пам'ять і може просто змінювати наявну пам'ять замість цього (чому ми маємо такі поняття, як часова локальність).
І все ж практична відповідь є maybe
. Продуктивність як і раніше значною мірою є показником продуктивності в будь-якій нетривіальній кодовій базі даних. Зазвичай ми не вважаємо, що жахливі для підтримки бази кодів, що відключаються на гоночні умови, є найбільш ефективними, навіть якщо ми нехтуємо помилками. Ефективність часто є функцією елегантності та простоти. Пік мікрооптимізацій може дещо конфліктувати, але вони, як правило, зарезервовані для найменших і найважливіших розділів коду.
Трансформація змінних бітів і байтів
Якщо виходити з точки зору низького рівня, якщо ми рентгенівські такі поняття, як objects
і strings
так далі, в основі цього лежать лише біти і байти в різних формах пам'яті з різними характеристиками швидкості / розміру (швидкість і розмір апаратного забезпечення пам'яті, як правило, взаємовиключними).
Ієрархії пам’яті комп’ютера подобається, коли ми неодноразово отримуємо доступ до одного і того ж фрагмента пам’яті, як на наведеній вище схемі, оскільки він зберігатиме цей часто доступний фрагмент пам’яті у найшвидшому вигляді пам’яті (кеш L1, наприклад, який майже так само швидко, як реєстр). Ми можемо неодноразово отримувати доступ до тієї самої пам’яті (повторно використовуючи її кілька разів) або неодноразово отримувати доступ до різних розділів шматка (напр .: перебирання елементів через суміжний фрагмент, який неодноразово отримує доступ до різних розділів цього фрагмента пам'яті).
Ми в кінцевому підсумку закидаємо ключ у тому процесі, якщо зміна цієї пам'яті закінчиться, бажаючи створити цілий новий блок пам'яті збоку, наприклад:
... у цьому випадку доступ до нового блоку пам'яті може вимагати обов'язкових помилок сторінки та помилок кешу, щоб повернути її в найшвидші форми пам'яті (аж до реєстру). Це може бути справжнім вбивцею.
Однак існують способи пом'якшити це, використовуючи резервний пул попередньо виділеної пам’яті, який уже торкнувся.
Великі агрегати
Інша концептуальна проблема, яка виникає з точки зору трохи вищого рівня, - це просто робити непотрібні копії дійсно великих агрегатів.
Щоб уникнути занадто складної діаграми, давайте уявимо, що цей простий блок пам'яті був якось дорогим (можливо, символи UTF-32 на неймовірно обмеженому обладнанні).
У цьому випадку, якби ми хотіли замінити "HELP" на "KILL", і цей блок пам'яті був непорушним, нам довелося б створити цілий новий блок в цілому, щоб зробити унікальний новий об'єкт, навіть якщо змінилися лише його частини :
Розтягуючи нашу фантазію, така глибока копія всього іншого, щоб зробити одну маленьку частину унікальною, може бути досить дорогою (у реальних випадках цей блок пам'яті був би набагато значно більшим, щоб створити проблему).
Однак, незважаючи на такі витрати, подібний дизайн буде, як правило, набагато менш схильний до людських помилок. Кожен, хто працював на функціональній мові з чистими функціями, напевно, може оцінити це, особливо в багатопотокових випадках, коли ми можемо багатопотоково читати такий код без піклування у світі. Взагалі, люди-програмісти мають тенденцію до подолання змін стану, особливо тих, що викликають зовнішні побічні ефекти для станів, що не входять до сфери поточної функції. Навіть відновлення зовнішньої помилки (винятку) в такому випадку може бути неймовірно складно при змінних змінах зовнішнього стану в суміші.
Один із способів пом'якшити цю зайву роботу з копіювання - це перетворити ці блоки пам'яті в набір покажчиків (або посилань) на символи, як-от:
Вибачте, я не зміг зрозуміти, що нам не потрібно робити L
унікальні під час створення діаграми.
Синій колір позначає неглибоко скопійовані дані.
... на жаль, платити покажчик / довідкову вартість за персонаж буде надзвичайно дорого. Крім того, ми можемо розсипати вміст символів по всьому адресному простору і в кінцевому підсумку оплатити його у вигляді завантаження помилок сторінки та помилок кешу, легко зробивши це рішення ще гіршим, ніж копіювання всієї речі в повному обсязі.
Навіть якщо ми обережно розподіляли цих символів безперервно, скажімо, машина може завантажувати 8 символів і 8 покажчиків на персонаж у кеш-рядок. Ми закінчуємо завантаження пам’яті таким чином, щоб пройти нову рядок:
У цьому випадку ми потребуємо завантаження 7 різних рядків кешу, що мають суміжну пам’ять, щоб перейти цей рядок, коли в ідеалі нам потрібно лише 3.
Збивання даних
Щоб пом'якшити проблему вище, ми можемо застосувати ту саму базову стратегію, але на більш грубому рівні з 8 символів, наприклад
Для цього потрібно завантажити 4 рядки кеш-пам'яті (1 для 3 покажчиків і 3 для символів), щоб перейти для проходження цього рядка, що лише на 1 короткий від теоретичного оптимуму.
Тож це зовсім не погано. Деяка кількість пам’яті є, але пам’яті багато, а використання більше не уповільнює ситуацію, якщо зайва пам’ять просто холодна, дані не часто доступні. Це лише для гарячих, суміжних даних, коли зменшення використання пам'яті та швидкість часто йдуть рука об руку, коли ми хочемо вкласти більше пам’яті в одну сторінку чи рядок кешу та отримати доступ до неї перед виселенням. Це представлення досить кешоване.
Швидкість
Тож використання представництва, як описано вище, може дати цілком пристойний баланс ефективності. Мабуть, найбільш критичні для використання незмінні структури даних набудуть такого характеру, щоб змінити грубі фрагменти даних і зробити їх унікальними в процесі, при цьому дрібне копіювання немодифікованих фрагментів. Це також передбачає деяку накладну кількість атомних операцій для безпечного посилання на дрібні скопійовані фрагменти у багатопотоковому контексті (можливо, з деяким атомним підрахунком посилань).
Тим не менше, поки ці чіткі фрагменти даних представлені на досить грубому рівні, багато цього накладного виду зменшується і, можливо, навіть тривіалізується, при цьому все ж надаючи нам безпеку та простоту кодування та багатосторонніх функцій у чистому вигляді без зовнішньої сторони ефекти.
Зберігання нових та старих даних
Де я бачу незмінність як потенційно найбільш корисну з точки зору продуктивності (в практичному розумінні) - це коли ми можемо спокуситися зробити цілі копії великих даних, щоб зробити їх унікальними в умовах, що змінюються, де метою є створення чогось нового з те, що вже існує таким чином, коли ми хочемо зберегти як нове, так і старе, коли ми могли б просто зробити невеликі шматочки та шматочки цього унікального з ретельним незмінним дизайном.
Приклад: Скасувати систему
Прикладом цього є система скасування. Ми можемо змінити невелику частину структури даних і хочемо зберегти як оригінальну форму, яку ми можемо скасувати, так і нову форму. Завдяки такому непорушному дизайну, який робить унікальними лише невеликі, модифіковані розділи структури даних, ми можемо просто зберігати копію старих даних у відміненому записі, сплачуючи лише вартість пам'яті за додані унікальні дані порцій. Це забезпечує дуже ефективний баланс продуктивності (що робить впровадження системи скасування шматок пирога) та продуктивності.
Інтерфейси високого рівня
І все-таки щось незграбне виникає з вищезгаданим випадком. У контексті локального типу функцій змінні дані часто найпростіші та найпростіші для зміни. Зрештою, найпростіший спосіб зміни масиву - це просто прокручування його та зміна одного елемента за раз. Ми можемо в кінцевому підсумку збільшити інтелектуальні накладні витрати, якби у нас була велика кількість алгоритмів високого рівня, з яких можна було вибрати для перетворення масиву, і довелося вибрати відповідний, щоб забезпечити, щоб усі ці приємні дрібні копії були зроблені в той час, як модифіковані частини зроблено унікальним.
Напевно, найпростішим способом у цих випадках є використання змінних буферів локально всередині контексту функції (де вони, як правило, не відключають нас), які здійснюють зміни атомно в структурі даних, щоб отримати нову незмінну копію (я вважаю, деякі мови називають ці "перехідні") ...
... або ми можемо просто моделювати функції перетворення вищих і більш високих рівнів над даними, щоб ми могли приховати процес зміни буфера, що змінюється, і зафіксувати його в структурі без змін логіки. У будь-якому випадку, це ще не широко досліджена територія, і ми закінчили свою роботу, якщо ми будемо використовувати незмінні проекти більше, щоб створити значущі інтерфейси для трансформації цих структур даних.
Структури даних
Інша річ, яка виникає тут, полягає в тому, що незмінність, що використовується в критичному для продуктивного контексту, напевно, хоче, щоб структури даних розбивались на чіткі дані, коли шматки не надто малі за розміром, але й не надто великі.
Пов'язані списки, можливо, захочуть трохи змінитись, щоб пристосувати це і перетворитись на розкручені списки. Великі суміжні масиви можуть перетворитись на масив покажчиків у суміжні шматки з модульною індексацією для випадкового доступу.
Це потенційно змінює те, як ми цікаво дивимось на структури даних, водночас підштовхуючи модифікуючі функції цих структур даних, щоб вони нагадували більш об'ємну природу, щоб приховати додаткову складність у неглибокому копіюванні деяких бітів тут та зробити інших бітів унікальними там.
Продуктивність
У всякому разі, це мій маленький погляд нижчого рівня на цю тему. Теоретично незмінність може мати вартість від великої до меншої. Але дуже теоретичний підхід не завжди робить програми швидкими. Це може зробити їх масштабованими, але швидкість у реальному світі часто вимагає використання більш практичного мислення.
З практичної точки зору такі якості, як продуктивність, ремонтопридатність та безпека, як правило, перетворюються на одне велике розмиття, особливо для дуже великої бази коду. Хоча продуктивність у якомусь абсолютному сенсі знижується незмінністю, важко сперечатися, яку користь вона має від продуктивності та безпеки (включаючи безпеку ниток). Зі збільшенням до цих часто може призвести збільшення практичної продуктивності, хоча б тому, що розробники мають більше часу для налаштування та оптимізації свого коду, не забиваючись помилками.
Тому я думаю, що з цього практичного сенсу незмінні структури даних можуть насправді сприяти ефективності у багатьох випадках, як це не дивно. Ідеальний світ може шукати суміш цих двох: незмінних структур даних та змінних, причому змінні структури, як правило, дуже безпечні для використання в дуже локальному масштабі (наприклад, локальний для функції), тоді як незмінні можуть уникати зовнішньої сторони впливає прямо і перетворює всі зміни структури даних в атомну операцію, створюючи нову версію без ризику перегонів.