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 的微级别代码分析所带来的好处。

为了说明我的观点,在您的演示案例中,很难证明在 toDigitstoDigits' 函数中都使用 序列 进行操作。如果我们选择留在 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 的一个明显缺点是它在否定 bigints 上给出毫无意义的结果。

【讨论】:

只是为了确保:在常规构建中将我的代码编译为标准 .fs 文件,而不是通过 F# Interactive 运行,我不会有任何优势,对吧?无论如何,F# Interactive 总是会编译我传递的代码,因此性能将完全相同。

以上是关于F# 脚本/F# 交互中的性能调整的主要内容,如果未能解决你的问题,请参考以下文章

使用ImageMagick调整索引的PNG图像大小,同时保留颜色贴图

调整 cassandra 中的写入性能

调整 UIImage 大小 - 性能问题

如何对Oracle sql 进行性能优化的调整

08-leveldb性能优化

08-leveldb性能优化