使用currying会导致F#性能下降吗?

Cth*_*utu 12 f# currying

编写可以接受currying的函数时,可以将其写为返回函数的单参数函数.例如,

let add x =
    let inner y = x + y
    inner
Run Code Online (Sandbox Code Playgroud)

所以你可以这样做:

add 3 4
Run Code Online (Sandbox Code Playgroud)

要么:

let add3 = add 3
add3 4
Run Code Online (Sandbox Code Playgroud)

我的问题是,因为你返回一个函数,你在概念上调用了一个函数两次(外部函数和内部函数).这比这慢吗:

let add x y = x + y
Run Code Online (Sandbox Code Playgroud)

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

Jus*_*mer 12

let f x   = fun y -> x + y
let g x y = x + y
Run Code Online (Sandbox Code Playgroud)

在dnSpy中查看这些函数定义以获得优化的构建,可以看出它们是:

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

public static int g(int x, int y)
{
    return x + y;
}
Run Code Online (Sandbox Code Playgroud)

这并不奇怪,因为g实际上是一个简短的定义,f这是一般情况.在F#类语言中,函数在概念上总是采用单个值返回单个值.值可能是函数.如果一个人为这个函数签名保留了f,那就更容易看到了g

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.
Run Code Online (Sandbox Code Playgroud)

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

public static int f(int x, int y)
Run Code Online (Sandbox Code Playgroud)

虽然这是F#功能的更自然的表示.

public static Func<int, int> f(int x)
Run Code Online (Sandbox Code Playgroud)

虽然表现不佳.

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

想象一下,你正在实施 fold

let rec fold f s vs =
  match vs with
  | v::vs -> fold f (f s v) vs
  | []    -> s
Run Code Online (Sandbox Code Playgroud)

这里F#无法完全优化f s v.原因是f可能有一个比上面更复杂的实现,可能会返回一个不同的函数取决于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
Run Code Online (Sandbox Code Playgroud)

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

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

希望这可以帮助.


p.s*_*w.g 10

我在LINQPad 5中测试了这个.

关闭编译器优化后,F#编译器将为每个片段生成不同的IL.换句话说,如果有任何优化正在进行,那么它将由JITter决定,调用第一个表单可能会慢得多.

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

add 3 4
Run Code Online (Sandbox Code Playgroud)

产生IL等价的硬编码7,整个函数调用被优化掉:

ldc.i4.7
Run Code Online (Sandbox Code Playgroud)

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

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