在函数之间传递大型数据结构的 F# 效率影响

F# efficiency implications of passing large data structures between functions

F# 如何将数据从调用函数传递到被调用函数?它是在移交数据之前复制数据还是只是传递一个指针?我会认为后者,但想确定一下。 在相关说明中,以下 2 种 F# 代码样式是否对性能有任何影响。

let someFunction e =
    1//pretend this is a complicated function

let someOtherFunction e =
    2//pretend this is a complicated function

let foo f largeList=
    List.map (fun elem -> f elem)

let bar largeList =
    largeList
    |> foo someFunction
    |> foo someOtherFunction


let bar2 largeList =
    let foo2 f f2 =
        largeList
        |> List.map (fun elem -> f elem)
        |> List.map (fun elem -> f2 elem)
    foo2 someFunction someOtherFunction

您希望 bar 的性能与 bar2 不同吗?如果没有,我应该注意哪些情况会有所不同?

简答:

没有。不会复制整个列表,只是对它的引用。

长答案:

在 F# 中(就像在 C# 中一样)值和引用类型都可以通过值或引用传递。

默认情况下,值类型和引用类型都是按值传递的。

  • 对于值类型(结构),这意味着您将 传递整个数据结构的副本。

  • 在引用类型(类、可区分联合、记录等)的情况下,这意味着引用是按值传递的.这并不意味着复制了整个数据结构,它只是意味着复制了引用数据结构的int/int64

如果您正在使用可变数据结构,例如ResizeArray<'T> (.NET List<'T>) 类,按值传递引用可能会产生影响。例如,也许您传递给它的函数将元素添加到列表中?这样的更新将应用于从两个位置引用的数据结构。由于您的问题使用了不可变的 F# List,因此您不必担心这个!

您还可以通过引用传递 value/reference 类型,有关详细信息,请参阅:https://msdn.microsoft.com/en-us/library/dd233213.aspx#Anchor_4

F# list 是作为单链表实现的,这意味着访问头部和前置操作是 O(1)。这些数据结构的内存效率也很高,因为当您将一个元素添加到列表中时,您只需要存储新值和对列表其余部分的引用。

所以你可以看到它是如何工作的,这样的数据结构可以这样实现:

type ExampleList<'T> = 
    |Empty
    |Cons of 'T * List<'T>

附加信息:

List.map 被急切求值,这意味着每次调用它时,都会创建一个新列表。如果您使用 Seq.map(F# List 实现了 IEnumerable<'T> 接口),它是延迟计算的,您可以仅在列表的枚举中计算这两个映射操作。

largeList
|> Seq.map (fun elem -> f elem)
|> Seq.map (fun elem -> f2 elem)
|> List.ofSeq

这对于大型列表来说可能更有效,因为它只涉及分配一个新的结果列表,而不是两个。