对F#进行性能分析以实现递归功能

时间:2018-12-01 23:03:06

标签: f# profiling

我决定使用F#以富有表现力的方式解决代码2018年问世第一天的第二个问题(执行循环求和并找到第一个重复的和),但是性能不足,我找不到减速的原因。

问题在Python 3中得以解决

For a given input,总计约140,000,此代码将在几秒钟内执行。

data = list(map(int, '''
+1
-1
'''.strip().splitlines()))
from itertools import cycle, accumulate
class superset(set):
    def add(self, other):
        super().add(other)
        return other

def mapwhile(func, pred, iterable):
    for i in iterable:
        if not pred(i):
            yield func(i)
            return
        yield func(i)

def last(iterable):
    return list(iterable)[-1]

s = superset([0])
print(last(mapwhile(s.add, lambda x: x not in s, accumulate(cycle(data)))))

像F#一样解决问题

我在match表达式上添加了一个条件断点,以每千分之一的时间i进行一次,看来这段代码的执行速度约为100次/秒,即使一个小时后也无法解决。大幅下降,幅度惊人。

let input = @"
+1
-1
"
let cycle xs = seq { while true do yield! xs }
let accumusum xs = Seq.scan(fun acc elem -> acc + elem) 0 xs

let rec findfreqcycle i (s:int Set) (data:int seq) = 
    let head, tail = Seq.head data, Seq.tail data
    match s.Contains(head) with
    | false -> findfreqcycle (i+1) (s.Add(head)) (tail)
    | true ->  head


let data = input.Trim().Split('\n') |> Seq.map int |> Seq.toList |> cycle
accumusum data |> findfreqcycle 0 Set.empty

据我所知,每个代码示例的核心思想或多或少都是相同的。 热切地仅对输入进行一次解析,并使用生成器函数/序列懒惰地重复每个数字。

唯一的区别是,在F#示例中,实际找到第一个重复求和的函数是递归的。内存分析表明内存使用率几乎保持不变,并且尾递归处于活动状态。

我可能在做错什么,如何更好地描述这些递归和生成函数的性能?

3 个答案:

答案 0 :(得分:5)

如注释中所述,Seq.tail效率极低,尤其是如果您以自己的方式循环使用它时。原因是它创建了一个在原始序列上进行迭代的新序列,并跳过了第一个元素(因此,在进行1000次迭代之后,您必须遍历1000个序列,每个序列都跳过一个元素)。

如果使用列表,则带有头部和尾部的模式会更好,因为功能列表是为这种处理而设计的。就您而言,您可以执行以下操作(遵循与原始功能相同的模式):

let rec findfreqcycle sum (s:int Set) input data = 
    match data with 
    | x::xs when s.Contains (sum + x) -> (sum + x)
    | x::xs -> findfreqcycle (sum + x) (s.Add (sum + x)) input xs
    | [] ->  findfreqcycle sum s input input

let data = input.Trim().Split('\n') |> Seq.map int |> Seq.toList 
findfreqcycle 0 Set.empty data data

我对其进行了更改,以使其使用模式匹配(在列表上)。我还更改了代码,以使其采用一个有限列表,并且当它到达结尾时,它又重新开始。结果,它也可以对数字进行求和(而不是使用Seq.scan-在这里不起作用,因为我没有使用无限列表)。

根据Pastebin的输入,我在大约0.17秒内得到了结果448。

答案 1 :(得分:5)

我决定根据Tomas的回答尝试使用Seq.scan和Seq.pick进行实现,并得到了这个结果。他是对的,这不是很好。从好的方面来看,它的执行时间约为0.3秒。

let cycle xs = seq { while true do yield! xs }    
let accumusum xs = Seq.scan(fun acc elem -> acc + elem) 0 xs

let tryfind (sum, s:int Set) =
    match s.Contains(sum) with
    | true -> Some(sum)
    | false -> None

let scanstate (sum, s:int Set) el =
    el, s.Add(sum)

let findfreqcycle (data:int seq) =
    let seen = Seq.scan scanstate (Seq.head data, Set.empty) (Seq.tail data)
    Seq.pick tryfind seen

let data = cycle <| (input.Trim().Split('\n') |> Seq.map int |> Seq.toList)
accumusum data |> findfreqcycle

答案 2 :(得分:2)

OP已经有一个可以接受的答案,但我想我提出了一些变体。

任务要求输入值上运行的聚合(Set),同时当Set处于由于我们已经看到而无法向其添加数字的状态时仍允许提前退出。

通常我们fold汇总一个州,但是fold不允许我们提早退出。因此,建议使用scan,它是允许提前退出的流式fold + pick

一种替代方法是编写一个fold,以便在达到某个状态时允许快捷方式:val foldAndCheck: (a' -> 'b -> CheckResult<'a, 'c>) -> 'a -> 'b seq -> 'c optionfold就像聚合所有值的for循环,foldAndCheck就像聚合值直到一个点然后返回结果的for循环。

它看起来可能像这样:

type [<Struct>] CheckResult<'T, 'U> =
  | Continue of c:'T
  | Done     of d:'U

// val foldAndCheck: (a' -> 'b -> CheckResult<'a, 'c>) -> 'a -> 'b seq -> 'c option
let foldAndCheck f z (s : _ seq) =
  let f = OptimizedClosures.FSharpFunc<_, _, _>.Adapt f
  use e = s.GetEnumerator ()
  let rec loop s =
    if e.MoveNext () then
      match f.Invoke (s, e.Current) with
      | Continue ss -> loop ss
      | Done     rr -> Some rr 
    else
      None
  loop z

let cycle xs = seq { while true do yield! xs }

let run (input : string) =
  let folder s v = if Set.contains v s then Done v else Continue (Set.add v s)
  input.Trim().Split('\n') 
  |> Seq.map int 
  |> cycle
  |> Seq.scan (+) 0
  |> foldAndCheck folder Set.empty

在我的机器上运行它时,我会得到如下数字:

Result: Some 448
Took  : 280 ms
CC    : (31, 2, 1)

(CC是第0、1和2代中的垃圾回收)

然后我创建了一个F#程序,我认为它等效于Python程序,因为它使用了可变集和mapWhile函数:

let addAndReturn (set : HashSet<_>) =
  fun v ->
    set.Add v |> ignore
    v

let mapWhile func pred (s : _ seq) =
  seq {
    // F# for v in s ->
    //  doesn't support short-cutting. So therefore the use while
    use e = s.GetEnumerator ()
    let mutable cont = true
    while cont && e.MoveNext () do
      let v = e.Current
      if not (pred v) then
        cont <- false
        yield func v
      else
        yield func v
  }

let cycle xs = seq { while true do yield! xs }

let accumulate xs = Seq.scan (+) 0 xs

let last xs = Seq.last xs

let run (input : string) =
  let data = input.Trim().Split('\n') |> Seq.map int 
  let s = HashSet<int> ()

  data
  |> cycle
  |> accumulate
  |> mapWhile (addAndReturn s) (fun x -> s.Contains x |> not)
  |> last

性能数字:

Result: 448
Took  : 50 ms
CC    : (1, 1, 1)

如果我们说允许突变+ seq,则解决方案如下所示:

let cycle xs = seq { while true do yield! xs }

let run (input : string) =
  let s = HashSet<int> ()

  input.Trim().Split('\n')
  |> Seq.map int 
  |> cycle
  |> Seq.scan (+) 0
  |> Seq.find (fun v -> s.Add v |> not)

运行如下:

Result: 448
Took  : 40 ms
CC    : (1, 1, 1)

还有一些很酷的技巧可以用来进一步提高搜索性能,但是这并不值得,因为此时大多数成本是解析整数。