使用柯里化会导致 F# 中的性能降低吗?

Does using currying result in lower performance in F#?

在编写可以接受柯里化的函数时,您可以将其编写为 return 函数的单参数函数。例如,

let add x =
    let inner y = x + y
    inner

所以你可以这样做:

add 3 4

或:

let add3 = add 3
add3 4

我的问题是,因为你 return 一个函数,你在概念上调用了一个函数两次(外部函数和内部函数)。这是否慢于:

let add x y = x + y

或者编译器是否优化了柯里化定义中 add 3 4 的调用?

我在 LINQPad 5.

中对此进行了测试

关闭编译器优化后,F# 编译器将为每个代码段生成不同的 IL。换句话说,如果有任何优化正在进行,它留给了 JITter,调用第一种形式可能会更慢。

但是,当打开编译器优化时,两种形式在我能想到的每个场景中都会产生相同的 IL 输出来测试它。事实上,对于这两种形式,调用:

add 3 4

产生等同于硬编码 7 的 IL,并且优化了整个函数调用:

ldc.i4.7

换句话说,F# 编译器在优化逻辑上相同的代码块时非常彻底。

当然,这不是一个详尽的答案,在某些情况下,编译器实际上会对它们进行不同的处理。

let f x   = fun y -> x + y
let g x y = x + y

查看 dnSpy 中的这些函数定义以获得优化的构建表明它们是:

public static int f(int x, int y)
{
    return x + y;
}

public static int g(int x, int y)
{
    return x + y;
}

这并不奇怪,因为 g 实际上是 f 的简写定义,这是一般情况。在类似 F# 的语言中,函数在概念上总是采用单个值 returning 单个值。值可能是函数。这更容易看出是否将 fg

的函数签名括起来
val f: int -> int -> int
// Actually is
// val f: int -> (int -> int)
// ie f is a function that takes a single int and returns a function that takes a single int and returns an int.

为了让 F# 在 .NET 上执行得更快,f 在程序集中的物理表示是:

public static int f(int x, int y)

虽然这是 F# 函数的更自然表示。

public static Func<int, int> f(int x)

虽然表现不佳。

通常 F# 足够聪明,可以通过上面的优化和调用来避免抽象的开销。但是,有些情况下 F# 无法为您优化。

假设您正在实施 fold

let rec fold f s vs =
  match vs with
  | v::vs -> fold f (f s v) vs
  | []    -> s

此处 F# 无法完全优化 f s v。原因是 f 可能有一个比上面更复杂的实现,可能 return 一个不同的函数取决于 s.

如果您查看 dnSpy,您会注意到 F# 正在使用 InvokeFast 调用函数,但这会进行内部测试以查看是否可以快速调用它。在折叠中,我们然后对每个值进行此测试,即使这是相同的功能。

这就是为什么人们有时会看到 fold 这样写的原因:

let fold f s vs =
  let f = OptimizedClosures.FSharpFunc<_, _, _>.Adapt f
  let rec loop s vs =
    match vs with
    | v::vs -> loop (f.Invoke (s, v)) vs
    | []    -> s
  loop s vs

Adapt 在循环之前测试 f 是否确实可以优化,然后 return 是一个高效的适配器。在一般情况下,它可能仍然有点慢,但这就是调用者的意图。

注意;对于像 'T -> 'U 这样的简单函数值,这种潜在的性能下降不会发生。这总是可以有效地调用。

希望这对您有所帮助。