F#
F # Performance Tips and Tricks
Recherche…
Utiliser tail-recursion pour une itération efficace
Venant de langages impératifs, de nombreux développeurs se demandent comment écrire une for-loop
qui se termine plus tôt, car F#
ne supporte pas le break
, le continue
ou le return
. La réponse dans F#
est d'utiliser la récursivité, qui est une manière flexible et idiomatique d'itérer tout en offrant d'excellentes performances.
Disons que nous voulons implémenter tryFind
pour List
. Si F#
charge le return
nous écrirons tryFind
un peu comme ceci:
let tryFind predicate vs =
for v in vs do
if predicate v then
return Some v
None
Cela ne fonctionne pas dans F#
. Au lieu de cela, nous écrivons la fonction en utilisant récursivité:
let tryFind predicate vs =
let rec loop = function
| v::vs -> if predicate v then
Some v
else
loop vs
| _ -> None
loop vs
La récursion est performante dans F#
car, lorsque le compilateur F#
détecte qu'une fonction est récursive, il réécrit la récursivité en une while-loop
efficace. En utilisant ILSpy
nous pouvons voir que cela est vrai pour notre loop
fonctions:
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;
}
En dehors de certaines affectations inutiles (qui, espérons-le, le JIT-er élimine), il s'agit essentiellement d'une boucle efficace.
De plus, la récursion de la queue est idiomatique pour F#
car elle nous permet d'éviter un état mutable. Considérons une fonction de sum
qui sum
tous les éléments d’une List
. Un premier essai évident serait ceci:
let sum vs =
let mutable s = LanguagePrimitives.GenericZero
for v in vs do
s <- s + v
s
Si nous réécrivons la boucle en queue récursive, nous pouvons éviter l'état mutable:
let sum vs =
let rec loop s = function
| v::vs -> loop (s + v) vs
| _ -> s
loop LanguagePrimitives.GenericZero vs
Pour plus d'efficacité, le compilateur F#
transforme cela en une while-loop
qui utilise l'état mutable.
Mesurer et vérifier vos hypothèses de performance
Cet exemple est écrit avec F#
en tête mais les idées sont applicables dans tous les environnements
La première règle lors de l'optimisation des performances est de ne pas s'appuyer sur des hypothèses. Toujours mesurer et vérifier vos hypothèses.
Comme nous n'écrivons pas directement le code machine, il est difficile de prédire comment le compilateur et JIT: er transforment votre programme en code machine. C'est pourquoi nous devons mesurer le temps d'exécution pour constater que nous obtenons l'amélioration des performances attendue et vérifier que le programme réel ne contient pas de surcharge cachée.
La vérification est le processus très simple qui implique le reverse engineering de l'exécutable en utilisant par exemple des outils tels que ILSpy . Le JIT: er complique la vérification en ce sens que la visualisation du code machine réel est délicate mais faisable. Cependant, l'examen du IL-code
donne généralement de gros avantages.
Le problème le plus difficile est la mesure; plus difficile car il est difficile de mettre en place des situations réalistes permettant de mesurer les améliorations du code. Still Measuring est inestimable.
Analyser des fonctions simples F #
Examinons quelques fonctions simples F#
qui accumulent tous les entiers dans 1..n
écrits de différentes manières. Comme la gamme est une série arithmétique simple, le résultat peut être calculé directement, mais pour les besoins de cet exemple, nous parcourons la plage.
Tout d'abord, nous définissons des fonctions utiles pour mesurer le temps nécessaire à une fonction:
// 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
exécute une action à plusieurs reprises, nous devons exécuter les tests pendant quelques centaines de millisecondes pour réduire les écarts.
Ensuite, nous définissons quelques fonctions qui accumulent tous les entiers dans 1..n
de différentes manières.
// 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
Nous supposons que le résultat est le même (sauf pour l'une des fonctions qui utilise l'incrément de 2
), mais y a-t-il une différence de performance. Pour mesurer ceci, la fonction suivante est définie:
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
Le résultat du test lors de l'exécution sur .NET 4.5.2 x64:
Nous constatons une différence spectaculaire et certains résultats sont inattendus.
Regardons les mauvais cas:
liste
// Accumulates all integers 1..n using 'List'
let accumulateUsingList n =
List.init (n + 1) id
|> List.sum
Ce qui se passe ici est une liste complète contenant tous les entiers 1..n
est créé et réduit en utilisant une somme. Cela devrait être plus coûteux que de simplement itérer et accumuler sur la plage, il semble environ ~ 42 fois plus lent que la boucle for.
De plus, nous pouvons voir que le GC a fonctionné environ 100 fois pendant le test, car le code allouait beaucoup d'objets. Cela coûte aussi le processeur.
Seq
// Accumulates all integers 1..n using 'Seq'
let accumulateUsingSeq n =
Seq.init (n + 1) id
|> Seq.sum
La version de Seq
n'alloue pas une List
complète donc c'est un peu surprenant que cette ~ 270x soit plus lente que la boucle for. De plus, on voit que le GC a exécuté 661x.
Seq
est inefficace lorsque la quantité de travail par élément est très faible (dans ce cas, en agrégeant deux entiers).
Le but n'est pas de ne jamais utiliser Seq
. Le point est de mesurer.
( manofstick edit: Seq.init
est le coupable de ce grave problème de performances. Il est beaucoup plus efficace d'utiliser l'expression { 0 .. n }
au lieu de l' Seq.init (n+1) id
. Cela deviendra beaucoup plus efficace encore. quand ce PR est fusionné et libéré, même après la publication, Seq.init ... |> Seq.sum
sera toujours lent, mais quelque peu contre-intuitif, Seq.init ... |> Seq.map id |> Seq.sum
sera assez rapide pour maintenir une compatibilité ascendante avec l' Seq.init
de Seq.init
, qui ne calcule pas initialement Current
, mais les encapsule plutôt dans un objet Lazy
- même si cela devrait être un peu meilleur en raison de ce PR Note à l'éditeur: désolé c'est une sorte de notes décousues, mais je ne veux pas que les gens soient rebutés par Seq quand l'amélioration est imminente ... Quand cela arrivera, il serait bon de mettre à jour les graphiques qui sont sur cette page. )
foreach-expression over 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 différence entre ces deux fonctions est très subtile mais la différence de performance n’est pas, à peu près ~ 76x. Pourquoi? Ingénérons le mauvais code:
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
est implémenté comme une boucle while
efficace, mais for i in [1..n]
est converti en:
FSharpList<int> fSharpList =
SeqModule.ToList<int>(
Operators.CreateSequence<int>(
Operators.OperatorIntrinsics.RangeInt32(1, 1, n)));
Cela signifie d'abord que nous créons une Seq
over 1..n
et que nous ToList
finalement ToList
.
Coûteux.
incrément d'expression de foreach de 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
Encore une fois la différence entre ces deux fonctions est subtile mais la différence de performance est brutale: ~ 25x
Encore une fois, 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
est créé sur 1..2..n
et ensuite nous 1..2..n
Seq
utilisant l'énumérateur.
Nous nous attendions à ce que F#
crée quelque chose comme ceci:
public static int accumulateUsingForEachStep2(int n)
{
int sum = 0;
for (int i = 1; i < n; i += 2)
{
sum += i;
}
return sum;
}
Cependant, F#
compilateur F#
ne prend en charge que les boucles efficaces sur les plages int32 incrémentées de un. Pour tous les autres cas, il se rabat sur Operators.OperatorIntrinsics.RangeInt32
. Ce qui expliquera le prochain résultat surprenant
foreach-expression sur 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
Cela effectue ~ 47x plus lentement que la boucle for, la seule différence est que nous parcourons des entiers de 64 bits. ILSpy
nous montre pourquoi:
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#
ne prend en charge que les boucles efficaces pour les numéros int32
qu'il doit utiliser les Operators.OperatorIntrinsics.RangeInt64
secours.OperatorIntrinsics.RangeInt64.
Les autres cas sont à peu près similaires:
La raison pour laquelle les performances se dégradent pour les tests de plus grande envergure réside dans le fait que l'invocation de l' action
fait de plus en plus au fur et à action
que nous travaillons de moins en moins en action
.
Faire une boucle vers 0
peut parfois apporter des avantages en termes de performances, car cela peut sauver un registre de CPU, mais dans ce cas, le CPU a des registres à épargner, cela ne semble donc pas faire de différence.
Conclusion
La mesure est importante car sinon nous pourrions penser que toutes ces alternatives sont équivalentes mais que certaines alternatives sont ~ 270x plus lentes que d'autres.
L'étape de vérification implique l'ingénierie inverse. L'exécutable nous aide à expliquer pourquoi nous avons obtenu ou non les performances attendues. De plus, la vérification peut nous aider à prédire la performance dans les cas où il est trop difficile d'effectuer une mesure correcte.
Il est difficile de prévoir les performances là-bas, toujours Mesurer, toujours vérifier vos hypothèses de performance.
Comparaison de différents pipelines de données F #
Dans F#
il existe de nombreuses options pour créer des pipelines de données, par exemple: List
, Seq
et Array
.
Quel pipeline de données est préférable du point de vue de l’utilisation de la mémoire et des performances?
Afin de répondre à cette question, nous comparerons les performances et l'utilisation de la mémoire en utilisant différents pipelines.
Pipeline de données
Afin de mesurer les frais généraux, nous utiliserons un pipeline de données à faible coût par unité de traitement, par produit traité:
let seqTest n =
Seq.init (n + 1) id
|> Seq.map int64
|> Seq.filter (fun v -> v % 2L = 0L)
|> Seq.map ((+) 1L)
|> Seq.sum
Nous allons créer des pipelines équivalents pour toutes les alternatives et les comparer.
Nous allons varier la taille de n
mais laisser le nombre total de travail être le même.
Solutions de pipeline de données
Nous comparerons les alternatives suivantes:
- Code impératif
- Tableau (non paresseux)
- Liste (non paresseux)
- LINQ (flux de tirage paresseux)
- Seq (flux de tirage paresseux)
- Nessos (flux de traction / poussée paresseux)
- PullStream (flux de tirage simpliste)
- PushStream (flux de diffusion simpliste)
Bien que ce ne soit pas un pipeline de données, nous le comparerons au code Imperative
car cela correspond le mieux à la manière dont le processeur exécute le code. Cela devrait être le moyen le plus rapide de calculer le résultat, ce qui nous permet de mesurer les performances des pipelines de données.
Array
et List
calculent un Array
/ List
complet à chaque étape, de sorte que nous attendons une surcharge de mémoire.
LINQ
et Seq
sont tous deux basés sur IEnumerable<'T>
qui est un flux d'extraction paresseux (pull signifie que le flux consommateur extrait des données du flux producteur). Nous nous attendons donc à ce que les performances et l’utilisation de la mémoire soient identiques.
Nessos
est une bibliothèque de flux haute performance qui prend en charge à la fois la fonction push & pull (comme Java Stream
).
PullStream et PushStream sont des implémentations simplistes des flux Pull
& Push
.
Résultats de performance de l'exécution sur: F # 4.0 - .NET 4.6.1 - x64
Les barres indiquent le temps écoulé, inférieur est meilleur. La quantité totale de travail utile est la même pour tous les tests, les résultats sont donc comparables. Cela signifie également que peu d'exécutions impliquent des ensembles de données plus importants.
Comme d'habitude lors de la mesure on voit des résultats intéressants.
-
List
performances médiocres est comparée à d'autres alternatives pour les grands ensembles de données. Cela peut être dû à uneGC
ou à une mauvaise localisation de cache. - Performance du
Array
meilleure que prévu. -
LINQ
fonctionne mieux queSeq
, ceci est inattendu car les deux sont basés surIEnumerable<'T>
. Cependant,Seq
interne est basé sur une implémentation générique pour tous les algorithmes, tandis queLINQ
utilise des algorithmes spécialisés. -
Push
effectue mieux quePull
. Cela est attendu car le pipeline de données push a moins de vérifications - Les pipelines de données
Push
simplistes sont comparables àNessos
. Cependant,Nessos
soutient la traction et le parallélisme. - Pour les pipelines de données de petite taille, les performances de
Nessos
se dégradent car la configuration des pipelines entraîne une surcharge. - Comme prévu, le code
Imperative
le meilleur
GC Collection compte à partir de l'exécution sur: F # 4.0 - .NET 4.6.1 - x64
Les barres indiquent le nombre total de comptes de collection de GC
pendant le test, inférieur est meilleur. Il s'agit d'une mesure du nombre d'objets créés par le pipeline de données.
Comme d'habitude lors de la mesure on voit des résultats intéressants.
-
List
crée vraisemblablement plus d'objets queArray
car uneList
est essentiellement une liste unique de nœuds liés. Un tableau est une zone de mémoire continue. - En regardant les chiffres sous-jacents,
List
etArray
imposent des collections de 2 générations. Ce type de collection est cher. -
Seq
déclenche une quantité surprenante de collections. C'est étonnamment encore pire queList
à cet égard. -
LINQ
,Nessos
,Push
andPull
ne déclenchent aucune collecte pour quelques exécutions. Cependant, les objets sont alloués pour que leGC
finisse par fonctionner. - Comme prévu, le code
Imperative
n'allouant aucun objet, aucune collectionGC
n'a été déclenchée.
Conclusion
Tous les pipelines de données effectuent la même quantité de travail utile dans tous les cas de test, mais nous constatons des différences significatives dans les performances et l'utilisation de la mémoire entre les différents pipelines.
En outre, nous remarquons que la surcharge des pipelines de données diffère en fonction de la taille des données traitées. Par exemple, pour les petites tailles, Array
fonctionne assez bien.
Il ne faut pas oublier que la quantité de travail effectuée à chaque étape du pipeline est très faible pour pouvoir mesurer les frais généraux. Dans des situations "réelles", la surcharge de Seq
peut ne pas avoir d'importance, car le travail prend plus de temps.
Les différences d'utilisation de la mémoire sont plus préoccupantes. GC
n'est pas libre et il est bénéfique pour les applications de longue durée de maintenir la pression du GC
basse.
Pour les développeurs F#
concernés par les performances et l'utilisation de la mémoire, il est recommandé de vérifier Nessos Streams .
Si vous avez besoin de performances de haut niveau placées stratégiquement, le code Imperative
mérite d’être considéré.
Enfin, en matière de performance, ne faites pas d’hypothèses. Mesurer et vérifier.
Code source complet:
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