使用柯里化会导致 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 单个值。值可能是函数。这更容易看出是否将 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
可能有一个比上面更复杂的实现,可能 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
这样的简单函数值,这种潜在的性能下降不会发生。这总是可以有效地调用。
希望这对您有所帮助。
在编写可以接受柯里化的函数时,您可以将其编写为 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 单个值。值可能是函数。这更容易看出是否将 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
可能有一个比上面更复杂的实现,可能 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
这样的简单函数值,这种潜在的性能下降不会发生。这总是可以有效地调用。
希望这对您有所帮助。