F#
Suggerimenti e trucchi per le prestazioni F #
Ricerca…
Usando la ricorsione in coda per un'iterazione efficiente
Provenienti da linguaggi imperativi, molti sviluppatori si chiedono come scrivere un for-loop
che esce presto poiché F#
non supporta break
, continue
o return
. La risposta in F#
consiste nell'usare la ricorsione in coda che è un modo flessibile e idiomatico per iterare pur fornendo prestazioni eccellenti.
Diciamo che vogliamo implementare tryFind
per List
. Se F#
supportato return
avremmo scritto tryFind
un po 'come questo:
let tryFind predicate vs =
for v in vs do
if predicate v then
return Some v
None
Questo non funziona in F#
. Invece scriviamo la funzione usando la ricorsione in coda:
let tryFind predicate vs =
let rec loop = function
| v::vs -> if predicate v then
Some v
else
loop vs
| _ -> None
loop vs
La ricorsione in coda è performante in F#
perché quando il compilatore F#
rileva che una funzione è ricorsiva in coda, riscrive la ricorsione in un efficiente while-loop
. Usando ILSpy
possiamo vedere che questo è vero per il nostro loop
funzioni:
internal static FSharpOption<a> loop@3-10<a>(FSharpFunc<a, bool> predicate, FSharpList<a> _arg1)
{
while (_arg1.TailOrNull != null)
{
FSharpList<a> fSharpList = _arg1;
FSharpList<a> vs = fSharpList.TailOrNull;
a v = fSharpList.HeadOrDefault;
if (predicate.Invoke(v))
{
return FSharpOption<a>.Some(v);
}
FSharpFunc<a, bool> arg_2D_0 = predicate;
_arg1 = vs;
predicate = arg_2D_0;
}
return null;
}
A parte alcuni compiti non necessari (che si spera che il JIT-er elimini) questo è essenzialmente un ciclo efficiente.
Inoltre, la ricorsione in coda è idiomatica per F#
quanto ci consente di evitare lo stato mutabile. Considera una funzione sum
che somma tutti gli elementi in una List
. Un primo tentativo ovvio sarebbe questo:
let sum vs =
let mutable s = LanguagePrimitives.GenericZero
for v in vs do
s <- s + v
s
Se riscriviamo il loop in coda-ricorsione possiamo evitare lo stato mutabile:
let sum vs =
let rec loop s = function
| v::vs -> loop (s + v) vs
| _ -> s
loop LanguagePrimitives.GenericZero vs
Per efficienza, il compilatore F#
trasforma in un while-loop
che utilizza lo stato mutabile.
Misura e verifica le ipotesi di rendimento
Questo esempio è scritto con F#
in mente ma le idee sono applicabili in tutti gli ambienti
La prima regola quando si ottimizzano le prestazioni consiste nel non fare affidamento sull'ipotesi; Misurare sempre e verificare le ipotesi.
Poiché non stiamo scrivendo direttamente il codice macchina, è difficile prevedere come il compilatore e JIT: er trasformino il programma in codice macchina. Ecco perché dobbiamo misurare il tempo di esecuzione per vedere che otteniamo il miglioramento delle prestazioni che ci aspettiamo e verificare che il programma attuale non contenga alcun overhead nascosto.
La verifica è il processo abbastanza semplice che coinvolge il reverse engineering dell'eseguibile usando strumenti come ILSpy . Il JIT: er complica la verifica in quanto vedere il codice macchina effettivo è difficile ma fattibile. Tuttavia, di solito l'esame del IL-code
fornisce grandi vantaggi.
Il problema più difficile è Misurare; più difficile perché è difficile configurare situazioni realistiche che consentano di misurare i miglioramenti nel codice. La misurazione continua è inestimabile.
Analizzando semplici funzioni F #
Esaminiamo alcune semplici funzioni F#
che accumulano tutti gli interi in 1..n
scritti in vari modi. Poiché l'intervallo è una semplice serie aritmetica, il risultato può essere calcolato direttamente, ma per lo scopo di questo esempio, iteriamo sull'intervallo.
Per prima cosa definiamo alcune utili funzioni per misurare il tempo impiegato da una funzione:
// now () returns current time in milliseconds since start
let now : unit -> int64 =
let sw = System.Diagnostics.Stopwatch ()
sw.Start ()
fun () -> sw.ElapsedMilliseconds
// time estimates the time 'action' repeated a number of times
let time repeat action : int64*'T =
let v = action () // Warm-up and compute value
let b = now ()
for i = 1 to repeat do
action () |> ignore
let e = now ()
e - b, v
time
esegue ripetutamente un'azione, è necessario eseguire i test per alcune centinaia di millisecondi per ridurre la varianza.
Quindi definiamo alcune funzioni che accumulano tutti gli interi in 1..n
in modi diversi.
// Accumulates all integers 1..n using 'List'
let accumulateUsingList n =
List.init (n + 1) id
|> List.sum
// Accumulates all integers 1..n using 'Seq'
let accumulateUsingSeq n =
Seq.init (n + 1) id
|> Seq.sum
// Accumulates all integers 1..n using 'for-expression'
let accumulateUsingFor n =
let mutable sum = 0
for i = 1 to n do
sum <- sum + i
sum
// Accumulates all integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach n =
let mutable sum = 0
for i in 1..n do
sum <- sum + i
sum
// Accumulates all integers 1..n using 'foreach-expression' over list range
let accumulateUsingForEachOverList n =
let mutable sum = 0
for i in [1..n] do
sum <- sum + i
sum
// Accumulates every second integer 1..n using 'foreach-expression' over range
let accumulateUsingForEachStep2 n =
let mutable sum = 0
for i in 1..2..n do
sum <- sum + i
sum
// Accumulates all 64 bit integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach64 n =
let mutable sum = 0L
for i in 1L..int64 n do
sum <- sum + i
sum |> int
// Accumulates all integers n..1 using 'for-expression' in reverse order
let accumulateUsingReverseFor n =
let mutable sum = 0
for i = n downto 1 do
sum <- sum + i
sum
// Accumulates all 64 integers n..1 using 'tail-recursion' in reverse order
let accumulateUsingReverseTailRecursion n =
let rec loop sum i =
if i > 0 then
loop (sum + i) (i - 1)
else
sum
loop 0 n
Assumiamo che il risultato sia lo stesso (ad eccezione di una delle funzioni che utilizza l'incremento di 2
) ma c'è una differenza nelle prestazioni. Per misurare questo è definita la seguente funzione:
let testRun (path : string) =
use testResult = new System.IO.StreamWriter (path)
let write (l : string) = testResult.WriteLine l
let writef fmt = FSharp.Core.Printf.kprintf write fmt
write "Name\tTotal\tOuter\tInner\tCC0\tCC1\tCC2\tTime\tResult"
// total is the total number of iterations being executed
let total = 10000000
// outers let us variate the relation between the inner and outer loop
// this is often useful when the algorithm allocates different amount of memory
// depending on the input size. This can affect cache locality
let outers = [| 1000; 10000; 100000 |]
for outer in outers do
let inner = total / outer
// multiplier is used to increase resolution of certain tests that are significantly
// faster than the slower ones
let testCases =
[|
// Name of test multiplier action
"List" , 1 , accumulateUsingList
"Seq" , 1 , accumulateUsingSeq
"for-expression" , 100 , accumulateUsingFor
"foreach-expression" , 100 , accumulateUsingForEach
"foreach-expression over List" , 1 , accumulateUsingForEachOverList
"foreach-expression increment of 2" , 1 , accumulateUsingForEachStep2
"foreach-expression over 64 bit" , 1 , accumulateUsingForEach64
"reverse for-expression" , 100 , accumulateUsingReverseFor
"reverse tail-recursion" , 100 , accumulateUsingReverseTailRecursion
|]
for name, multiplier, a in testCases do
System.GC.Collect (2, System.GCCollectionMode.Forced, true)
let cc g = System.GC.CollectionCount g
printfn "Accumulate using %s with outer=%d and inner=%d ..." name outer inner
// Collect collection counters before test run
let pcc0, pcc1, pcc2 = cc 0, cc 1, cc 2
let ms, result = time (outer*multiplier) (fun () -> a inner)
let ms = (float ms / float multiplier)
// Collect collection counters after test run
let acc0, acc1, acc2 = cc 0, cc 1, cc 2
let cc0, cc1, cc2 = acc0 - pcc0, acc1 - pcc1, acc1 - pcc1
printfn " ... took: %f ms, GC collection count %d,%d,%d and produced %A" ms cc0 cc1 cc2 result
writef "%s\t%d\t%d\t%d\t%d\t%d\t%d\t%f\t%d" name total outer inner cc0 cc1 cc2 ms result
Il risultato del test durante l'esecuzione su .NET 4.5.2 x64:
Vediamo una differenza drammatica e alcuni risultati sono inaspettatamente cattivi.
Diamo un'occhiata ai casi brutti:
Elenco
// Accumulates all integers 1..n using 'List'
let accumulateUsingList n =
List.init (n + 1) id
|> List.sum
Quello che succede qui è una lista completa contenente tutti gli interi 1..n
viene creata e ridotta usando una somma. Questo dovrebbe essere più costoso della semplice iterazione e accumulo nell'intervallo, sembra circa 42 volte più lento del ciclo for.
Inoltre, possiamo vedere che il GC ha eseguito circa 100x durante l'esecuzione del test perché il codice ha allocato molti oggetti. Anche questo costa CPU.
Seq
// Accumulates all integers 1..n using 'Seq'
let accumulateUsingSeq n =
Seq.init (n + 1) id
|> Seq.sum
La versione Seq
non assegna un List
completo quindi è un po 'sorprendente che questo ~ 270x più lento del ciclo for. Inoltre, vediamo che il GC ha eseguito 661x.
Seq
è inefficiente quando la quantità di lavoro per articolo è molto piccola (in questo caso aggregando due numeri interi).
Il punto è non usare mai Seq
. Il punto è misurare.
( modifica manofstick: Seq.init
è il colpevole di questo grave problema di prestazioni. È molto più efficace usare l'espressione { 0 .. n }
invece di Seq.init (n+1) id
. Questo diventerà molto più efficiente ancora quando questo PR viene unito e rilasciato Anche dopo il rilascio, il Seq.init ... |> Seq.sum
originale Seq.init ... |> Seq.sum
sarà ancora lento, ma in qualche modo contro-intuitivamente, Seq.init ... |> Seq.map id |> Seq.sum
sarà abbastanza veloce, per mantenere la compatibilità con l'implementazione di Seq.init
, che non calcola inizialmente Current
, ma piuttosto li avvolge in un oggetto Lazy
- anche se anche questo dovrebbe funzionare un po 'meglio a causa di questo PR Note per l'editore: scusa si tratta di una specie di note vaganti, ma non voglio che le persone vengano messe fuori Seq quando il miglioramento è dietro l'angolo ... Quando arriveranno i tempi sarebbe bello aggiornare i grafici che sono in questa pagina. )
foreach-expression su List
// Accumulates all integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach n =
let mutable sum = 0
for i in 1..n do
sum <- sum + i
sum
// Accumulates all integers 1..n using 'foreach-expression' over list range
let accumulateUsingForEachOverList n =
let mutable sum = 0
for i in [1..n] do
sum <- sum + i
sum
La differenza tra queste due funzioni è molto sottile, ma la differenza di prestazioni non è di circa 76x. Perché? Eseguiamo il reverse engineering del codice errato:
public static int accumulateUsingForEach(int n)
{
int sum = 0;
int i = 1;
if (n >= i)
{
do
{
sum += i;
i++;
}
while (i != n + 1);
}
return sum;
}
public static int accumulateUsingForEachOverList(int n)
{
int sum = 0;
FSharpList<int> fSharpList = SeqModule.ToList<int>(Operators.CreateSequence<int>(Operators.OperatorIntrinsics.RangeInt32(1, 1, n)));
for (FSharpList<int> tailOrNull = fSharpList.TailOrNull; tailOrNull != null; tailOrNull = fSharpList.TailOrNull)
{
int i = fSharpList.HeadOrDefault;
sum += i;
fSharpList = tailOrNull;
}
return sum;
}
accumulateUsingForEach
è implementato come un ciclo while
efficiente ma for i in [1..n]
viene convertito in:
FSharpList<int> fSharpList =
SeqModule.ToList<int>(
Operators.CreateSequence<int>(
Operators.OperatorIntrinsics.RangeInt32(1, 1, n)));
Ciò significa innanzitutto creare un Seq
su 1..n
e infine chiamare ToList
.
Costoso.
incremento di foreach-expression di 2
// Accumulates all integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach n =
let mutable sum = 0
for i in 1..n do
sum <- sum + i
sum
// Accumulates every second integer 1..n using 'foreach-expression' over range
let accumulateUsingForEachStep2 n =
let mutable sum = 0
for i in 1..2..n do
sum <- sum + i
sum
Ancora una volta la differenza tra queste due funzioni è sottile ma la differenza di prestazioni è brutale: ~ 25x
Ancora una volta ILSpy
:
public static int accumulateUsingForEachStep2(int n)
{
int sum = 0;
IEnumerable<int> enumerable = Operators.OperatorIntrinsics.RangeInt32(1, 2, n);
foreach (int i in enumerable)
{
sum += i;
}
return sum;
}
Un Seq
viene creato su 1..2..n
e poi 1..2..n
su Seq
usando l'enumeratore.
Ci aspettavamo che F#
creasse qualcosa del genere:
public static int accumulateUsingForEachStep2(int n)
{
int sum = 0;
for (int i = 1; i < n; i += 2)
{
sum += i;
}
return sum;
}
Tuttavia, il compilatore F#
supporta solo cicli efficienti su intervalli int32 che aumentano di uno. Per tutti gli altri casi ricade su Operators.OperatorIntrinsics.RangeInt32
. Che spiegherà il prossimo risultato sorprendente
foreach-expression over 64 bit
// Accumulates all 64 bit integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach64 n =
let mutable sum = 0L
for i in 1L..int64 n do
sum <- sum + i
sum |> int
Questo esegue ~ 47 volte più lento del ciclo for, l'unica differenza è che iteriamo su interi a 64 bit. ILSpy
ci mostra perché:
public static int accumulateUsingForEach64(int n)
{
long sum = 0L;
IEnumerable<long> enumerable = Operators.OperatorIntrinsics.RangeInt64(1L, 1L, (long)n);
foreach (long i in enumerable)
{
sum += i;
}
return (int)sum;
}
F#
supporta solo cicli efficienti per i numeri int32
che deve utilizzare il fallback Operators.OperatorIntrinsics.RangeInt64
.
Gli altri casi si presentano approssimativamente simili:
Il motivo per cui le prestazioni si riducono per le esecuzioni di test più grandi è che il sovraccarico di invocare l' action
è in aumento man mano che facciamo sempre meno lavoro in action
.
Il loop verso 0
può talvolta dare benefici in termini di prestazioni poiché potrebbe salvare un registro della CPU, ma in questo caso la CPU ha i registri da risparmiare, quindi non sembra fare la differenza.
Conclusione
La misurazione è importante perché altrimenti potremmo pensare che tutte queste alternative siano equivalenti, ma alcune alternative sono ~ 270 volte più lente di altre.
La fase di verifica implica il reverse engineering, l'eseguibile ci aiuta a spiegare perché abbiamo ottenuto o meno le prestazioni che ci aspettavamo. Inoltre, la verifica può aiutarci a prevedere le prestazioni nei casi in cui è troppo difficile eseguire una misurazione adeguata.
È difficile prevedere le prestazioni là sempre Misura, verifica sempre le ipotesi di rendimento.
Confronto tra diverse pipeline di dati F #
In F#
ci sono molte opzioni per la creazione di pipeline di dati, ad esempio: List
, Seq
e Array
.
Quale pipeline di dati è preferibile dall'uso della memoria e dal punto di vista delle prestazioni?
Per rispondere a questo, confronteremo le prestazioni e l'utilizzo della memoria utilizzando diverse pipeline.
Pipeline di dati
Per misurare il sovraccarico, utilizzeremo una pipeline di dati con un costo di cpu basso per articoli elaborati:
let seqTest n =
Seq.init (n + 1) id
|> Seq.map int64
|> Seq.filter (fun v -> v % 2L = 0L)
|> Seq.map ((+) 1L)
|> Seq.sum
Creeremo condutture equivalenti per tutte le alternative e le confronteranno.
Variamo la dimensione di n
ma lasciamo che il numero totale di lavori sia lo stesso.
Alternative alla pipeline di dati
Confronteremo le seguenti alternative:
- Codice imperativo
- Matrice (non pigra)
- Elenco (non pigro)
- LINQ (Lazy pull stream)
- Seq (Lazy pull stream)
- Nessos (Lazy pull / push stream)
- PullStream (flusso di pull semplicistico)
- PushStream (flusso push semplicistico)
Sebbene non si tratti di una pipeline di dati, confronteremo il codice Imperative
dal momento che più strettamente corrisponde al modo in cui la CPU esegue il codice. Questo dovrebbe essere il modo più veloce per calcolare il risultato, consentendoci di misurare il sovraccarico delle prestazioni delle pipeline di dati.
Array
e List
calcolano una Array
/ List
in ogni passaggio, quindi ci aspettiamo un sovraccarico della memoria.
LINQ
e Seq
sono entrambi basati su IEnumerable<'T>
che è lazy pull stream (pull significa che lo stream consumer sta estraendo i dati dallo stream del produttore). Pertanto, ci aspettiamo che le prestazioni e l'utilizzo della memoria siano identici.
Nessos
è una libreria di stream ad alte prestazioni che supporta sia push & pull (come Java Stream
).
PullStream e PushStream sono implementazioni semplicistiche dei flussi Pull
& Push
.
Prestazioni Risultati dall'esecuzione su: F # 4.0 - .NET 4.6.1 - x64
Le barre mostrano il tempo trascorso, più in basso è meglio. La quantità totale di lavoro utile è la stessa per tutti i test, quindi i risultati sono comparabili. Ciò significa anche che poche esecuzioni implicano set di dati più grandi.
Come al solito quando si misura uno vedi risultati interessanti.
-
List
prestazioni diList
scadenti vengono confrontate con altre alternative per set di dati di grandi dimensioni. Questo può essere dovuto aGC
o alla scarsa localizzazione della cache. -
Array
prestazioni dellaArray
sono migliori del previsto. -
LINQ
prestazioni migliori diSeq
, questo è inaspettato perché entrambi sono basati suIEnumerable<'T>
. Tuttavia,Seq
internamente si basa su una generica impementazione per tutti gli algoritmi mentreLINQ
utilizza algoritmi specializzati. -
Push
esegue meglio diPull
. Questo è previsto poiché la pipeline di dati push ha meno controlli - Le semplicistiche pipeline di dati
Push
sono comparabili a quelle diNessos
. Tuttavia,Nessos
supporta pull e parallelismo. - Per le pipeline di dati di piccole dimensioni, le prestazioni di
Nessos
riducono a causa del sovraccarico dell'installazione delle pipeline. - Come previsto, il codice
Imperative
ottenuto il meglio
Conteggio delle raccolte GC dall'esecuzione su: F # 4.0 - .NET 4.6.1 - x64
Le barre mostrano il numero totale di conteggi di raccolta GC
durante il test, più basso è migliore. Questa è una misura del numero di oggetti creati dalla pipeline di dati.
Come al solito quando si misura uno vedi risultati interessanti.
- È previsto che la
List
crei più oggetti rispettoArray
perché unList
è essenzialmente un singolo elenco di nodi collegati. Una matrice è un'area di memoria continua. - Guardando i numeri sottostanti sia
List
Array
costringono 2 collezioni di generazione. Questo tipo di collezione è costoso. -
Seq
sta innescando una quantità sorprendente di collezioni. È sorprendentemente persino peggiore diList
in questo senso. -
LINQ
,Nessos
,Push
ePull
non attivano raccolte per poche esecuzioni. Tuttavia, gli oggetti sono assegnati in modo che ilGC
alla fine debba essere eseguito. - Come previsto dal momento che il codice
Imperative
non ha assegnato alcun oggetto, non sono state attivate raccolteGC
.
Conclusione
Tutte le pipeline di dati svolgono la stessa quantità di lavoro utile in tutti i casi di test, ma vediamo differenze significative nelle prestazioni e nell'utilizzo della memoria tra le diverse pipeline.
Inoltre, notiamo che il sovraccarico delle pipeline di dati varia a seconda delle dimensioni dei dati elaborati. Ad esempio, per le piccole dimensioni, l' Array
si comporta abbastanza bene.
Si dovrebbe tenere a mente che la quantità di lavoro svolto in ogni fase della pipeline è molto piccola per misurare il sovraccarico. In situazioni "reali" il sovraccarico di Seq
potrebbe non avere importanza perché il lavoro effettivo richiede più tempo.
Di maggiore preoccupazione sono le differenze di utilizzo della memoria. GC
non è gratuito ed è vantaggioso per le applicazioni di lunga durata che riducono la pressione di GC
.
Per gli sviluppatori di F#
preoccupati per le prestazioni e l'utilizzo della memoria, si consiglia di controllare Nessos Streams .
Se avete bisogno di prestazioni di alto livello, il codice Imperative
posizionato strategicamente vale la pena di essere preso in considerazione.
Infine, quando si tratta di prestazioni, non fare supposizioni. Misura e verifica.
Codice sorgente completo:
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