不同 F 数据流水线的比较
在 F#
中,有许多用于创建数据管道的选项,例如:List
,Seq
和 Array
。
从内存使用和性能角度来看,哪种数据管道更受欢迎?
为了回答这个问题,我们将使用不同的管道来比较性能和内存使用情况。
数据管道
为了衡量开销,我们将使用每个处理项目的 cpu 成本低的数据管道:
let seqTest n =
Seq.init (n + 1) id
|> Seq.map int64
|> Seq.filter (fun v -> v % 2L = 0L)
|> Seq.map ((+) 1L)
|> Seq.sum
我们将为所有替代方案创建等效的管道并进行比较。
我们将改变 n
的大小,但让工作总数相同。
数据管道替代方案
我们将比较以下替代方案:
- 势在必行的代码
- 数组(非懒惰)
- 清单(非懒惰)
- LINQ(懒人拉流)
- Seq(懒人拉流)
- Nessos(懒惰拉/推流)
- PullStream(简单的拉流)
- PushStream(简单推送流)
虽然不是数据管道,但我们将与 Imperative
代码进行比较,因为它最接近于 CPU 执行代码的方式。这应该是计算结果的最快可能方式,允许我们衡量数据管道的性能开销。
Array
和 List
在每一步计算一个完整的 Array
/ List
,所以我们期望内存开销。
LINQ
和 Seq
都基于 IEnumerable<'T>
,这是懒惰的拉流(pull 意味着消费者流正在从生产者流中提取数据)。因此,我们期望性能和内存使用情况相同。
Nessos
是一个支持推拉的高性能流库(如 Java Stream
)。
PullStream 和 PushStream 是 Pull
和 Push
流的简单实现。
运行的性能结果:F#4.0 - .NET 4.6.1 - x64
条形显示经过的时间,越低越好。所有测试的有用工作总量相同,因此结果具有可比性。这也意味着少数运行意味着更大的数据集。
像往常一样测量一个看到有趣的结果。
- 将
List
性能差与大数据集的其他替代方案进行比较。这可能是因为GC
或缓存局部性差。 Array
的表现好于预期。LINQ
比Seq
表现更好,这是出乎意料的,因为两者都是以IEnumerable<'T>
为基础的。但是,Seq
内部基于所有算法的通用实现,而LINQ
使用专门的算法。Push
比Pull
表现更好。这是预期的,因为推送数据管道具有较少的检查- 简单的
Push
数据管道的性能与Nessos
相当。但是,Nessos
支持拉动和平行。 - 对于小型数据流水线,
Nessos
的性能可能会降低,因为流水线会设置开销。 - 正如所料,
Imperative
代码表现最佳
运行时的 GC 集合计数:F#4.0 - .NET 4.6.1 - x64
条形图显示测试期间的 GC
集合计数总数,越低越好。这是对数据管道创建的对象数量的度量。
像往常一样测量一个看到有趣的结果。
List
预计会产生比Array
更多的对象,因为List
本质上是一个单独的节点链表。数组是连续的内存区域。- 看看潜在的数字,
List
和Array
强制推动 2 代收藏。这种收藏很贵。 Seq
正在引发惊人数量的收藏品。在这方面,令人惊讶地甚至比List
更糟糕。LINQ
,Nessos
,Push
和Pull
在几次运行中都没有触发任何集合。但是,对象被分配,因此GC
最终将必须运行。- 正如预期的那样,由于
Imperative
代码没有分配任何对象,因此没有触发GC
集合。
结论
所有数据管道在所有测试用例中都执行相同数量的有用工作,但我们发现不同管道之间在性能和内存使用方面存在显着差异。
此外,我们注意到数据管道的开销因处理的数据大小而异。例如,对于小尺寸,Array
表现相当不错。
应该记住,管道中每个步骤中执行的工作量非常小,以便测量开销。在真实情况下,Seq
的开销可能并不重要,因为实际工作更耗时。
更值得关注的是内存使用差异。GC
不是免费的,对于长时间运行的应用来说,保持 GC
压力是有益的。
对于关注性能和内存使用情况的 F#
开发人员,建议查看 Nessos Streams 。
如果你需要具有策略性位置的顶级性能 Imperative
代码值得考虑。
最后,谈到性能时不要做出假设。测量和验证。
完整源代码:
module PushStream =
type Receiver<'T> = 'T -> bool
type Stream<'T> = Receiver<'T> -> unit
let inline filter (f : 'T -> bool) (s : Stream<'T>) : Stream<'T> =
fun r -> s (fun v -> if f v then r v else true)
let inline map (m : 'T -> 'U) (s : Stream<'T>) : Stream<'U> =
fun r -> s (fun v -> r (m v))
let inline range b e : Stream<int> =
fun r ->
let rec loop i = if i <= e && r i then loop (i + 1)
loop b
let inline sum (s : Stream<'T>) : 'T =
let mutable state = LanguagePrimitives.GenericZero<'T>
s (fun v -> state <- state + v; true)
state
module PullStream =
[<Struct>]
[<NoComparison>]
[<NoEqualityAttribute>]
type Maybe<'T>(v : 'T, hasValue : bool) =
member x.Value = v
member x.HasValue = hasValue
override x.ToString () =
if hasValue then
sprintf "Just %A" v
else
"Nothing"
let Nothing<'T> = Maybe<'T> (Unchecked.defaultof<'T>, false)
let inline Just v = Maybe<'T> (v, true)
type Iterator<'T> = unit -> Maybe<'T>
type Stream<'T> = unit -> Iterator<'T>
let filter (f : 'T -> bool) (s : Stream<'T>) : Stream<'T> =
fun () ->
let i = s ()
let rec pop () =
let mv = i ()
if mv.HasValue then
let v = mv.Value
if f v then Just v else pop ()
else
Nothing
pop
let map (m : 'T -> 'U) (s : Stream<'T>) : Stream<'U> =
fun () ->
let i = s ()
let pop () =
let mv = i ()
if mv.HasValue then
Just (m mv.Value)
else
Nothing
pop
let range b e : Stream<int> =
fun () ->
let mutable i = b
fun () ->
if i <= e then
let p = i
i <- i + 1
Just p
else
Nothing
let inline sum (s : Stream<'T>) : 'T =
let i = s ()
let rec loop state =
let mv = i ()
if mv.HasValue then
loop (state + mv.Value)
else
state
loop LanguagePrimitives.GenericZero<'T>
module PerfTest =
open System.Linq
#if USE_NESSOS
open Nessos.Streams
#endif
let now =
let sw = System.Diagnostics.Stopwatch ()
sw.Start ()
fun () -> sw.ElapsedMilliseconds
let time n a =
let inline cc i = System.GC.CollectionCount i
let v = a ()
System.GC.Collect (2, System.GCCollectionMode.Forced, true)
let bcc0, bcc1, bcc2 = cc 0, cc 1, cc 2
let b = now ()
for i in 1..n do
a () |> ignore
let e = now ()
let ecc0, ecc1, ecc2 = cc 0, cc 1, cc 2
v, (e - b), ecc0 - bcc0, ecc1 - bcc1, ecc2 - bcc2
let arrayTest n =
Array.init (n + 1) id
|> Array.map int64
|> Array.filter (fun v -> v % 2L = 0L)
|> Array.map ((+) 1L)
|> Array.sum
let imperativeTest n =
let rec loop s i =
if i >= 0L then
if i % 2L = 0L then
loop (s + i + 1L) (i - 1L)
else
loop s (i - 1L)
else
s
loop 0L (int64 n)
let linqTest n =
(((Enumerable.Range(0, n + 1)).Select int64).Where(fun v -> v % 2L = 0L)).Select((+) 1L).Sum()
let listTest n =
List.init (n + 1) id
|> List.map int64
|> List.filter (fun v -> v % 2L = 0L)
|> List.map ((+) 1L)
|> List.sum
#if USE_NESSOS
let nessosTest n =
Stream.initInfinite id
|> Stream.take (n + 1)
|> Stream.map int64
|> Stream.filter (fun v -> v % 2L = 0L)
|> Stream.map ((+) 1L)
|> Stream.sum
#endif
let pullTest n =
PullStream.range 0 n
|> PullStream.map int64
|> PullStream.filter (fun v -> v % 2L = 0L)
|> PullStream.map ((+) 1L)
|> PullStream.sum
let pushTest n =
PushStream.range 0 n
|> PushStream.map int64
|> PushStream.filter (fun v -> v % 2L = 0L)
|> PushStream.map ((+) 1L)
|> PushStream.sum
let seqTest n =
Seq.init (n + 1) id
|> Seq.map int64
|> Seq.filter (fun v -> v % 2L = 0L)
|> Seq.map ((+) 1L)
|> Seq.sum
let perfTest (path : string) =
let testCases =
[|
"array" , arrayTest
"imperative" , imperativeTest
"linq" , linqTest
"list" , listTest
"seq" , seqTest
#if USE_NESSOS
"nessos" , nessosTest
#endif
"pull" , pullTest
"push" , pushTest
|]
use out = new System.IO.StreamWriter (path)
let write (msg : string) = out.WriteLine msg
let writef fmt = FSharp.Core.Printf.kprintf write fmt
write "Name\tTotal\tOuter\tInner\tElapsed\tCC0\tCC1\tCC2\tResult"
let total = 10000000
let outers = [| 10; 1000; 1000000 |]
for outer in outers do
let inner = total / outer
for name, a in testCases do
printfn "Running %s with total=%d, outer=%d, inner=%d ..." name total outer inner
let v, ms, cc0, cc1, cc2 = time outer (fun () -> a inner)
printfn " ... %d ms, cc0=%d, cc1=%d, cc2=%d, result=%A" ms cc0 cc1 cc2 v
writef "%s\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d" name total outer inner ms cc0 cc1 cc2 v
[<EntryPoint>]
let main argv =
System.Environment.CurrentDirectory <- System.AppDomain.CurrentDomain.BaseDirectory
PerfTest.perfTest "perf.tsv"
0