在映射列表时每次迭代后是否释放内存?

时间:2017-11-11 12:28:12

标签: list memory f#

很抱歉,如果这是初学者的问题,但我需要确定。

当调用函数时,它可能会创建临时对象,其内存分配应在退出时释放。

我的问题是:当一个函数映射到一个列表时,每个函数调用分配的内存是立即释放还是仅在整个列表被处理后才释放?

这是一个例子,除了在每个函数调用中创建了两个对象(newList和newRec)之外,代码具体没有意义。

分配给newList和newRec的内存是否会在每次"迭代后被释放"或者只有在调用List.map退出后才会释放所有内存?

这可能很容易理解为命令式语言中的循环,但我不知道F#编译器如何处理这种情况。

type MyRecord = { AList: int list; Name: string }
let myRecord = { AList = [1..100]; Name = "SomeRecord" }
let foo (arec: MyRecord) i =
    let newList = arec.AList |> List.filter (fun x -> x >= i)
    let newRec = { arec with AList = newList }
    List.sum newRec.AList
let res = [1..100] |> List.map (foo myRecord)

2 个答案:

答案 0 :(得分:5)

都不是。 F#基于automatic memory management garbage collection。导致释放内存块的原因不是语法条件,而是运行时条件。内存块在无法访问后被释放。

如果有办法从当前范围中的变量获取对象,则可以访问该对象。函数foo正在执行时,newListnewRec可以访问,因此不会释放它们。当函数返回时,newListnewRec不再可以直接访问,但是使它们可以释放的原因是它们不再间接可达。请考虑foo的以下变体:

let bar (arec: MyRecord) i =
    let newList = arec.AList |> List.filter (fun x -> x >= i)
    let newRec = { arec with AList = newList }
    newRec.AList

bar返回时,newRec对象不再可访问,但newList对象仍然存在,因为它由函数返回,因此可以由函数的调用者使用。

自动内存管理意味着您不必关心对象的生命周期。特别是,不可能尝试访问已被释放的对象¹:通过构造,如果你可以访问一个对象,它是可以访问的,因此不会被释放。

foo的特定情况下,只要对foo的调用返回,它创建的newRecnewList对象就无法访问。这并不一定意味着它们会立即被释放;最迟,它们将在下一个完整的垃圾收集器运行期间被释放。在没有被释放的情况下保留多长时间无法到达的对象是垃圾收集器质量的问题;它是内存使用和性能之间的折衷(运行GC经常会留下很少的未收集的垃圾,但会花费CPU时间;运行GC很少消耗很少的CPU时间,但会留下大量未收集的垃圾)。

在任何情况下,您通过foo多次调用List.map这一事实与内存管理无关。 <{1}}返回时没有什么特别的事情发生。

¹除了与使用非托管内存的其他语言编写的代码交互。

答案 1 :(得分:4)

这真是一个关于.NET而不是F#的问题。 F#编译器可以做一些事情来增加或减少分配/释放,但只要它不会产生持有超过必要时间的引用的代码,.NET应该能够释放(垃圾收集)未使用的内存。对于像List.map这样的简单函数,情况绝对应该如此。

什么时候发布内存的问题是一个非常复杂的问题,在像.NET这样的平台上,它有一个先进的垃圾收集器,多年来已经用很多工程资源进行了精细调整。这是我个人甚至无法回答的问题。但是,我可以通过这个简单的实验来展示一些东西。

在我们的映射功能中,让我们创建一个包含一百万个项目的列表并返回其长度:

[1 .. 100] |> List.map (fun _ -> [1 .. 1_000_000].Length)

当我们将这个功能映射到包含100个项目的列表时,它会成功运行,我会在我的机器上得到一个结果。

现在让我们在映射函数中返回实际的百万长列表,并再次在一百长的列表中运行它:

[1 .. 100] |> List.map (fun _ -> [1 .. 1_000_000])

这导致例外:Exception of type 'System.OutOfMemoryException' was thrown.

这表明我们不能在内存中容纳100个长度为100万的列表。但是如果第一段代码能够运行,那么一次只能在内存中容纳至少100万个列表。因此,我们可以推断在List.map完成处理所有项目之前必须进行一些中间列表的垃圾收集。但是,它并不一定会在每次迭代后立即发生。