함수형 프로그래밍에서 대부분의 데이터 구조를 변경 불가능하게하려면 더 많은 메모리 사용이 필요합니까?


63

함수형 프로그래밍에서는 거의 모든 데이터 구조가 불변이므로 상태를 변경해야 할 때 새 구조가 생성됩니다. 이것이 더 많은 메모리 사용량을 의미합니까? 객체 지향 프로그래밍 패러다임을 잘 알고 있습니다. 이제 함수형 프로그래밍 패러다임에 대해 배우려고합니다. 불변의 모든 것의 개념은 나를 혼란스럽게한다. 불변 구조를 사용하는 프로그램은 가변 구조를 가진 프로그램보다 훨씬 더 많은 메모리를 필요로하는 것처럼 보입니다. 나는 이것을 올바른 방법으로보고 있습니까?


7
이는 대부분의 불변 데이터 구조가 변경에 기본 데이터를 재사용한다는 것을 의미 할 수 있습니다 . Eric Lippert는 C #의 불변성에
Oded

3
순전히 기능적인 데이터 구조를 살펴볼 것입니다.이 책은 Haskell의 대부분의 컨테이너 라이브러리를 작성한 동일한 사람이 쓴 훌륭한 책입니다 (책은 주로 SML 임)
jozefg

1
메모리 소비 대신 실행 시간 과 관련된이 답변은 다음과 같이 흥미로울 것입니다. stackoverflow.com/questions/1990464/…
9000

답변:


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::vectorC ++에서 와 같은 일종의 동적 재배치 컨테이너에 래핑 됨)로 이동 목록 데이터 구조 인 경우 기능적 언어는 종종 연결된 목록을 선호합니다. 연결된 목록을 사용하면 선행 작업 ( 'cons')이 기존 목록을 새 목록의 꼬리로 재사용 할 수 있으므로 실제로 할당되는 것은 새 목록 헤드입니다. 다른 유형의 데이터 구조 (세트, 트리)에도 비슷한 전략이 있습니다.

그리고 게으른 평가, la Haskell이 있습니다. 아이디어는 생성 한 데이터 구조가 즉시 완전히 생성되지는 않는다는 것입니다. 대신, "thunks"로 저장됩니다 (필요한 경우 값을 구성하기위한 레시피로 생각할 수 있음). 값이 필요할 때만 썽크가 실제 값으로 확장됩니다. 즉, 평가가 필요할 때까지 메모리 할당을 연기 할 수 있으며이 시점에서 여러 메모리를 하나의 메모리 할당에 결합 할 수 있습니다.


와우, 하나의 작은 답변과 많은 정보 / 통찰력. 감사합니다 :)
Gerry

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 프로그래밍 언어를 언급하고 싶습니다 . 이 언어를 모르지만 고유 한 유형이 일종의 "파괴적인 업데이트"를 지원한다고 가정합니다.

즉, 상태를 업데이트하는 의미론은 함수를 적용하여 이전 값에서 새 값을 작성한다는 점이지만 고유성 제한 조건은 컴파일러가 이전 값이 참조되지 않음을 알기 때문에 내부적으로 데이터 오브젝트를 재사용 할 수있게합니다. 새로운 가치가 창출 된 후 프로그램에서 더 이상.

자세한 내용 은 Clean 홈페이지 및이 Wikipedia 기사를 참조하십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.