【问题标题】:Performance of F# code terribleF# 代码的性能很糟糕
【发布时间】:2012-06-21 16:43:11
【问题描述】:

这是我的第一个 F# 程序。我想我会实施康威的生命游戏作为第一个练习。

请帮助我理解为什么下面的代码性能如此糟糕。

let GetNeighbours (p : int, w : int, h : int) : seq<int> =
    let (f1, f2, f3, f4) = (p > w, p % w <> 1, p % w <> 0, p < w * (h - 1))
    [
    (p - w - 1, f1 && f2);
    (p - w, f1);
    (p - w + 1, f1 && f3);
    (p - 1, f2);
    (p + 1, f3);
    (p + w - 1, f4 && f2);
    (p + w, f4);
    (p + w + 1, f4 && f3)
    ]
    |> List.filter (fun (s, t) -> t)
    |> List.map (fun (s, t) -> s)
    |> Seq.cast

let rec Evolve (B : seq<int>, S : seq<int>, CC : seq<int>, g : int) : unit =
    let w = 10
    let h = 10
    let OutputStr = (sprintf "Generation %d:  %A" g CC) // LINE_MARKER_1
    printfn "%s" OutputStr
    let CCN = CC |> Seq.map (fun s -> (s, GetNeighbours (s, w, h)))
    let Survivors =
        CCN
        |> Seq.map (fun (s, t) -> (s, t |> Seq.map (fun u -> (CC |> Seq.exists (fun v -> u = v)))))
        |> Seq.map (fun (s, t) -> (s, t |> Seq.filter (fun u -> u)))
        |> Seq.map (fun (s, t) -> (s, Seq.length t))
        |> Seq.filter (fun (s, t) -> (S |> Seq.exists (fun u -> t = u)))
        |> Seq.map (fun (s, t) -> s)
    let NewBorns =
        CCN
        |> Seq.map (fun (s, t) -> t)
        |> Seq.concat
        |> Seq.filter (fun s -> not (CC |> Seq.exists (fun t -> t = s)))
        |> Seq.groupBy (fun s -> s)
        |> Seq.map (fun (s, t) -> (s, Seq.length t))
        |> Seq.filter (fun (s, t) -> B |> Seq.exists (fun u -> u = t))
        |> Seq.map (fun (s, t) -> s)
    let NC = Seq.append Survivors NewBorns
    let SWt = new System.Threading.SpinWait ()
    SWt.SpinOnce ()
    if System.Console.KeyAvailable then
        match (System.Console.ReadKey ()).Key with
        | System.ConsoleKey.Q -> ()
        | _ -> Evolve (B, S, NC, (g + 1))
    else 
        Evolve (B, S, NC, (g + 1))

let B = [3]
let S = [2; 3]
let IC = [4; 13; 14]
let g = 0
Evolve (B, S, IC, g)

前五次迭代,即第 0、1、2、3、4 代,毫无问题地发生。然后,在大约 100 毫秒的短暂暂停后,第 5 代完成。但在那之后,程序挂在标记为“LINE_MARKER_1”的行处,如断点 Visual Studio 所示。它永远不会到达printfn 行。

奇怪的是,到了第 2 代,函数Evolve 中的CC 序列已经稳定到序列[4; 13; 14; 3],所以我认为第6 代没有理由不能进化。

我知道粘贴大段代码并在调试时寻求帮助通常被认为是令人反感的,但我不知道如何将其减少到最低限度的工作示例。任何可以帮助我调试的指针都将不胜感激。

提前感谢您的帮助。

编辑

我真的相信任何希望帮助我的人都可能会忽略GetNeighbours 函数。为了完整起见,我将其包括在内。

【问题讨论】:

  • @Shredderroy : Seq 用于惰性评估,您几乎肯定不希望这样做。
  • F# 序列没有提供不错的性能。使用Seq.lengthSeq.append 会使性能更差。
  • 另外 - 请注意您不需要Seq.cast(它不会像您认为的那样做)。将 |&gt; Seq.cast 替换为 :&gt; _ 可以满足您的要求,但仍会检查类型。你也不能投到seq
  • @RamonSnir 是的,List 是解决O(exp(N)) 的一种方法。但是我认为更大的问题是不理解 Seq。就建议而言,“从不使用 Seq”并不是很有教育意义 :)
  • @Shredderroy:由于局部性,数组的性能将优于Seqs,甚至列出此类处理。

标签: performance recursion f# sequences


【解决方案1】:

修复性能的最简单方法是使用Seq.cache

let GetNeighbours (p : int, w : int, h : int) : seq<int> =
    let (f1, f2, f3, f4) = (p > w, p % w <> 1, p % w <> 0, p < w * (h - 1))
    [
    (p - w - 1, f1 && f2);
    (p - w, f1);
    (p - w + 1, f1 && f3);
    (p - 1, f2);
    (p + 1, f3);
    (p + w - 1, f4 && f2);
    (p + w, f4);
    (p + w + 1, f4 && f3)
    ]
    |> List.filter (fun (s, t) -> t)
    |> List.map (fun (s, t) -> s)
    :> seq<_> // <<<<<<<<<<<<<<<<<<<<<<<< MINOR EDIT, avoid boxing

let rec Evolve (B : seq<int>, S : seq<int>, CC : seq<int>, g : int) : unit =
    let w = 10
    let h = 10
    let OutputStr = (sprintf "Generation %d:  %A" g CC) // LINE_MARKER_1
    printfn "%s" OutputStr
    let CCN =
        CC
        |> Seq.map (fun s -> (s, GetNeighbours (s, w, h)))
        |> Seq.cache // <<<<<<<<<<<<<<<<<< EDIT
    let Survivors =
        CCN
        |> Seq.map (fun (s, t) -> (s, t |> Seq.map (fun u -> (CC |> Seq.exists (fun v -> u = v)))))
        |> Seq.map (fun (s, t) -> (s, t |> Seq.filter (fun u -> u)))
        |> Seq.map (fun (s, t) -> (s, Seq.length t))
        |> Seq.filter (fun (s, t) -> (S |> Seq.exists (fun u -> t = u)))
        |> Seq.map (fun (s, t) -> s)
    let NewBorns =
        CCN
        |> Seq.map (fun (s, t) -> t)
        |> Seq.concat
        |> Seq.filter (fun s -> not (CC |> Seq.exists (fun t -> t = s)))
        |> Seq.groupBy (fun s -> s)
        |> Seq.map (fun (s, t) -> (s, Seq.length t))
        |> Seq.filter (fun (s, t) -> B |> Seq.exists (fun u -> u = t))
        |> Seq.map (fun (s, t) -> s)
    let NC =
        Seq.append Survivors NewBorns
        |> Seq.cache // <<<<<<<<<<<<<<<<<< EDIT
    let SWt = new System.Threading.SpinWait ()
    SWt.SpinOnce ()
    if System.Console.KeyAvailable then
        match (System.Console.ReadKey ()).Key with
        | System.ConsoleKey.Q -> ()
        | _ -> Evolve (B, S, NC, (g + 1))
    else
        Evolve (B, S, NC, (g + 1))

let B = [3]
let S = [2; 3]
let IC = [4; 13; 14]
let g = 0
Evolve (B, S, IC, g)

最大的问题不是使用Seq 本身,问题是正确使用它。默认情况下,序列不是惰性的,而是定义了在每次遍历时重新评估的计算。这意味着除非您对此采取措施(例如 Seq.cache),否则重新评估序列可能会破坏程序的算法复杂性。

您的原始程序具有指数级复杂性。要看到这一点,请注意它每次迭代都会使遍历的元素数量翻倍。

还请注意,在您的编程风格中,使用Seq 运算符后跟Seq.cache 比使用ListArray 运算符有一个小的优势:这可以避免分配中间数据结构,从而降低GC 压力并可能加快事情有点过了。

【讨论】:

  • 太棒了!通过这两个小的编辑,我在 11428 毫秒内获得了 10,000 次进化。这比带有列表的版本稍快,后者需要 12719 毫秒来进行 10,000 次进化。现在我将尝试大卫的建议并使用数组。
  • 我认为这真的不是关于速度,而是关于复杂性。您的原始代码的算法复杂性很差。所有建议的解决方案都具有相同的复杂性 - 时间差异对您的用例(以及大多数用例)来说并不重要。
【解决方案2】:

查看 cmets 和所有内容,但这段代码运行得非常糟糕 - 使用 List.* 和其他一些较小的优化:

let GetNeighbours p w h =
    let (f1, f2, f3, f4) = p > w, p % w <> 1, p % w <> 0, p < w * (h - 1)
    [
        p - w - 1, f1 && f2
        p - w, f1
        p - w + 1, f1 && f3
        p - 1, f2
        p + 1, f3
        p + w - 1, f4 && f2
        p + w, f4
        p + w + 1, f4 && f3
    ]
    |> List.choose (fun (s, t) -> if t then Some s else None)

let rec Evolve B S CC g =
    let w = 10
    let h = 10
    let OutputStr = sprintf "Generation %d:  %A" g CC // LINE_MARKER_1
    printfn "%s" OutputStr
    let CCN = CC |> List.map (fun s -> s, GetNeighbours s w h)
    let Survivors =
        CCN
        |> List.choose (fun (s, t) ->
            let t =
                t
                |> List.filter (fun u -> CC |> List.exists ((=) u))
                |> List.length
            if S |> List.exists ((=) t) then
                Some s
            else None)
    let NewBorns =
        CCN
        |> List.collect snd
        |> List.filter (not << fun s -> CC |> List.exists ((=) s))
        |> Seq.countBy id
        |> List.ofSeq
        |> List.choose (fun (s, t) ->
            if B |> List.exists ((=) t) then
                Some s
            else None)
    let NC = List.append Survivors NewBorns
    let SWt = new System.Threading.SpinWait()
    SWt.SpinOnce()
    if System.Console.KeyAvailable then
        match (System.Console.ReadKey()).Key with
        | System.ConsoleKey.Q -> ()
        | _ -> Evolve B S NC (g + 1)
    else 
        Evolve B S NC (g + 1)

let B = [3]
let S = [2; 3]
let IC = [4; 13; 14]
let g = 0
Evolve B S IC g

【讨论】:

  • 我会在这里使用Seq.countBy 而不是Seq.groupBy
  • 您可以通过切换到数组将运行时间缩短一半。
  • @Daniel 数组确实似乎更快。
【解决方案3】:

只是想我会添加一个简单的答案,以防像我这样的其他初学者遇到同样的问题。

根据上述Ramon Snirildjarnpad 的建议,我将Seq.X 调用更改为List.X。我不得不添加一个简单的额外转换步骤来解释List 没有groupBy 的事实,但是这样做之后,代码现在运行起来就像一个魅力!

非常感谢。

【讨论】:

    【解决方案4】:

    ML 系列语言最令人惊奇的特征之一是短代码通常是快速代码,这也适用于 F#。

    将您的实现与我在博客 here 上更快的实现进行比较:

    let count (a: _ [,]) x y =
      let m, n = a.GetLength 0, a.GetLength 1
      let mutable c = 0
      for x=x-1 to x+1 do
        for y=y-1 to y+1 do
          if x>=0 && x<m && y>=0 && y<n && a.[x, y] then
            c <- c + 1
      if a.[x, y] then c-1 else c
    
    let rule (a: _ [,]) x y =
      match a.[x, y], count a x y with
      | true, (2 | 3) | false, 3 -> true
      | _ -> false
    

    【讨论】:

    • 非常感谢分享!是的,这更短更快。我能提出的唯一辩护是我的实现将“板”与进化分开。在我的源代码(此处未显示)中,我有以下“板”:“硬正方形”(边缘截断的正方形)、圆柱体、genus-1 环面、RP2。我也想写一些其他的板,具有有趣的低维拓扑。我一直在尝试了解如何改进我的代码。
    • @Shredderroy 我明白了。当我第一次看到您的代码时,我感到非常困惑,但如果它被设计为在拓扑之上是通用的,那么这是有道理的。我在博士期间做了类似的事情来研究网络拓扑对非晶材料模拟的影响。
    猜你喜欢
    • 1970-01-01
    • 2021-04-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-26
    • 2013-02-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多