Ricerca…


Usando la ricorsione in coda per un'iterazione efficiente

Provenienti da linguaggi imperativi, molti sviluppatori si chiedono come scrivere un for-loop che esce presto poiché F# non supporta break , continue o return . La risposta in F# consiste nell'usare la ricorsione in coda che è un modo flessibile e idiomatico per iterare pur fornendo prestazioni eccellenti.

Diciamo che vogliamo implementare tryFind per List . Se F# supportato return avremmo scritto tryFind un po 'come questo:

let tryFind predicate vs =
  for v in vs do
    if predicate v then
      return Some v
  None

Questo non funziona in F# . Invece scriviamo la funzione usando la ricorsione in coda:

let tryFind predicate vs =
  let rec loop = function
    | v::vs -> if predicate v then 
                   Some v 
               else 
                   loop vs
    | _ -> None
  loop vs

La ricorsione in coda è performante in F# perché quando il compilatore F# rileva che una funzione è ricorsiva in coda, riscrive la ricorsione in un efficiente while-loop . Usando ILSpy possiamo vedere che questo è vero per il nostro loop funzioni:

internal static FSharpOption<a> loop@3-10<a>(FSharpFunc<a, bool> predicate, FSharpList<a> _arg1)
{
  while (_arg1.TailOrNull != null)
  {
    FSharpList<a> fSharpList = _arg1;
    FSharpList<a> vs = fSharpList.TailOrNull;
    a v = fSharpList.HeadOrDefault;
    if (predicate.Invoke(v))
    {
      return FSharpOption<a>.Some(v);
    }
    FSharpFunc<a, bool> arg_2D_0 = predicate;
    _arg1 = vs;
    predicate = arg_2D_0;
  }
  return null;
}

A parte alcuni compiti non necessari (che si spera che il JIT-er elimini) questo è essenzialmente un ciclo efficiente.

Inoltre, la ricorsione in coda è idiomatica per F# quanto ci consente di evitare lo stato mutabile. Considera una funzione sum che somma tutti gli elementi in una List . Un primo tentativo ovvio sarebbe questo:

let sum vs =
  let mutable s = LanguagePrimitives.GenericZero
  for v in vs do
    s <- s + v
  s

Se riscriviamo il loop in coda-ricorsione possiamo evitare lo stato mutabile:

let sum vs =
  let rec loop s = function
    | v::vs -> loop (s + v) vs
    | _ -> s
  loop LanguagePrimitives.GenericZero vs

Per efficienza, il compilatore F# trasforma in un while-loop che utilizza lo stato mutabile.

Misura e verifica le ipotesi di rendimento

Questo esempio è scritto con F# in mente ma le idee sono applicabili in tutti gli ambienti

La prima regola quando si ottimizzano le prestazioni consiste nel non fare affidamento sull'ipotesi; Misurare sempre e verificare le ipotesi.

Poiché non stiamo scrivendo direttamente il codice macchina, è difficile prevedere come il compilatore e JIT: er trasformino il programma in codice macchina. Ecco perché dobbiamo misurare il tempo di esecuzione per vedere che otteniamo il miglioramento delle prestazioni che ci aspettiamo e verificare che il programma attuale non contenga alcun overhead nascosto.

La verifica è il processo abbastanza semplice che coinvolge il reverse engineering dell'eseguibile usando strumenti come ILSpy . Il JIT: er complica la verifica in quanto vedere il codice macchina effettivo è difficile ma fattibile. Tuttavia, di solito l'esame del IL-code fornisce grandi vantaggi.

Il problema più difficile è Misurare; più difficile perché è difficile configurare situazioni realistiche che consentano di misurare i miglioramenti nel codice. La misurazione continua è inestimabile.

Analizzando semplici funzioni F #

Esaminiamo alcune semplici funzioni F# che accumulano tutti gli interi in 1..n scritti in vari modi. Poiché l'intervallo è una semplice serie aritmetica, il risultato può essere calcolato direttamente, ma per lo scopo di questo esempio, iteriamo sull'intervallo.

Per prima cosa definiamo alcune utili funzioni per misurare il tempo impiegato da una funzione:

// now () returns current time in milliseconds since start
let now : unit -> int64 =
  let sw = System.Diagnostics.Stopwatch ()
  sw.Start ()
  fun () -> sw.ElapsedMilliseconds

// time estimates the time 'action' repeated a number of times
let time repeat action : int64*'T =
  let v = action ()  // Warm-up and compute value

  let b = now ()
  for i = 1 to repeat do
    action () |> ignore
  let e = now ()

  e - b, v

time esegue ripetutamente un'azione, è necessario eseguire i test per alcune centinaia di millisecondi per ridurre la varianza.

Quindi definiamo alcune funzioni che accumulano tutti gli interi in 1..n in modi diversi.

// Accumulates all integers 1..n using 'List'
let accumulateUsingList n =
  List.init (n + 1) id
  |> List.sum

// Accumulates all integers 1..n using 'Seq'
let accumulateUsingSeq n =
  Seq.init (n + 1) id
  |> Seq.sum

// Accumulates all integers 1..n using 'for-expression'
let accumulateUsingFor n =
  let mutable sum = 0
  for i = 1 to n do
    sum <- sum + i
  sum

// Accumulates all integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach n =
  let mutable sum = 0
  for i in 1..n do
    sum <- sum + i
  sum

// Accumulates all integers 1..n using 'foreach-expression' over list range
let accumulateUsingForEachOverList n =
  let mutable sum = 0
  for i in [1..n] do
    sum <- sum + i
  sum

// Accumulates every second integer 1..n using 'foreach-expression' over range
let accumulateUsingForEachStep2 n =
  let mutable sum = 0
  for i in 1..2..n do
    sum <- sum + i
  sum

// Accumulates all 64 bit integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach64 n =
  let mutable sum = 0L
  for i in 1L..int64 n do
    sum <- sum + i
  sum |> int

// Accumulates all integers n..1 using 'for-expression' in reverse order
let accumulateUsingReverseFor n =
  let mutable sum = 0
  for i = n downto 1 do
    sum <- sum + i
  sum

// Accumulates all 64 integers n..1 using 'tail-recursion' in reverse order
let accumulateUsingReverseTailRecursion n =
  let rec loop sum i =
    if i > 0 then
      loop (sum + i) (i - 1)
    else
      sum
  loop 0 n

Assumiamo che il risultato sia lo stesso (ad eccezione di una delle funzioni che utilizza l'incremento di 2 ) ma c'è una differenza nelle prestazioni. Per misurare questo è definita la seguente funzione:

let testRun (path : string) =
  use testResult = new System.IO.StreamWriter (path)
  let write   (l : string)  = testResult.WriteLine l
  let writef  fmt           = FSharp.Core.Printf.kprintf write fmt

  write "Name\tTotal\tOuter\tInner\tCC0\tCC1\tCC2\tTime\tResult"

  // total is the total number of iterations being executed
  let total   = 10000000
  // outers let us variate the relation between the inner and outer loop
  //  this is often useful when the algorithm allocates different amount of memory
  //  depending on the input size. This can affect cache locality
  let outers  = [| 1000; 10000; 100000 |]
  for outer in outers do
    let inner = total / outer

    // multiplier is used to increase resolution of certain tests that are significantly
    //  faster than the slower ones

    let testCases =
      [|
    //   Name of test                         multiplier    action
        "List"                              , 1           , accumulateUsingList
        "Seq"                               , 1           , accumulateUsingSeq
        "for-expression"                    , 100         , accumulateUsingFor
        "foreach-expression"                , 100         , accumulateUsingForEach
        "foreach-expression over List"      , 1           , accumulateUsingForEachOverList
        "foreach-expression increment of 2" , 1           , accumulateUsingForEachStep2
        "foreach-expression over 64 bit"    , 1           , accumulateUsingForEach64
        "reverse for-expression"            , 100         , accumulateUsingReverseFor
        "reverse tail-recursion"            , 100         , accumulateUsingReverseTailRecursion
      |]
    for name, multiplier, a in testCases do
      System.GC.Collect (2, System.GCCollectionMode.Forced, true)
      let cc g = System.GC.CollectionCount g

      printfn "Accumulate using %s with outer=%d and inner=%d ..." name outer inner

      // Collect collection counters before test run
      let pcc0, pcc1, pcc2 = cc 0, cc 1, cc 2

      let ms, result       = time (outer*multiplier) (fun () -> a inner)
      let ms               = (float ms / float multiplier)

      // Collect collection counters after test run
      let acc0, acc1, acc2 = cc 0, cc 1, cc 2
      let cc0, cc1, cc2    = acc0 - pcc0, acc1 - pcc1, acc1 - pcc1
      printfn "  ... took: %f ms, GC collection count %d,%d,%d and produced %A" ms cc0 cc1 cc2 result

      writef "%s\t%d\t%d\t%d\t%d\t%d\t%d\t%f\t%d" name total outer inner cc0 cc1 cc2 ms result

Il risultato del test durante l'esecuzione su .NET 4.5.2 x64:

Risultati dei test su .NET 4.5.2 x64

Vediamo una differenza drammatica e alcuni risultati sono inaspettatamente cattivi.

Diamo un'occhiata ai casi brutti:

Elenco

// Accumulates all integers 1..n using 'List'
let accumulateUsingList n =
  List.init (n + 1) id
  |> List.sum

Quello che succede qui è una lista completa contenente tutti gli interi 1..n viene creata e ridotta usando una somma. Questo dovrebbe essere più costoso della semplice iterazione e accumulo nell'intervallo, sembra circa 42 volte più lento del ciclo for.

Inoltre, possiamo vedere che il GC ha eseguito circa 100x durante l'esecuzione del test perché il codice ha allocato molti oggetti. Anche questo costa CPU.

Seq

// Accumulates all integers 1..n using 'Seq'
let accumulateUsingSeq n =
  Seq.init (n + 1) id
  |> Seq.sum

La versione Seq non assegna un List completo quindi è un po 'sorprendente che questo ~ 270x più lento del ciclo for. Inoltre, vediamo che il GC ha eseguito 661x.

Seq è inefficiente quando la quantità di lavoro per articolo è molto piccola (in questo caso aggregando due numeri interi).

Il punto è non usare mai Seq . Il punto è misurare.

( modifica manofstick: Seq.init è il colpevole di questo grave problema di prestazioni. È molto più efficace usare l'espressione { 0 .. n } invece di Seq.init (n+1) id . Questo diventerà molto più efficiente ancora quando questo PR viene unito e rilasciato Anche dopo il rilascio, il Seq.init ... |> Seq.sum originale Seq.init ... |> Seq.sum sarà ancora lento, ma in qualche modo contro-intuitivamente, Seq.init ... |> Seq.map id |> Seq.sum sarà abbastanza veloce, per mantenere la compatibilità con l'implementazione di Seq.init , che non calcola inizialmente Current , ma piuttosto li avvolge in un oggetto Lazy - anche se anche questo dovrebbe funzionare un po 'meglio a causa di questo PR Note per l'editore: scusa si tratta di una specie di note vaganti, ma non voglio che le persone vengano messe fuori Seq quando il miglioramento è dietro l'angolo ... Quando arriveranno i tempi sarebbe bello aggiornare i grafici che sono in questa pagina. )

foreach-expression su List

// Accumulates all integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach n =
  let mutable sum = 0
  for i in 1..n do
    sum <- sum + i
  sum

// Accumulates all integers 1..n using 'foreach-expression' over list range
let accumulateUsingForEachOverList n =
  let mutable sum = 0
  for i in [1..n] do
    sum <- sum + i
  sum

La differenza tra queste due funzioni è molto sottile, ma la differenza di prestazioni non è di circa 76x. Perché? Eseguiamo il reverse engineering del codice errato:

public static int accumulateUsingForEach(int n)
{
  int sum = 0;
  int i = 1;
  if (n >= i)
  {
    do
    {
      sum += i;
      i++;
    }
    while (i != n + 1);
  }
  return sum;
}

public static int accumulateUsingForEachOverList(int n)
{
  int sum = 0;
  FSharpList<int> fSharpList = SeqModule.ToList<int>(Operators.CreateSequence<int>(Operators.OperatorIntrinsics.RangeInt32(1, 1, n)));
  for (FSharpList<int> tailOrNull = fSharpList.TailOrNull; tailOrNull != null; tailOrNull = fSharpList.TailOrNull)
  {
    int i = fSharpList.HeadOrDefault;
    sum += i;
    fSharpList = tailOrNull;
  }
  return sum;
}

accumulateUsingForEach è implementato come un ciclo while efficiente ma for i in [1..n] viene convertito in:

FSharpList<int> fSharpList =
  SeqModule.ToList<int>(
    Operators.CreateSequence<int>(
      Operators.OperatorIntrinsics.RangeInt32(1, 1, n)));

Ciò significa innanzitutto creare un Seq su 1..n e infine chiamare ToList .

Costoso.

incremento di foreach-expression di 2

// Accumulates all integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach n =
  let mutable sum = 0
  for i in 1..n do
    sum <- sum + i
  sum

// Accumulates every second integer 1..n using 'foreach-expression' over range
let accumulateUsingForEachStep2 n =
  let mutable sum = 0
  for i in 1..2..n do
    sum <- sum + i
  sum

Ancora una volta la differenza tra queste due funzioni è sottile ma la differenza di prestazioni è brutale: ~ 25x

Ancora una volta ILSpy :

public static int accumulateUsingForEachStep2(int n)
{
  int sum = 0;
  IEnumerable<int> enumerable = Operators.OperatorIntrinsics.RangeInt32(1, 2, n);
  foreach (int i in enumerable)
  {
    sum += i;
  }
  return sum;
}

Un Seq viene creato su 1..2..n e poi 1..2..n su Seq usando l'enumeratore.

Ci aspettavamo che F# creasse qualcosa del genere:

public static int accumulateUsingForEachStep2(int n)
{
  int sum = 0;
  for (int i = 1; i < n; i += 2)
  {
    sum += i;
  }
  return sum;
}

Tuttavia, il compilatore F# supporta solo cicli efficienti su intervalli int32 che aumentano di uno. Per tutti gli altri casi ricade su Operators.OperatorIntrinsics.RangeInt32 . Che spiegherà il prossimo risultato sorprendente

foreach-expression over 64 bit

// Accumulates all 64 bit integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach64 n =
  let mutable sum = 0L
  for i in 1L..int64 n do
    sum <- sum + i
  sum |> int

Questo esegue ~ 47 volte più lento del ciclo for, l'unica differenza è che iteriamo su interi a 64 bit. ILSpy ci mostra perché:

public static int accumulateUsingForEach64(int n)
{
  long sum = 0L;
  IEnumerable<long> enumerable = Operators.OperatorIntrinsics.RangeInt64(1L, 1L, (long)n);
  foreach (long i in enumerable)
  {
    sum += i;
  }
  return (int)sum;
}

F# supporta solo cicli efficienti per i numeri int32 che deve utilizzare il fallback Operators.OperatorIntrinsics.RangeInt64 .

Gli altri casi si presentano approssimativamente simili:

Risultati dei test su .NET 4.5.2 x64

Il motivo per cui le prestazioni si riducono per le esecuzioni di test più grandi è che il sovraccarico di invocare l' action è in aumento man mano che facciamo sempre meno lavoro in action .

Il loop verso 0 può talvolta dare benefici in termini di prestazioni poiché potrebbe salvare un registro della CPU, ma in questo caso la CPU ha i registri da risparmiare, quindi non sembra fare la differenza.

Conclusione

La misurazione è importante perché altrimenti potremmo pensare che tutte queste alternative siano equivalenti, ma alcune alternative sono ~ 270 volte più lente di altre.

La fase di verifica implica il reverse engineering, l'eseguibile ci aiuta a spiegare perché abbiamo ottenuto o meno le prestazioni che ci aspettavamo. Inoltre, la verifica può aiutarci a prevedere le prestazioni nei casi in cui è troppo difficile eseguire una misurazione adeguata.

È difficile prevedere le prestazioni là sempre Misura, verifica sempre le ipotesi di rendimento.

Confronto tra diverse pipeline di dati F #

In F# ci sono molte opzioni per la creazione di pipeline di dati, ad esempio: List , Seq e Array .

Quale pipeline di dati è preferibile dall'uso della memoria e dal punto di vista delle prestazioni?

Per rispondere a questo, confronteremo le prestazioni e l'utilizzo della memoria utilizzando diverse pipeline.

Pipeline di dati

Per misurare il sovraccarico, utilizzeremo una pipeline di dati con un costo di cpu basso per articoli elaborati:

let seqTest n =
  Seq.init (n + 1) id
  |> Seq.map    int64
  |> Seq.filter (fun v -> v % 2L = 0L)
  |> Seq.map    ((+) 1L)
  |> Seq.sum

Creeremo condutture equivalenti per tutte le alternative e le confronteranno.

Variamo la dimensione di n ma lasciamo che il numero totale di lavori sia lo stesso.

Alternative alla pipeline di dati

Confronteremo le seguenti alternative:

  1. Codice imperativo
  2. Matrice (non pigra)
  3. Elenco (non pigro)
  4. LINQ (Lazy pull stream)
  5. Seq (Lazy pull stream)
  6. Nessos (Lazy pull / push stream)
  7. PullStream (flusso di pull semplicistico)
  8. PushStream (flusso push semplicistico)

Sebbene non si tratti di una pipeline di dati, confronteremo il codice Imperative dal momento che più strettamente corrisponde al modo in cui la CPU esegue il codice. Questo dovrebbe essere il modo più veloce per calcolare il risultato, consentendoci di misurare il sovraccarico delle prestazioni delle pipeline di dati.

Array e List calcolano una Array / List in ogni passaggio, quindi ci aspettiamo un sovraccarico della memoria.

LINQ e Seq sono entrambi basati su IEnumerable<'T> che è lazy pull stream (pull significa che lo stream consumer sta estraendo i dati dallo stream del produttore). Pertanto, ci aspettiamo che le prestazioni e l'utilizzo della memoria siano identici.

Nessos è una libreria di stream ad alte prestazioni che supporta sia push & pull (come Java Stream ).

PullStream e PushStream sono implementazioni semplicistiche dei flussi Pull & Push .

Prestazioni Risultati dall'esecuzione su: F # 4.0 - .NET 4.6.1 - x64

Prestazioni Risultati dall'esecuzione su: F # 4.0 - .NET 4.6.1 - x64

Le barre mostrano il tempo trascorso, più in basso è meglio. La quantità totale di lavoro utile è la stessa per tutti i test, quindi i risultati sono comparabili. Ciò significa anche che poche esecuzioni implicano set di dati più grandi.

Come al solito quando si misura uno vedi risultati interessanti.

  1. List prestazioni di List scadenti vengono confrontate con altre alternative per set di dati di grandi dimensioni. Questo può essere dovuto a GC o alla scarsa localizzazione della cache.
  2. Array prestazioni della Array sono migliori del previsto.
  3. LINQ prestazioni migliori di Seq , questo è inaspettato perché entrambi sono basati su IEnumerable<'T> . Tuttavia, Seq internamente si basa su una generica impementazione per tutti gli algoritmi mentre LINQ utilizza algoritmi specializzati.
  4. Push esegue meglio di Pull . Questo è previsto poiché la pipeline di dati push ha meno controlli
  5. Le semplicistiche pipeline di dati Push sono comparabili a quelle di Nessos . Tuttavia, Nessos supporta pull e parallelismo.
  6. Per le pipeline di dati di piccole dimensioni, le prestazioni di Nessos riducono a causa del sovraccarico dell'installazione delle pipeline.
  7. Come previsto, il codice Imperative ottenuto il meglio

Conteggio delle raccolte GC dall'esecuzione su: F # 4.0 - .NET 4.6.1 - x64

Conteggio delle raccolte GC dall'esecuzione su: F # 4.0 - .NET 4.6.1 - x64

Le barre mostrano il numero totale di conteggi di raccolta GC durante il test, più basso è migliore. Questa è una misura del numero di oggetti creati dalla pipeline di dati.

Come al solito quando si misura uno vedi risultati interessanti.

  1. È previsto che la List crei più oggetti rispetto Array perché un List è essenzialmente un singolo elenco di nodi collegati. Una matrice è un'area di memoria continua.
  2. Guardando i numeri sottostanti sia List Array costringono 2 collezioni di generazione. Questo tipo di collezione è costoso.
  3. Seq sta innescando una quantità sorprendente di collezioni. È sorprendentemente persino peggiore di List in questo senso.
  4. LINQ , Nessos , Push e Pull non attivano raccolte per poche esecuzioni. Tuttavia, gli oggetti sono assegnati in modo che il GC alla fine debba essere eseguito.
  5. Come previsto dal momento che il codice Imperative non ha assegnato alcun oggetto, non sono state attivate raccolte GC .

Conclusione

Tutte le pipeline di dati svolgono la stessa quantità di lavoro utile in tutti i casi di test, ma vediamo differenze significative nelle prestazioni e nell'utilizzo della memoria tra le diverse pipeline.

Inoltre, notiamo che il sovraccarico delle pipeline di dati varia a seconda delle dimensioni dei dati elaborati. Ad esempio, per le piccole dimensioni, l' Array si comporta abbastanza bene.

Si dovrebbe tenere a mente che la quantità di lavoro svolto in ogni fase della pipeline è molto piccola per misurare il sovraccarico. In situazioni "reali" il sovraccarico di Seq potrebbe non avere importanza perché il lavoro effettivo richiede più tempo.

Di maggiore preoccupazione sono le differenze di utilizzo della memoria. GC non è gratuito ed è vantaggioso per le applicazioni di lunga durata che riducono la pressione di GC .

Per gli sviluppatori di F# preoccupati per le prestazioni e l'utilizzo della memoria, si consiglia di controllare Nessos Streams .

Se avete bisogno di prestazioni di alto livello, il codice Imperative posizionato strategicamente vale la pena di essere preso in considerazione.

Infine, quando si tratta di prestazioni, non fare supposizioni. Misura e verifica.

Codice sorgente completo:

module PushStream =
  type Receiver<'T> = 'T -> bool
  type Stream<'T>   = Receiver<'T> -> unit

  let inline filter (f : 'T -> bool) (s : Stream<'T>) : Stream<'T> =
    fun r -> s (fun v -> if f v then r v else true)

  let inline map (m : 'T -> 'U) (s : Stream<'T>) : Stream<'U> =
    fun r -> s (fun v -> r (m v))

  let inline range b e : Stream<int> =
    fun r ->
      let rec loop i = if i <= e && r i then loop (i + 1)
      loop b

  let inline sum (s : Stream<'T>) : 'T =
    let mutable state = LanguagePrimitives.GenericZero<'T>
    s (fun v -> state <- state + v; true)
    state

module PullStream =

  [<Struct>]
  [<NoComparison>]
  [<NoEqualityAttribute>]
  type Maybe<'T>(v : 'T, hasValue : bool) =
    member    x.Value        = v
    member    x.HasValue     = hasValue
    override  x.ToString ()  =
      if hasValue then
        sprintf "Just %A" v
      else
        "Nothing"

  let Nothing<'T>     = Maybe<'T> (Unchecked.defaultof<'T>, false)
  let inline Just v   = Maybe<'T> (v, true)

  type Iterator<'T> = unit -> Maybe<'T>
  type Stream<'T>   = unit -> Iterator<'T>

  let filter (f : 'T -> bool) (s : Stream<'T>) : Stream<'T> =
    fun () ->
      let i = s ()
      let rec pop () =
        let mv = i ()
        if mv.HasValue then
          let v = mv.Value
          if f v then Just v else pop ()
        else
          Nothing
      pop

  let map (m : 'T -> 'U) (s : Stream<'T>) : Stream<'U> =
    fun () ->
      let i = s ()
      let pop () =
        let mv = i ()
        if mv.HasValue then
          Just (m mv.Value)
        else
          Nothing
      pop

  let range b e : Stream<int> =
    fun () ->
      let mutable i = b
      fun () ->
        if i <= e then
          let p = i
          i <- i + 1
          Just p
        else
          Nothing

  let inline sum (s : Stream<'T>) : 'T =
    let i = s ()
    let rec loop state =
      let mv = i ()
      if mv.HasValue then
        loop (state + mv.Value)
      else
        state
    loop LanguagePrimitives.GenericZero<'T>

module PerfTest =

  open System.Linq
#if USE_NESSOS
  open Nessos.Streams
#endif

  let now =
    let sw = System.Diagnostics.Stopwatch ()
    sw.Start ()
    fun () -> sw.ElapsedMilliseconds

  let time n a =
    let inline cc i       = System.GC.CollectionCount i

    let v                 = a ()

    System.GC.Collect (2, System.GCCollectionMode.Forced, true)

    let bcc0, bcc1, bcc2  = cc 0, cc 1, cc 2
    let b                 = now ()

    for i in 1..n do
      a () |> ignore

    let e = now ()
    let ecc0, ecc1, ecc2  = cc 0, cc 1, cc 2

    v, (e - b), ecc0 - bcc0, ecc1 - bcc1, ecc2 - bcc2

  let arrayTest n =
    Array.init (n + 1) id
    |> Array.map    int64
    |> Array.filter (fun v -> v % 2L = 0L)
    |> Array.map    ((+) 1L)
    |> Array.sum

  let imperativeTest n =
    let rec loop s i =
      if i >= 0L then
        if i % 2L = 0L then
          loop (s + i + 1L) (i - 1L)
        else
          loop s (i - 1L)
      else
        s
    loop 0L (int64 n)

  let linqTest n =
    (((Enumerable.Range(0, n + 1)).Select int64).Where(fun v -> v % 2L = 0L)).Select((+) 1L).Sum()

  let listTest n =
    List.init (n + 1) id
    |> List.map     int64
    |> List.filter  (fun v -> v % 2L = 0L)
    |> List.map     ((+) 1L)
    |> List.sum

#if USE_NESSOS
  let nessosTest n =
    Stream.initInfinite id
    |> Stream.take    (n + 1)
    |> Stream.map     int64
    |> Stream.filter  (fun v -> v % 2L = 0L)
    |> Stream.map     ((+) 1L)
    |> Stream.sum
#endif

  let pullTest n =
    PullStream.range 0 n
    |> PullStream.map     int64
    |> PullStream.filter  (fun v -> v % 2L = 0L)
    |> PullStream.map     ((+) 1L)
    |> PullStream.sum

  let pushTest n =
    PushStream.range 0 n
    |> PushStream.map     int64
    |> PushStream.filter  (fun v -> v % 2L = 0L)
    |> PushStream.map     ((+) 1L)
    |> PushStream.sum

  let seqTest n =
    Seq.init (n + 1) id
    |> Seq.map    int64
    |> Seq.filter (fun v -> v % 2L = 0L)
    |> Seq.map    ((+) 1L)
    |> Seq.sum

  let perfTest (path : string) =
    let testCases =
      [|
        "array"       , arrayTest       
        "imperative"  , imperativeTest  
        "linq"        , linqTest        
        "list"        , listTest        
        "seq"         , seqTest         
#if USE_NESSOS
        "nessos"      , nessosTest      
#endif
        "pull"        , pullTest        
        "push"        , pushTest        
      |]
    use out                   = new System.IO.StreamWriter (path)
    let write (msg : string)  = out.WriteLine msg
    let writef fmt            = FSharp.Core.Printf.kprintf write fmt

    write "Name\tTotal\tOuter\tInner\tElapsed\tCC0\tCC1\tCC2\tResult"

    let total   = 10000000
    let outers = [| 10; 1000; 1000000 |]
    for outer in outers do
      let inner = total / outer
      for name, a in testCases do
        printfn "Running %s with total=%d, outer=%d, inner=%d ..." name total outer inner
        let v, ms, cc0, cc1, cc2 = time outer (fun () -> a inner)
        printfn "  ... %d ms, cc0=%d, cc1=%d, cc2=%d, result=%A" ms cc0 cc1 cc2 v
        writef "%s\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d" name total outer inner ms cc0 cc1 cc2 v

[<EntryPoint>]
let main argv =
  System.Environment.CurrentDirectory <- System.AppDomain.CurrentDomain.BaseDirectory
  PerfTest.perfTest "perf.tsv"
  0


Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow