F#:从seq中删除重复项很慢

时间:2016-04-06 18:50:55

标签: performance f#

我正在尝试编写一个函数,根据给定的相等函数确定连续的重复项,来自seq<'a>,但有一个扭曲:我需要从 last 复制一个运行重复项以使其成为结果序列。例如,如果我有序列[("a", 1); ("b", 2); ("b", 3); ("b", 4); ("c", 5)],并且我使用fun ((x1, y1),(x2, y2)) -> x1=x2来检查相等性,那么我想看到的结果是[("a", 1); ("b", 4); ("c", 5)]。这个函数的关键在于我有数据点进入,有时数据点合法地具有相同的时间戳,但我只关心最新的一个,所以我想抛弃前面的那些具有相同的时间戳。我实现的功能如下:

let rec dedupeTakingLast equalityFn prev s = seq {
     match ( Seq.isEmpty s ) with
     | true -> match prev with 
                 | None -> yield! s
                 | Some value -> yield value
     | false ->
         match prev with 
         | None -> yield! dedupeTakingLast equalityFn (Some (Seq.head s)) (Seq.tail s) 
         | Some prevValue -> 
             if not (equalityFn(prevValue, (Seq.head s))) then 
                 yield prevValue
             yield! dedupeTakingLast equalityFn (Some (Seq.head s)) (Seq.tail s)
}

就实际工作而言,它有效:

> [("a", 1); ("b", 2); ("b", 3); ("b", 4); ("c", 5)] 
  |> dedupeTakingLast (fun ((x1, y1),(x2, y2)) -> x1=x2) None 
  |> List.ofSeq;;
val it : (string * int) list = [("a", 1); ("b", 4); ("c", 5)]

但是,就性能而言,这是一场灾难:

> #time
List.init 1000 (fun _ -> 1) 
|> dedupeTakingLast (fun (x,y) -> x = y) None 
|> List.ofSeq
#time;;    
--> Timing now on    
Real: 00:00:09.958, CPU: 00:00:10.046, GC gen0: 54, gen1: 1, gen2: 1
val it : int list = [1]    
--> Timing now off
显然,我在这里做了一件非常愚蠢的事,但我看不出它是什么。性能受到何种影响?我意识到有更好的实现,但我特别感兴趣的是理解为什么这个实现是如此之慢。

编辑:仅供参考,设法在功能风格上实现了一个体面的实现,仅依赖于Seq.函数。性能还可以,使用迭代器下面的 @gradbot 占用命令式实现的时间大约是1.6倍。似乎问题的根源是在我最初的努力中在递归调用中使用Seq.headSeq.tail

let dedupeTakingLastSeq equalityFn s = 
    s 
    |> Seq.map Some
    |> fun x -> Seq.append x [None]
    |> Seq.pairwise
    |> Seq.map (fun (x,y) -> 
            match (x,y) with 
            | (Some a, Some b) -> (if (equalityFn a b) then None else Some a)  
            | (_,None) -> x
            | _ -> None )
    |> Seq.choose id

8 个答案:

答案 0 :(得分:6)

问题在于你如何使用序列。所有这些产量,头部和尾部都在旋转迭代器分支的迭代器网络,当你在调用List.ofSeq时最终实现它时,你将比你应该更多地迭代输入序列。< / p>

这些Seq.heads中的每一个都不是简单地采用序列的第一个元素 - 它采用序列尾部序列尾部序列的尾部的第一个元素,因此上。

检查一下 - 它会计算调用元素构造函数的次数:

let count = ref 0

Seq.init 1000 (fun i -> count := !count + 1; 1) 
|> dedupeTakingLast (fun (x,y) -> x = y) None 
|> List.ofSeq

顺便说一句,只需将所有Seqs切换为Lists即可立即启用。

答案 1 :(得分:5)

性能问题来自对Seq.tail的嵌套调用。这是Seq.tail

的源代码
[<CompiledName("Tail")>]
let tail (source: seq<'T>) =
    checkNonNull "source" source
    seq { use e = source.GetEnumerator() 
          if not (e.MoveNext()) then 
              invalidArg "source" (SR.GetString(SR.notEnoughElements))
          while e.MoveNext() do
              yield e.Current }

如果您致电Seq.tail(Seq.tail(Seq.tail(...))),编译器无法优化由GetEnumerator()创建的枚举器。后续返回的元素必须遍历每个嵌套序列和枚举器。这导致每个返回的元素必须在所有先前创建的序列中冒泡,并且所有这些序列也必须递增它们的内部状态。最终结果是运行时间为O(n ^ 2)而不是线性O(n)。

不幸的是,目前无法用F#中的功能样式表示这一点。您可以使用列表(x :: xs)但不能使用序列。在语言获得对序列的更好的原生支持之前,不要在递归函数中使用Seq.tail。

使用单个枚举器可以解决性能问题。

let RemoveDuplicatesKeepLast equals (items:seq<_>) =
    seq {
        use e = items.GetEnumerator()

        if e.MoveNext() then
            let mutable previous = e.Current

            while e.MoveNext() do
                if not (previous |> equals e.Current) then 
                    yield previous
                previous <- e.Current

            yield previous
    }

let test = [("a", 1); ("b", 2); ("b", 3); ("b", 4); ("c", 5)]
let FirstEqual a b = fst a = fst b

RemoveDuplicatesKeepLast FirstEqual test
|> printf "%A"

// output
// seq [("a", 1); ("b", 4); ("c", 5)]

此答案的第一个版本具有上述代码的递归版本而没有变异。

答案 2 :(得分:4)

Seq.isEmpty,Seq.head和Seq.tail很慢,因为它们都会创建一个新的Enumerator实例然后调用它。你最终得到了很多GC。

一般来说,序列只是前向的,如果你使用它们“就像列表的模式匹配”那样,性能就变得非常粗制。

看一下你的代码... | None -> yield! s创建一个新的枚举器,即使我们知道s是空的。每次递归调用可能最终会创建一个新的IEnumerable,然后直接从调用站点转换为带有yield的Enumerator!。

答案 3 :(得分:2)

正如其他答案所说,seq真的很慢。但是,真正的问题是为什么要在这里使用seq?特别是,您从列表开始,并且想要遍历整个列表,并且希望在最后创建新列表。除非您想使用序列特定功能,否则似乎没有任何理由可以使用序列。事实上,docs表示(强调我的):

  

序列是所有一种类型的逻辑系列元素。当您拥有大量有序的数据集时,序列特别有用,但 必然期望 使用所有元素 。单个序列元素仅在需要时计算,因此在不使用所有元素的情况下,序列可以提供比列表更好的性能。

答案 4 :(得分:2)

我也期待一个非seq的答案。这是另一个解决方案:

let t = [("a", 1); ("b", 2); ("b", 3); ("b", 4); ("c", 5)]
t |> Seq.groupBy fst |> Seq.map (snd >>  Seq.last)

我在1M列表上测试过:

Real: 00:00:00.000, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0
val it : seq<int * int> = seq [(2, 2); (1, 1)]

答案 5 :(得分:2)

为了有效地使用输入类型Seq,应该只迭代每个元素一次,避免创建其他序列。

另一方面,为了有效地使用输出类型List,应该自由使用constail函数,这两个函数基本上都是免费的。< / p>

结合这两个要求使我得到了这个解决方案:

// dedupeTakingLast2 : ('a -> 'a -> bool) -> seq<'a> -> 'a list
let dedupeTakingLast2 equalityFn = 
  Seq.fold 
  <| fun deduped elem ->     
       match deduped with
       | [] -> [ elem ]
       | x :: xs -> if equalityFn x elem 
                      then elem :: xs
                      else elem :: deduped
  <| []

但请注意,由于列表前置,输出列表将处于逆序中。我希望这不是一个破坏者,因为List.rev是一个相对昂贵的操作。

测试:

List.init 1000 (id) 
|> dedupeTakingLast2 (fun x y -> x - (x % 10) = y - (y % 10))
|> List.iter (printfn "%i ")

// 999 989 979 969 etc...

答案 6 :(得分:2)

这里有一个老问题,但我只是在寻找旧示例来演示我一直在研究的新库。它是System.Linq.Enumerable的替代品,但是它具有包装程序来替代F#的Seq。尚未完成,但需要进行polyfill匹配现有的API(即,不完整的材料只会转发到现有功能)。

可在以下位置的nuget中找到它:https://www.nuget.org/packages/Cistern.Linq.FSharp/

因此,我从答案的底部提取了修改后的Seq,并将其“转换”为Cistern.Linq.FSharp(这只是对“ Leq”的“ Seq。”进行搜索和替换),然后将其进行比较运行时恢复到原始状态。 Cistern版本的运行时间不到50%(我得到了约41%)。

open System
open Cistern.Linq.FSharp
open System.Diagnostics

let dedupeTakingLastCistern equalityFn s = 
    s 
    |> Linq.map Some
    |> fun x -> Linq.append x [None]
    |> Linq.pairwise
    |> Linq.map (fun (x,y) -> 
            match (x,y) with 
            | (Some a, Some b) -> (if (equalityFn a b) then None else Some a)  
            | (_,None) -> x
            | _ -> None )
    |> Linq.choose id

let dedupeTakingLastSeq equalityFn s = 
    s 
    |> Seq.map Some
    |> fun x -> Seq.append x [None]
    |> Seq.pairwise
    |> Seq.map (fun (x,y) -> 
            match (x,y) with 
            | (Some a, Some b) -> (if (equalityFn a b) then None else Some a)  
            | (_,None) -> x
            | _ -> None )
    |> Seq.choose id

let test data which f =
    let iterations = 1000

    let sw = Stopwatch.StartNew ()
    for i = 1 to iterations do
        data
        |> f (fun x y -> x = y)
        |> List.ofSeq    
        |> ignore
    printfn "%s %d" which sw.ElapsedMilliseconds


[<EntryPoint>]
let main argv =
    let data = List.init 10000 (fun _ -> 1)

    for i = 1 to 5 do
        test data "Seq" dedupeTakingLastSeq
        test data "Cistern" dedupeTakingLastCistern

    0

答案 7 :(得分:1)

这是一种使用库函数而不是Seq表达式的快速方法。

您的测试在我的电脑上以0.007秒的速度运行。

对于第一个不能很好地工作且可以改进的元素,它有一个非常讨厌的黑客。

let rec dedupe equalityfn prev (s:'a seq) : 'a seq =
    if Seq.isEmpty s then
        Seq.empty
    else
        let rest = Seq.skipWhile (equalityfn prev) s
        let valid = Seq.takeWhile (equalityfn prev) s
        let valid2 = if Seq.isEmpty valid  then Seq.singleton prev else (Seq.last valid) |> Seq.singleton
        let filtered = if Seq.isEmpty rest then Seq.empty else dedupe equalityfn (Seq.head rest) (rest)
        Seq.append valid2 filtered

let t = [("a", 1); ("b", 2); ("b", 3); ("b", 4); ("c", 5)]
        |> dedupe (fun (x1, y1) (x2, y2) -> x1=x2) ("asdfasdf",1)
        |> List.ofSeq;;

#time
List.init 1000 (fun _ -> 1)
|> dedupe (fun x y -> x = y) (189234784)
|> List.ofSeq
#time;;
--> Timing now on

Real: 00:00:00.007, CPU: 00:00:00.006, GC gen0: 0, gen1: 0
val it : int list = [189234784; 1]

--> Timing now off