Чи потребує більшої частини змінних даних у функціональному програмуванні більше використання пам'яті?


63

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


7
Це може означати, що більшість незмінних структур даних повторно використовують основні дані для змін. Ерік Ліпперт має чудовий серіал блогу про незмінність у C #
Одід

3
Я хотів би поглянути на чисто функціональні структури даних, це чудова книга, яку написав той самий хлопець, який написав більшу частину бібліотеки контейнерів Haskell (хоча книга в першу чергу SML)
jozefg

1
Ця відповідь, що стосується часу роботи замість споживання пам’яті, також може бути цікавою для вас: stackoverflow.com/questions/1990464/…
9000

1
Вам це може бути цікаво: en.wikipedia.org/wiki/Static_single_assignment_form
Шон Мак-Що-що

Відповіді:


35

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


24

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

Це залежить від структури даних, точних змін, які ви здійснили, а в деяких випадках і від оптимізатора. В якості одного з прикладів розглянемо попереднє додавання до списку:

list2 = prepend(42, list1) // list2 is now a list that contains 42 followed
                           // by the elements of list1. list1 is unchanged

Тут додаткова вимога пам’яті є постійною - так це і вартість виконання дзвінка prepend. Чому? Тому що prependпросто створюється нова клітина, яка має 42як голову, так і list1хвіст. Для цього не потрібно копіювати чи іншим чином повторювати list2. Тобто, за винятком пам'яті, необхідної для зберігання 42, list2повторно використовується та сама пам'ять, якою вона користується list1. Оскільки обидва списки незмінні, цей обмін є абсолютно безпечним.

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

Для масивів ситуація дещо інша. Ось чому, у багатьох мовах FP масиви не так часто використовуються. Однак якщо ви робите щось подібне arr2 = map(f, arr1)і arr1більше ніколи не використовуєтесь після цього рядка, розумний оптимізатор може насправді створити код, який мутує, arr1замість створення нового масиву (не впливаючи на поведінку програми). У такому випадку виступ буде, як звичайно, обов'язковою мовою.


1
Не цікаво, яка реалізація яких мов повторно використовує простір, як ви описали наприкінці?

@delnan У моєму університеті була дослідницька мова під назвою Qube, яка зробила це. Я не знаю, чи існує якась вживана в дикій формі мова, яка це робить. Однак синтез Haskell може досягти такого ж ефекту у багатьох випадках.
sepp2k

7

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

У різних мовах є різні способи впоратися з цим, і є кілька хитрощів, якими користується більшість з них.

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

Ще одна - вибір різних типів даних. Там, де масиви є структурою даних списку переходів на імперативних мовах (як правило, загорнуті у якийсь контейнер динамічного перерозподілу, наприклад std::vectorу C ++), функціональні мови часто віддають перевагу зв'язаним спискам. За допомогою пов'язаного списку операція "Prepend" ("мінуси") може повторно використовувати існуючий список як хвіст нового списку, тому все, що дійсно виділяється, - це нова глава списку. Подібні стратегії існують і для інших типів структур даних - наборів, дерев, ви їх називаєте.

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


Нічого собі, одна невелика відповідь і стільки інформації / розуміння. Дякую :)
Геррі

3

Я знаю лише трохи про Clojure, і це незмінні структури даних .

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

Графічно ми можемо представити щось подібне:

(def my-list '(1 2 3))

    +---+      +---+      +---+
    | 1 | ---> | 2 | ---> | 3 |
    +---+      +---+      +---+

(def new-list (conj my-list 0))

              +-----------------------------+
    +---+     | +---+      +---+      +---+ |
    | 0 | --->| | 1 | ---> | 2 | ---> | 3 | |
    +---+     | +---+      +---+      +---+ |
              +-----------------------------+

2

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

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

Детальніше див., Наприклад, "Очистити домашню сторінку" та цю статтю у Вікіпедії

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