F# 脚本/F# 交互中的性能调整
Posted
技术标签:
【中文标题】F# 脚本/F# 交互中的性能调整【英文标题】:Performance tuning in F# scripts / F# interactive 【发布时间】:2013-06-15 17:55:05 【问题描述】:我需要将任意整数(即bigint
)转换为其数字,以便通过索引访问它们。
不过,我发现自己在两种可能的算法实现之间徘徊:
open System
let toDigits (x:bigint) =
x.ToString()
|> Seq.map (fun c -> (int c) - (int '0'))
|> Seq.toArray
let toDigits' (x:bigint) =
seq
let x' = ref x
while !x' <> 0I do
yield (int (!x' % 10I))
x' := !x' / 10I
|> Seq.toArray |> Seq.rev
我喃喃自语的最快的是哪一个?为了帮助我回答这个问题,我设计了一个简单的profile
方法
let profile f times =
let x = ref 0
while !x < times do
incr x
f (bigint !x) |> ignore
当与 F# Interactive 的 #time
合并时产生以下输出:
> profile toDigits 10000000;;
Real: 00:00:11.609, CPU: 00:00:11.606, GC gen0: 825, gen1: 1, gen2: 0
val it : unit = ()
> profile toDigits' 10000000;;
Real: 00:00:28.891, CPU: 00:00:28.844, GC gen0: 1639, gen1: 3, gen2: 0
val it : unit = ()
这清楚地表明了toDigit
的优越性。不过,我想知道为什么,所以我问我的 F# 溢出者从现在开始我应该怎么做。
在一个典型的 Java 程序中,我只需启动一个分析器(例如 jvisualvm)并让它告诉我在某种 CPU 采样视图中哪些是热门方法。我想这与我在常规项目中使用 .fs 文件进行开发的方式完全相同。当我在 F# Interactive 中时,我有点迷茫。我应该将 .NET 分析器(在本例中为 ANTS 内存分析器)附加到 F# Interactive 吗?这种软件开发有什么特殊的工作流程吗?
【问题讨论】:
【参考方案1】:也许这不是您要寻找的答案,但在函数式编程的世界中,为任务选择正确的方法(算法、数据结构等)可能会带来更多比 .NET 分析器提供的任何彻底的类似 OOP 的微级别代码分析所带来的好处。
为了说明我的观点,在您的演示案例中,很难证明在 toDigits
和 toDigits'
函数中都使用 序列 进行操作。如果我们选择留在 array 空间内,则等效功能可以表示为
let toDigits'' (x: bigint) =
x.ToString().ToCharArray() |> Array.map (fun c -> int(c) - int('0'))
现在转向我们可以观察到的 FSI 提供的分析
> profile toDigits 10000000;;
Real: 00:00:13.020, CPU: 00:00:13.000, GC gen0: 1649, gen1: 2, gen2: 0
> profile toDigits'' 10000000;;
Real: 00:00:02.334, CPU: 00:00:02.343, GC gen0: 604, gen1: 1, gen2: 0
因此,我们获得了 5.6 倍的加速,这仅仅是因为更好地选择了要操作的数据结构,而 FSI 分析器正是对这一事实的确认。 p>
【讨论】:
非常有见地的答案:),尽管正如您已经指出的那样,这并不是我(中心)问题的真正答案。在这篇文章中,我特别关注专业人士在 F# / F# Interactive 中开发算法时通常的工作流程,而不是本身的良好算法实现。 为了指出专业人士的 OOP 和 FP 工作流程可能完全不同,我费心回答。当我想查看 F# 生成的 IL 时会发生这种情况,但这些情况很少发生…… 这是最好的答案,因为它指出在使用 F# 时应该修改通常的 OOP 工作流程。【参考方案2】:在一个典型的 Java 程序中,我只需启动一个分析器(例如 jvisualvm)并让它告诉我在某种 CPU 采样视图中哪些是热门方法。我想这与我在常规项目中使用 .fs 文件进行开发的方式完全相同。当我在 F# Interactive 中时,我有点迷茫。
使用fsx
文件和F# Interactive 并不意味着您应该完全放弃编译项目。我会将File Properties
中的Build Action
更改为Compile
,以便直接编译fsx 文件。我们需要在某些地方使用条件编译,例如
#if INTERACTIVE
#time "on";;
#endif
编译代码的优点是:
您可以使用 Visual Studio 分析器获取有关程序中热点的统计信息。 您可以使用 ILSpy 反编译程序。有时,IL 甚至等效的 C# 代码可以让您深入了解程序的行为方式。随着代码的发展,您可以考虑将核心函数移动到 fs
文件中,并将快速分析函数保留在 fsx
文件中。
回到你的例子,toDigits'
的改进是避免使用引用:
let toDigits'' (x:bigint) =
let rec loop x acc =
if x <> 0I then
loop (x/10I) (int(x%10I)::acc)
else acc
loop x [] |> List.toArray
结果显示,toDigits''
比 toDigits'
快 1.5 倍,比 toDigits
慢 1.5 倍。
很难击败toDigit
,因为它不对bigint
使用任何算术运算。 toDigit
的一个明显缺点是它在否定 bigint
s 上给出毫无意义的结果。
【讨论】:
只是为了确保:在常规构建中将我的代码编译为标准 .fs 文件,而不是通过 F# Interactive 运行,我不会有任何优势,对吧?无论如何,F# Interactive 总是会编译我传递的代码,因此性能将完全相同。以上是关于F# 脚本/F# 交互中的性能调整的主要内容,如果未能解决你的问题,请参考以下文章