不同 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