在函数式编程中,具有大多数不变的数据结构是否需要更多的内存使用?


63

在函数式编程中,由于几乎所有数据结构都是不可变的,因此当必须更改状态时,会创建一个新的结构。这是否意味着更多的内存使用量?我非常了解面向对象的编程范例,现在我正在尝试学习功能编程范例。一切不变的概念使我感到困惑。与使用可变结构的程序相比,使用不可变结构的程序似乎需要更多的内存。我什至以正确的方式看着这个吗?


7
可能意味着,但是大多数不可变的数据结构将基础数据重新用于更改。埃里克利珀大约有一个伟大的博客系列在C#中永恒
奥德

3
我看一看纯粹的功能数据结构,这是一本很棒的书,是由编写Haskell大部分容器库的人写的(尽管这本书主要是SML)
jozefg 2012年

1
这样的回答,关系到运行时间,而不是内存的消耗,也可能对你有意思:stackoverflow.com/questions/1990464/...
9000

Answers:


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 2012年

7

天真的实现确实会暴露此问题-当您创建一个新的数据结构而不是就地更新现有数据结构时,您将不得不承担一些开销。

不同的语言有不同的处理方式,大多数使用一些技巧。

一种策略是垃圾收集。创建新结构的那一刻或不久之后,对旧结构的引用就超出了范围,垃圾回收器将立即或足够快地将其拾取,具体取决于GC算法。这意味着尽管仍有开销,但它只是暂时的,不会随数据量线性增长。

另一个正在选择不同种类的数据结构。数组是命令式语言(通常用某种动态重新分配容器(例如std::vectorC ++)包装)中的常用列表数据结构,而功能性语言通常更喜欢链接列表。通过链接列表,前置操作('cons')可以将现有列表重用作为新列表的尾部,因此真正分配的只是新列表的头。对于其他类型的数据结构也存在类似的策略-集,树(随便命名)。

然后是懒惰的评估,Haskell。这个想法是,您创建的数据结构不会立即完全创建。而是将它们存储为“ thunk”(您可以将它们视为在需要时构造值的配方)。仅当需要该值时,该thunk才会扩展为实际值。这意味着可以将内存分配推迟到需要评估之前,此时,可以在一个内存分配中组合多个thunk。


哇,一个小答案,那么多的信息/见解。谢谢您:)
Gerry 2015年

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编程语言,该语言支持所谓的unique类型。我不懂这种语言,但我想唯一类型支持某种“破坏性更新”。

换句话说,虽然更新状态的语义是您通过应用函数从旧值创建新值,但是唯一性约束可以允许编译器在内部重用数据对象,因为它知道不会引用旧值在产生新值之后,程序中将不再有其他内容。

有关更多详细信息,请参见例如“清洁”主页和此维基百科文章。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.