在编写可以接受currying的函数时,可以将其编写为返回函数的单参数函数。例如,
let add x =
let inner y = x + y
inner
因此您可以执行以下操作:
add 3 4
或:
let add3 = add 3
add3 4
我的问题是,因为您返回了一个函数,所以从概念上讲,您两次调用了一个函数(外部函数和内部函数)。这比以下慢吗?
let add x y = x + y
还是编译器优化了当前定义中add 3 4
的调用?
答案 0 :(得分:12)
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#的语言中,函数在概念上始终采用单个值返回单个值。值可能是函数。这样更容易看出是否有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.
为了使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
可能比上面更复杂的实现,可能会根据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
是否确实可以优化,然后返回有效的适配器。在一般情况下,它可能仍然会慢一些,但这就是呼叫者想要的。
注意;对于像'T -> 'U
这样的简单函数值,这种潜在的性能下降不会发生。始终可以有效地调用它。
希望这会有所帮助。
答案 1 :(得分:10)
我在LINQPad 5中对此进行了测试。
关闭编译器优化后,F#编译器将为每个片段生成不同的IL。换句话说,如果正在进行任何优化,则由JITter负责,调用第一种形式可能会很慢。
但是,当启用编译器优化时,在我想到进行测试的每种情况下,两种形式都会产生相同的IL输出。实际上,使用这两种形式,都可以调用:
add 3 4
产生与硬编码的7
等效的IL,并且整个函数调用都已优化:
ldc.i4.7
换句话说,在优化逻辑上相同的代码块时,F#编译器非常彻底。
当然,这不是一个详尽的答案,在某些情况下,编译器实际上会对它们进行不同的处理。