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:

Résultats de test 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:

Résultats de test sur .NET 4.5.2 x64

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:

  1. Code impératif
  2. Tableau (non paresseux)
  3. Liste (non paresseux)
  4. LINQ (flux de tirage paresseux)
  5. Seq (flux de tirage paresseux)
  6. Nessos (flux de traction / poussée paresseux)
  7. PullStream (flux de tirage simpliste)
  8. 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

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.

  1. List performances médiocres est comparée à d'autres alternatives pour les grands ensembles de données. Cela peut être dû à une GC ou à une mauvaise localisation de cache.
  2. Performance du Array meilleure que prévu.
  3. LINQ fonctionne mieux que Seq , ceci est inattendu car les deux sont basés sur IEnumerable<'T> . Cependant, Seq interne est basé sur une implémentation générique pour tous les algorithmes, tandis que LINQ utilise des algorithmes spécialisés.
  4. Push effectue mieux que Pull . Cela est attendu car le pipeline de données push a moins de vérifications
  5. Les pipelines de données Push simplistes sont comparables à Nessos . Cependant, Nessos soutient la traction et le parallélisme.
  6. 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.
  7. 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

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.

  1. List crée vraisemblablement plus d'objets que Array car une List est essentiellement une liste unique de nœuds liés. Un tableau est une zone de mémoire continue.
  2. En regardant les chiffres sous-jacents, List et Array imposent des collections de 2 générations. Ce type de collection est cher.
  3. Seq déclenche une quantité surprenante de collections. C'est étonnamment encore pire que List à cet égard.
  4. LINQ , Nessos , Push and Pull ne déclenchent aucune collecte pour quelques exécutions. Cependant, les objets sont alloués pour que le GC finisse par fonctionner.
  5. Comme prévu, le code Imperative n'allouant aucun objet, aucune collection GC 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


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow