F#
F # Prestatietips en trucs
Zoeken…
Tail-recursion gebruiken voor efficiënte iteratie
Veel ontwikkelaars komen uit verplichte talen en vragen zich af hoe ze een for-loop
die vroegtijdig wordt afgesloten, omdat F#
geen break
, continue
of return
. Het antwoord in F#
is om staartrecursie te gebruiken, wat een flexibele en idiomatische manier is om te itereren terwijl het nog steeds uitstekende prestaties levert.
Stel dat we tryFind
voor List
willen implementeren. Als F#
ondersteund return
, zouden we tryFind
een beetje als dit schrijven:
let tryFind predicate vs =
for v in vs do
if predicate v then
return Some v
None
Dit werkt niet in F#
. In plaats daarvan schrijven we de functie met behulp van staartrecursie:
let tryFind predicate vs =
let rec loop = function
| v::vs -> if predicate v then
Some v
else
loop vs
| _ -> None
loop vs
Tail-recursion is performant in F#
omdat wanneer de F#
compiler detecteert dat een functie tail-recursief is, het de recursie herschrijft in een efficiënte while-loop
. Met behulp van ILSpy
kunnen we zien dat dit waar is voor onze functie loop
:
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;
}
Afgezien van enkele onnodige opdrachten (die hopelijk de JIT-er elimineert) is dit in wezen een efficiënte lus.
Bovendien is staartrecursie idiomatisch voor F#
omdat het ons toelaat om een veranderlijke toestand te vermijden. Overweeg een sum
die alle elementen in een List
. Een voor de hand liggende eerste poging zou dit zijn:
let sum vs =
let mutable s = LanguagePrimitives.GenericZero
for v in vs do
s <- s + v
s
Als we de lus herschrijven naar staartrecursie, kunnen we de veranderlijke status vermijden:
let sum vs =
let rec loop s = function
| v::vs -> loop (s + v) vs
| _ -> s
loop LanguagePrimitives.GenericZero vs
Voor efficiëntie transformeert de F#
compiler dit in een while-loop
die een veranderlijke status gebruikt.
Meet en verifieer uw prestatie-aannames
Dit voorbeeld is geschreven met F#
in gedachten, maar de ideeën zijn toepasbaar in alle omgevingen
De eerste regel bij het optimaliseren voor prestaties is om niet te vertrouwen op veronderstelling; Meet en verifieer altijd uw aannames.
Omdat we niet direct machinecode schrijven, is het moeilijk te voorspellen hoe de compiler en JIT: er uw programma naar machinecode transformeren. Daarom moeten we de uitvoeringstijd meten om te zien dat we de prestatieverbetering krijgen die we verwachten en verifiëren dat het eigenlijke programma geen verborgen overhead bevat.
Verificatie is het vrij eenvoudige proces waarbij het uitvoerbare bestand wordt reverse-engineering met behulp van bijvoorbeeld tools zoals ILSpy . De JIT: er bemoeilijkt de verificatie doordat het zien van de werkelijke machinecode lastig maar uitvoerbaar is. Meestal levert het onderzoeken van de IL-code
de grote voordelen op.
Het moeilijkere probleem is meten; moeilijker omdat het lastig is om realistische situaties in te stellen waarmee verbeteringen in code kunnen worden gemeten. Toch is meten van onschatbare waarde.
Analyse van eenvoudige F # -functies
Laten we eens kijken naar enkele eenvoudige F#
-functies die alle gehele getallen in 1..n
, op verschillende manieren geschreven. Omdat het bereik een eenvoudige rekenkundige serie is, kan het resultaat direct worden berekend, maar voor dit voorbeeld herhalen we het bereik.
Eerst definiëren we enkele nuttige functies voor het meten van de tijd die een functie in beslag neemt:
// 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
voert herhaaldelijk een actie uit. We moeten de tests een paar honderd milliseconden uitvoeren om de variantie te verminderen.
Vervolgens definiëren we een paar functies die alle gehele getallen in 1..n
op verschillende manieren verzamelen.
// 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
We nemen aan dat het resultaat hetzelfde is (behalve voor een van de functies die een toename van 2
), maar is er verschil in prestaties. Om dit te meten is de volgende functie gedefinieerd:
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
Het testresultaat tijdens het draaien op .NET 4.5.2 x64:
We zien een dramatisch verschil en sommige resultaten zijn onverwacht slecht.
Laten we eens kijken naar de slechte gevallen:
Lijst
// Accumulates all integers 1..n using 'List'
let accumulateUsingList n =
List.init (n + 1) id
|> List.sum
Wat hier gebeurt, is een volledige lijst met alle gehele getallen 1..n
wordt gemaakt en verlaagd met een som. Dit zou duurder moeten zijn dan alleen maar itereren en accumuleren over het bereik, het lijkt ongeveer ~ 42x langzamer dan de for-lus.
Bovendien kunnen we zien dat de GC tijdens de testrun ongeveer 100x is uitgevoerd omdat de code veel objecten heeft toegewezen. Dit kost ook CPU.
Seq
// Accumulates all integers 1..n using 'Seq'
let accumulateUsingSeq n =
Seq.init (n + 1) id
|> Seq.sum
De Seq
versie kent geen volledige List
dus het is een beetje verrassend dat deze ~ 270x langzamer is dan de for-lus. Bovendien zien we dat de GC 661x heeft uitgevoerd.
Seq
is inefficiënt wanneer de hoeveelheid werk per artikel erg klein is (in dit geval twee gehele getallen samenvoegend).
Het punt is om nooit Seq
te gebruiken. Het punt is om te meten.
( manofstick-bewerking: Seq.init
is de oorzaak van dit ernstige prestatieprobleem. Het is veel efficiënter om de uitdrukking { 0 .. n }
plaats van Seq.init (n+1) id
. Dit wordt nog veel efficiënter wanneer deze PR wordt samengevoegd en vrijgegeven. Maar zelfs na de release, zal de originele Seq.init ... |> Seq.sum
nog steeds langzaam zijn, maar enigszins contra-intuïtief, Seq.init ... |> Seq.map id |> Seq.sum
zal vrij snel zijn. Dit was om achterwaartse compatibiliteit te behouden met de implementatie van Seq.init
, die de Current
aanvankelijk niet berekent, maar ze eerder in een Lazy
object verpakt - hoewel dit ook een beetje beter zou moeten presteren vanwege deze PR . Noot voor de redactie: sorry dit is een beetje kruipende notities, maar ik wil niet dat mensen worden afgeschrikt Seq wanneer verbetering voor de deur staat ... Wanneer die tijden komen zou het goed zijn om de grafieken bij te werken die op deze pagina staan. )
foreach-expressie over lijst
// 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
Het verschil tussen deze twee functies is heel subtiel, maar het prestatieverschil is niet grofweg ~ 76x. Waarom? Laten we de slechte code reverse-engineeren:
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
is geïmplementeerd als een efficiënte while
lus maar for i in [1..n]
wordt omgezet in:
FSharpList<int> fSharpList =
SeqModule.ToList<int>(
Operators.CreateSequence<int>(
Operators.OperatorIntrinsics.RangeInt32(1, 1, n)));
Dit betekent dat we eerst een Seq
boven 1..n
en ten slotte ToList
aanroepen.
Duur.
foreach-expression increment van 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
Nogmaals, het verschil tussen deze twee functies is subtiel, maar het prestatieverschil is brutaal: ~ 25x
Laten we nogmaals 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;
}
Een Seq
wordt aangemaakt via 1..2..n
en daarna herhalen we Seq
met behulp van de teller.
We hadden verwacht dat F#
zoiets zou creëren:
public static int accumulateUsingForEachStep2(int n)
{
int sum = 0;
for (int i = 1; i < n; i += 2)
{
sum += i;
}
return sum;
}
F#
compiler ondersteunt echter alleen efficiënt voor lussen over int32-bereiken die met één toenemen. Voor alle andere gevallen valt het terug op Operators.OperatorIntrinsics.RangeInt32
. Dat zal het volgende verrassende resultaat verklaren
foreach-expressie van meer dan 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
Dit werkt ~ 47x langzamer dan de for-lus, het enige verschil is dat we meer dan 64 bit gehele getallen itereren. ILSpy
laat ons zien waarom:
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#
ondersteunt alleen efficiënt voor lussen voor int32
nummers, het moet de fallback Operators.OperatorIntrinsics.RangeInt64
. Operators.OperatorIntrinsics.RangeInt64
.
De andere gevallen presteren ongeveer hetzelfde:
De reden dat de prestaties voor grotere testruns achteruitgaan, is dat de overhead van het aanroepen van de action
groeit naarmate we steeds minder werk in action
.
Lussen naar 0
kan soms prestatievoordelen opleveren, omdat het een CPU-register kan opslaan, maar in dit geval heeft de CPU registers om te sparen, zodat het geen verschil lijkt te maken.
Conclusie
Meten is belangrijk omdat we anders misschien denken dat al deze alternatieven gelijkwaardig zijn, maar sommige alternatieven zijn ~ 270x langzamer dan andere.
De verificatiestap omvat reverse-engineering. Het uitvoerbare bestand helpt ons uit te leggen waarom we wel of niet de prestaties hebben gekregen die we hadden verwacht. Bovendien kan Verificatie ons helpen de prestaties te voorspellen in het geval het te moeilijk is om een goede meting uit te voeren.
Het is moeilijk om daar de prestaties te voorspellen. Meet, controleer altijd uw prestaties.
Vergelijking van verschillende F # datapijplijnen
In F#
er veel opties voor het maken van gegevenspijplijnen, bijvoorbeeld: List
, Seq
en Array
.
Welke datapijplijn heeft de voorkeur vanuit geheugengebruik en prestatieperspectief?
Om dit te beantwoorden, vergelijken we prestaties en geheugengebruik met behulp van verschillende pijplijnen.
Gegevenspijplijn
Om de overhead te meten, gebruiken we een datapijplijn met lage cpu-kosten per verwerkte artikelen:
let seqTest n =
Seq.init (n + 1) id
|> Seq.map int64
|> Seq.filter (fun v -> v % 2L = 0L)
|> Seq.map ((+) 1L)
|> Seq.sum
We zullen gelijkwaardige pijpleidingen maken voor alle alternatieven en deze vergelijken.
We zullen de grootte van n
variëren, maar laten het totale aantal werk hetzelfde zijn.
Alternatieve datapijplijn
We zullen de volgende alternatieven vergelijken:
- Gebiedende wijs
- Array (niet lui)
- Lijst (niet lui)
- LINQ (Lazy pull stream)
- Seq (Lazy pull stream)
- Nessos (Lazy pull / push stream)
- PullStream (Simplistische pull-stream)
- PushStream (Simplistische push-stream)
Hoewel het geen gegevenspijplijn is, zullen we vergelijken met Imperative
code, omdat die het beste overeenkomt met hoe de CPU code uitvoert. Dat zou die snelst mogelijke manier moeten zijn om het resultaat te berekenen, zodat we de prestatieoverhead van datapijplijnen kunnen meten.
Array
en List
berekenen een volledige Array
/ List
bij elke stap, dus we verwachten geheugenoverhead.
LINQ
en Seq
zijn beide gebaseerd op IEnumerable<'T>
dat is een luie pull-stroom (pull betekent dat de consumentenstroom gegevens uit de producentenstroom haalt). We verwachten daarom dat de prestaties en het geheugengebruik identiek zijn.
Nessos
is een krachtige stream-bibliotheek die zowel push & pull ondersteunt (zoals Java Stream
).
PullStream en PushStream zijn simplistische implementaties van Pull
& Push
streams.
Prestaties Resultaten van het draaien op: F # 4.0 - .NET 4.6.1 - x64
De balken geven de verstreken tijd aan, lager is beter. De totale hoeveelheid nuttig werk is hetzelfde voor alle tests, dus de resultaten zijn vergelijkbaar. Dit betekent ook dat weinig runs grotere datasets impliceert.
Zoals gebruikelijk bij het meten zie je interessante resultaten.
-
List
prestaties slecht is in vergelijking met andere alternatieven voor grote datasets. Dit kan komen doorGC
of slechte cache-locatie. -
Array
prestaties beter dan verwacht. -
LINQ
presteert beter danSeq
, dit is onverwacht omdat beide gebaseerd zijn opIEnumerable<'T>
.Seq
intern gebaseerd op een generieke impementatie voor alle algoritmen, terwijlLINQ
gespecialiseerde algoritmen gebruikt. -
Push
presteert beter danPull
. Dit wordt verwacht omdat de push-gegevenspijplijn minder controles heeft - De simplistische
Push
gegevenspijplijnen presteren vergelijkbaar metNessos
.Nessos
ondersteunt echter pull en parallellisme. - Voor kleine
Nessos
verslechteren de prestaties vanNessos
mogelijk omdat pijpleidingen overhead opzetten. - Zoals verwacht presteerde de
Imperative
code het beste
GC Collection telt vanaf: F # 4.0 - .NET 4.6.1 - x64
De balken tonen het totale aantal GC
verzameltellingen tijdens de test, lager is beter. Dit is een meting van hoeveel objecten door de gegevenspijplijn zijn gemaakt.
Zoals gebruikelijk bij het meten zie je interessante resultaten.
-
List
maakt naar verwachting meer objecten danArray
omdat eenList
in wezen een enkele gekoppelde lijst met knooppunten is. Een array is een continu geheugengebied. - Kijkend naar de onderliggende getallen dwingen beide
List
&Array
collecties van 2 generaties. Dit soort collecties zijn duur. -
Seq
verrassend veel collecties teweeg. Het is in dit opzicht verrassend nog erger danList
. -
LINQ
,Nessos
,Push
enPull
triggeren geen collecties voor enkele runs. Er worden echter objecten toegewezen, zodat deGC
uiteindelijk moet worden uitgevoerd. - Zoals verwacht, omdat de
Imperative
geen objecten toewijst, zijn er geenGC
collecties geactiveerd.
Conclusie
Alle datapijplijnen doen evenveel nuttig werk in alle testgevallen, maar we zien aanzienlijke verschillen in prestaties en geheugengebruik tussen de verschillende pijplijnen.
Bovendien merken we dat de overhead van datapijplijnen verschilt, afhankelijk van de grootte van de verwerkte gegevens. Voor kleine maten presteert Array
bijvoorbeeld behoorlijk goed.
Houd er rekening mee dat de hoeveelheid werk die wordt uitgevoerd in elke stap in de pijplijn erg klein is om de overhead te meten. In "echte" situaties doet de overhead van Seq
misschien niet toe, omdat het eigenlijke werk meer tijd kost.
Van groter belang zijn de verschillen in geheugengebruik. GC
is niet gratis en het is gunstig voor langlopende applicaties om de GC
druk GC
te houden.
Voor F#
-ontwikkelaars die zich zorgen maken over prestaties en geheugengebruik, is het raadzaam om Nessos Streams te bekijken .
Als u topprestaties nodig heeft, is strategisch geplaatste Imperative
code het overwegen waard.
Ten slotte, als het gaat om prestaties, maak geen aannames. Meten en verifiëren.
Volledige broncode:
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