我正在尝试编写一个函数,根据给定的相等函数确定连续的重复项,来自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.head
和Seq.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
答案 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
,应该自由使用cons
和tail
函数,这两个函数基本上都是免费的。< / 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