F#
F # Prestandatips och tricks
Sök…
Använda svansrekursion för effektiv iteration
Kommer från nödvändiga språk många utvecklare undrar hur man skriver en for-loop
som kommer ut tidigt eftersom F#
inte stöder break
, continue
eller return
. Svaret i F#
är att använda svansrekursion som är ett flexibelt och idiomatiskt sätt att iterera samtidigt som det fortfarande ger utmärkta prestanda.
Säg att vi vill implementera tryFind
för List
. Om F#
stöds return
skulle vi skriva tryFind
lite så här:
let tryFind predicate vs =
for v in vs do
if predicate v then
return Some v
None
Detta fungerar inte i F#
. Istället skriver vi funktionen med svansrekursion:
let tryFind predicate vs =
let rec loop = function
| v::vs -> if predicate v then
Some v
else
loop vs
| _ -> None
loop vs
Svansrekursion är performant i F#
eftersom när F#
-kompilatorn upptäcker att en funktion är svansrekursiv skriver den om rekursionen till en effektiv while-loop
. Använda ILSpy
vi kan se att detta är sant för vår funktion 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;
}
Bortsett från några onödiga uppdrag (som förhoppningsvis eliminerar JIT-er) är detta i huvudsak en effektiv slinga.
Dessutom är svansrekursion idiomatisk för F#
eftersom det tillåter oss att undvika muterbara tillstånd. Tänk på en sum
som summerar alla element i en List
. Ett uppenbart första försök skulle vara detta:
let sum vs =
let mutable s = LanguagePrimitives.GenericZero
for v in vs do
s <- s + v
s
Om vi skriver om slingan till svansrekursion kan vi undvika det muterbara tillståndet:
let sum vs =
let rec loop s = function
| v::vs -> loop (s + v) vs
| _ -> s
loop LanguagePrimitives.GenericZero vs
För effektivitet förvandlar F#
-kompilern detta till en while-loop
som använder ett muterbara tillstånd.
Mät och verifiera dina antaganden om prestanda
Detta exempel är skrivet med F#
i åtanke men idéerna är tillämpliga i alla miljöer
Den första regeln när man optimerar för prestanda är att inte lita på antagandet; Mät och verifiera alltid dina antaganden.
Eftersom vi inte skriver maskinkod direkt är det svårt att förutsäga hur kompilatorn och JIT: er förvandlar ditt program till maskinkod. Det är därför vi måste mäta körningstiden för att se att vi får den prestandaförbättring vi förväntar oss och verifiera att det verkliga programmet inte innehåller något doldt omkostnad.
Verifiering är den ganska enkla processen som innebär omvänd konstruktion av den körbara med exempelvis verktyg som ILSpy . JIT: er komplicerar verifiering genom att det är svårt att se själva maskinkoden men genomförbart. Men vanligtvis att granska IL-code
ger de stora vinsterna.
Det svårare problemet är att mäta; svårare eftersom det är svårt att ställa in realistiska situationer som gör det möjligt att mäta förbättringar i koden. Att mäta är fortfarande ovärderligt.
Analysera enkla F # -funktioner
Låt oss undersöka några enkla F#
-funktioner som samlar alla heltal i 1..n
skrivna på olika olika sätt. Eftersom intervallet är en enkel aritmetisk serie kan resultatet beräknas direkt men för detta exempel upprepas vi över intervallet.
Först definierar vi några användbara funktioner för att mäta tiden en funktion tar:
// 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
kör en åtgärd upprepade gånger vi måste köra testerna i några hundra millisekunder för att minska variansen.
Sedan definierar vi några funktioner som samlar alla heltal på 1..n
på olika sätt.
// 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
Vi antar att resultatet är detsamma (med undantag för en av funktionerna som använder steget 2
) men är det skillnad i prestanda. För att mäta detta definieras följande funktion:
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
Testresultatet när det körs på .NET 4.5.2 x64:
Vi ser dramatisk skillnad och några av resultaten är oväntat dåliga.
Låt oss titta på de dåliga fallen:
Lista
// Accumulates all integers 1..n using 'List'
let accumulateUsingList n =
List.init (n + 1) id
|> List.sum
Vad som händer här är en fullständig lista som innehåller alla heltal 1..n
skapas och reduceras med en summa. Detta borde vara dyrare än att bara iterera och ackumuleras över intervallet, det verkar ungefär ~ 42x långsammare än för slingan.
Dessutom kan vi se att GC körde cirka 100 gånger under testkörningen eftersom koden tilldelade många objekt. Detta kostar också CPU.
Seq
// Accumulates all integers 1..n using 'Seq'
let accumulateUsingSeq n =
Seq.init (n + 1) id
|> Seq.sum
Seq
versionen tilldelar inte en fullständig List
så det är lite överraskande att denna ~ 270x är långsammare än för slingan. Dessutom ser vi att GC har kört 661x.
Seq
är ineffektiv när mängden arbete per artikel är mycket litet (i detta fall sammanförande två heltal).
Poängen är att aldrig använda Seq
. Poängen är att mäta.
( manofstick-redigering: Seq.init
är den skyldige i denna allvarliga prestationsfråga. Det är mycket effektivare att använda uttrycket { 0 .. n }
istället för Seq.init (n+1) id
. Detta kommer att bli mycket effektivare fortfarande när denna PR slås samman och släpps. Även efter utgivningen kommer den ursprungliga Seq.init ... |> Seq.sum
fortfarande att vara långsam, men något Seq.init ... |> Seq.map id |> Seq.sum
, Seq.init ... |> Seq.map id |> Seq.sum
kommer att vara ganska snabbt. Detta var för att upprätthålla bakåtkompatibilitet med Seq.init
implementering, som inte beräknar Current
initialt, utan snarare lindar dem i ett Lazy
objekt - även om detta också borde fungera lite bättre på grund av denna PR . Notera till redaktör: ledsen att det här är en typ av pratande anteckningar, men jag vill inte att folk ska skjutas upp Seq när förbättringar är precis runt hörnet ... När den tiden kommer kommer det vara bra att uppdatera diagrammen som finns på den här sidan. )
förhandsuttryck över listan
// 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
Skillnaden mellan dessa två funktioner är väldigt subtil, men prestationsskillnaden är inte, ungefär ~ 76x. Varför? Låt oss omvända den dåliga koden:
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
implementeras som en effektiv while
loop men for i in [1..n]
omvandlas till:
FSharpList<int> fSharpList =
SeqModule.ToList<int>(
Operators.CreateSequence<int>(
Operators.OperatorIntrinsics.RangeInt32(1, 1, n)));
Detta innebär att vi först skapar en Seq
över 1..n
och slutligen ringer ToList
.
Dyr.
föruttryckningsökning av 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
Återigen är skillnaden mellan dessa två funktioner subtil men prestationsskillnaden är brutal: ~ 25x
Låt oss köra 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;
}
En Seq
skapas över 1..2..n
och sedan itereras över Seq
hjälp av enumerator.
Vi förväntade oss att F#
skulle skapa något liknande:
public static int accumulateUsingForEachStep2(int n)
{
int sum = 0;
for (int i = 1; i < n; i += 2)
{
sum += i;
}
return sum;
}
Men F#
-kompileraren stöder bara effektiva för slingor över int32-intervall som ökar med en. För alla andra fall faller det tillbaka på Operators.OperatorIntrinsics.RangeInt32
. Vilket kommer att förklara nästa överraskande resultat
föruttryck över 64 bitar
// 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
Detta utför ~ 47x långsammare än för slingan, den enda skillnaden är att vi itererar över 64 bitars heltal. ILSpy
visar varför:
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#
stöder bara effektiva för slingor för int32
nummer, den måste använda fallback Operators.OperatorIntrinsics.RangeInt64
.
De andra fallen fungerar ungefär lika:
Anledningen till att prestandan försämras för större testkörningar är att omkostningen för att åberopa action
växer när vi gör mindre och mindre arbete i action
.
Om du går mot 0
kan det ibland ge prestandafördelar eftersom det kan spara ett CPU-register, men i detta fall har CPU-register att spara så det verkar inte göra någon skillnad.
Slutsats
Mätning är viktig eftersom vi annars skulle kunna tro att alla dessa alternativ är likvärdiga men vissa alternativ är ~ 270x långsammare än andra.
Verifieringssteget innebär omvänd konstruktion som den körbara hjälper oss förklara varför vi gjorde eller inte fick de resultat vi förväntade oss. Verifiering kan dessutom hjälpa oss att förutsäga prestanda i de fall det är för svårt att göra en korrekt mätning.
Det är svårt att förutsäga prestanda där alltid Mät, alltid verifiera dina prestandaantaganden.
Jämförelse av olika F #-dataledningar
I F#
finns det många alternativ för att skapa datapipelines, till exempel: List
, Seq
och Array
.
Vilken datapipeline är att föredra utifrån minnesanvändning och prestandaperspektiv?
För att besvara detta jämför vi prestanda och minnesanvändning med olika pipelines.
Datapipeline
För att mäta omkostnaderna kommer vi att använda en datapipeline med låg cpu-kostnad per bearbetade objekt:
let seqTest n =
Seq.init (n + 1) id
|> Seq.map int64
|> Seq.filter (fun v -> v % 2L = 0L)
|> Seq.map ((+) 1L)
|> Seq.sum
Vi kommer att skapa likvärdiga rörledningar för alla alternativ och jämföra dem.
Vi kommer att variera storleken på n
men låt det totala antalet arbeten vara detsamma.
Data Pipeline Alternativ
Vi kommer att jämföra följande alternativ:
- Imperativ kod
- Array (icke-lat)
- Lista (icke-lat)
- LINQ (Lazy pull stream)
- Seq (Lazy pull stream)
- Nessos (Lazy pull / push stream)
- PullStream (Simplistic pull stream)
- PushStream (Simplistic push stream)
Även om det inte är en datapipeline kommer vi att jämföra med Imperative
, eftersom den bäst matchar hur CPU: n kör kod. Det borde vara det snabbaste möjliga sättet att beräkna resultatet så att vi kan mäta prestandakostnaderna för dataledningar.
Array
och List
beräknar en fullständig Array
/ List
i varje steg så att vi förväntar oss minnesomkostnader.
LINQ
och Seq
är båda baserade kring IEnumerable<'T>
som är lat dragström (pull innebär att konsumentströmmen drar data ur producentströmmen). Vi förväntar oss därför att prestandan och minnesanvändningen är identiska.
Nessos
är ett högpresterande strömbibliotek som stöder både push & pull (som Java Stream
).
PullStream och PushStream är förenklade implementeringar av Pull
& Push
strömmar.
Prestanda Resultat från körning på: F # 4.0 - .NET 4.6.1 - x64
Stängerna visar tiden som gått, lägre är bättre. Den totala mängden användbart arbete är densamma för alla tester så resultaten är jämförbara. Detta innebär också att få körningar innebär större datasätt.
Som vanligt när man mäter en ser intressanta resultat.
-
List
dålig prestanda jämförs med andra alternativ för stora datamängder. Detta kan bero påGC
eller dålig cache-lokalitet. -
Array
prestanda bättre än väntat. -
LINQ
presterar bättre änSeq
, detta är oväntat eftersom båda är baserade kringIEnumerable<'T>
. Emellertid ärSeq
internt baserat på en generisk impementering för alla algoritmer medanLINQ
använder specialiserade algoritmer. -
Push
fungerar bättre änPull
. Detta förväntas eftersom push-dataledningen har färre kontroller - De förenklade
Push
dataledningarna fungerar jämförbara medNessos
.Nessos
stöder dock drag och parallellitet. - För små
Nessos
försämrasNessos
prestanda möjligt eftersom rörledningar installerar över huvudet. - Som förväntat fungerade
Imperative
koden bäst
GC Collection räknar från att köra på: F # 4.0 - .NET 4.6.1 - x64
Stängerna visar det totala antalet GC
samlingar under testet, lägre är bättre. Detta är en mätning av hur många objekt som skapas av datapipeline.
Som vanligt när man mäter en ser intressanta resultat.
-
List
skapar förväntat fler objekt änArray
eftersom enList
huvudsak är en enda länkad noderlista. En matris är ett kontinuerligt minnesområde. - När man tittar på de underliggande siffrorna tvingar både
List
&Array
2-generationssamlingar. Den här typen av samling är dyra. -
Seq
utlöser en överraskande mängd samlingar. Det är förvånansvärt ännu värre änList
i detta avseende. -
LINQ
,Nessos
,Push
andPull
utlöser inga samlingar för få körningar. Men objekt tilldelas så attGC
så småningom måste köra. - Som förväntat sedan den
Imperative
koden allokerar inga objekt utlöste ingaGC
samlingar.
Slutsats
Alla dataledningar utför samma mängd användbart arbete i alla testfall, men vi ser betydande skillnader i prestanda och minnesanvändning mellan de olika rörledningarna.
Dessutom märker vi att omkostnaderna för datapipelinjer skiljer sig beroende på storleken på behandlade data. Till exempel, Array
presterar ganska bra för små storlekar.
Man bör komma ihåg hur mycket arbete som utförs i varje steg i rörledningen är mycket litet för att mäta omkostnaderna. I "riktiga" situationer overhead Seq
kanske ingen roll eftersom det faktiska arbetet är mer tidskrävande.
Mer oro är skillnaderna i minnesanvändning. GC
är inte gratis och det är fördelaktigt för applikationer med lång drift att hålla GC
trycket nere.
För F#
-utvecklare som är bekymrade över prestanda och minnesanvändning rekommenderas att kolla in Nessos Streams .
Om du behöver högsta prestanda strategiskt placerad är Imperative
kod värt att överväga.
Slutligen, när det gäller prestanda gör inte antaganden. Mät och verifiera.
Full källkod:
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