Zoeken…


Tail-recursion gebruiken voor efficiënte iteratie

Veel ontwikkelaars komen uit verplichte talen en vragen zich af hoe ze een for-loop die vroegtijdig wordt afgesloten, omdat F# geen break , continue of return . Het antwoord in F# is om staartrecursie te gebruiken, wat een flexibele en idiomatische manier is om te itereren terwijl het nog steeds uitstekende prestaties levert.

Stel dat we tryFind voor List willen implementeren. Als F# ondersteund return , zouden we tryFind een beetje als dit schrijven:

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

Dit werkt niet in F# . In plaats daarvan schrijven we de functie met behulp van staartrecursie:

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

Tail-recursion is performant in F# omdat wanneer de F# compiler detecteert dat een functie tail-recursief is, het de recursie herschrijft in een efficiënte while-loop . Met behulp van ILSpy kunnen we zien dat dit waar is voor onze functie loop :

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

Afgezien van enkele onnodige opdrachten (die hopelijk de JIT-er elimineert) is dit in wezen een efficiënte lus.

Bovendien is staartrecursie idiomatisch voor F# omdat het ons toelaat om een veranderlijke toestand te vermijden. Overweeg een sum die alle elementen in een List . Een voor de hand liggende eerste poging zou dit zijn:

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

Als we de lus herschrijven naar staartrecursie, kunnen we de veranderlijke status vermijden:

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

Voor efficiëntie transformeert de F# compiler dit in een while-loop die een veranderlijke status gebruikt.

Meet en verifieer uw prestatie-aannames

Dit voorbeeld is geschreven met F# in gedachten, maar de ideeën zijn toepasbaar in alle omgevingen

De eerste regel bij het optimaliseren voor prestaties is om niet te vertrouwen op veronderstelling; Meet en verifieer altijd uw aannames.

Omdat we niet direct machinecode schrijven, is het moeilijk te voorspellen hoe de compiler en JIT: er uw programma naar machinecode transformeren. Daarom moeten we de uitvoeringstijd meten om te zien dat we de prestatieverbetering krijgen die we verwachten en verifiëren dat het eigenlijke programma geen verborgen overhead bevat.

Verificatie is het vrij eenvoudige proces waarbij het uitvoerbare bestand wordt reverse-engineering met behulp van bijvoorbeeld tools zoals ILSpy . De JIT: er bemoeilijkt de verificatie doordat het zien van de werkelijke machinecode lastig maar uitvoerbaar is. Meestal levert het onderzoeken van de IL-code de grote voordelen op.

Het moeilijkere probleem is meten; moeilijker omdat het lastig is om realistische situaties in te stellen waarmee verbeteringen in code kunnen worden gemeten. Toch is meten van onschatbare waarde.

Analyse van eenvoudige F # -functies

Laten we eens kijken naar enkele eenvoudige F# -functies die alle gehele getallen in 1..n , op verschillende manieren geschreven. Omdat het bereik een eenvoudige rekenkundige serie is, kan het resultaat direct worden berekend, maar voor dit voorbeeld herhalen we het bereik.

Eerst definiëren we enkele nuttige functies voor het meten van de tijd die een functie in beslag neemt:

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

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

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

  e - b, v

time voert herhaaldelijk een actie uit. We moeten de tests een paar honderd milliseconden uitvoeren om de variantie te verminderen.

Vervolgens definiëren we een paar functies die alle gehele getallen in 1..n op verschillende manieren verzamelen.

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

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

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

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

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

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

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

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

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

We nemen aan dat het resultaat hetzelfde is (behalve voor een van de functies die een toename van 2 ), maar is er verschil in prestaties. Om dit te meten is de volgende functie gedefinieerd:

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

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

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

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

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

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

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

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

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

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

Het testresultaat tijdens het draaien op .NET 4.5.2 x64:

Testresultaten op .NET 4.5.2 x64

We zien een dramatisch verschil en sommige resultaten zijn onverwacht slecht.

Laten we eens kijken naar de slechte gevallen:

Lijst

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

Wat hier gebeurt, is een volledige lijst met alle gehele getallen 1..n wordt gemaakt en verlaagd met een som. Dit zou duurder moeten zijn dan alleen maar itereren en accumuleren over het bereik, het lijkt ongeveer ~ 42x langzamer dan de for-lus.

Bovendien kunnen we zien dat de GC tijdens de testrun ongeveer 100x is uitgevoerd omdat de code veel objecten heeft toegewezen. Dit kost ook CPU.

Seq

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

De Seq versie kent geen volledige List dus het is een beetje verrassend dat deze ~ 270x langzamer is dan de for-lus. Bovendien zien we dat de GC 661x heeft uitgevoerd.

Seq is inefficiënt wanneer de hoeveelheid werk per artikel erg klein is (in dit geval twee gehele getallen samenvoegend).

Het punt is om nooit Seq te gebruiken. Het punt is om te meten.

( manofstick-bewerking: Seq.init is de oorzaak van dit ernstige prestatieprobleem. Het is veel efficiënter om de uitdrukking { 0 .. n } plaats van Seq.init (n+1) id . Dit wordt nog veel efficiënter wanneer deze PR wordt samengevoegd en vrijgegeven. Maar zelfs na de release, zal de originele Seq.init ... |> Seq.sum nog steeds langzaam zijn, maar enigszins contra-intuïtief, Seq.init ... |> Seq.map id |> Seq.sum zal vrij snel zijn. Dit was om achterwaartse compatibiliteit te behouden met de implementatie van Seq.init , die de Current aanvankelijk niet berekent, maar ze eerder in een Lazy object verpakt - hoewel dit ook een beetje beter zou moeten presteren vanwege deze PR . Noot voor de redactie: sorry dit is een beetje kruipende notities, maar ik wil niet dat mensen worden afgeschrikt Seq wanneer verbetering voor de deur staat ... Wanneer die tijden komen zou het goed zijn om de grafieken bij te werken die op deze pagina staan. )

foreach-expressie over lijst

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

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

Het verschil tussen deze twee functies is heel subtiel, maar het prestatieverschil is niet grofweg ~ 76x. Waarom? Laten we de slechte code reverse-engineeren:

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

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

accumulateUsingForEach is geïmplementeerd als een efficiënte while lus maar for i in [1..n] wordt omgezet in:

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

Dit betekent dat we eerst een Seq boven 1..n en ten slotte ToList aanroepen.

Duur.

foreach-expression increment van 2

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

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

Nogmaals, het verschil tussen deze twee functies is subtiel, maar het prestatieverschil is brutaal: ~ 25x

Laten we nogmaals ILSpy :

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

Een Seq wordt aangemaakt via 1..2..n en daarna herhalen we Seq met behulp van de teller.

We hadden verwacht dat F# zoiets zou creëren:

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

F# compiler ondersteunt echter alleen efficiënt voor lussen over int32-bereiken die met één toenemen. Voor alle andere gevallen valt het terug op Operators.OperatorIntrinsics.RangeInt32 . Dat zal het volgende verrassende resultaat verklaren

foreach-expressie van meer dan 64 bit

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

Dit werkt ~ 47x langzamer dan de for-lus, het enige verschil is dat we meer dan 64 bit gehele getallen itereren. ILSpy laat ons zien waarom:

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

F# ondersteunt alleen efficiënt voor lussen voor int32 nummers, het moet de fallback Operators.OperatorIntrinsics.RangeInt64 . Operators.OperatorIntrinsics.RangeInt64 .

De andere gevallen presteren ongeveer hetzelfde:

Testresultaten op .NET 4.5.2 x64

De reden dat de prestaties voor grotere testruns achteruitgaan, is dat de overhead van het aanroepen van de action groeit naarmate we steeds minder werk in action .

Lussen naar 0 kan soms prestatievoordelen opleveren, omdat het een CPU-register kan opslaan, maar in dit geval heeft de CPU registers om te sparen, zodat het geen verschil lijkt te maken.

Conclusie

Meten is belangrijk omdat we anders misschien denken dat al deze alternatieven gelijkwaardig zijn, maar sommige alternatieven zijn ~ 270x langzamer dan andere.

De verificatiestap omvat reverse-engineering. Het uitvoerbare bestand helpt ons uit te leggen waarom we wel of niet de prestaties hebben gekregen die we hadden verwacht. Bovendien kan Verificatie ons helpen de prestaties te voorspellen in het geval het te moeilijk is om een goede meting uit te voeren.

Het is moeilijk om daar de prestaties te voorspellen. Meet, controleer altijd uw prestaties.

Vergelijking van verschillende F # datapijplijnen

In F# er veel opties voor het maken van gegevenspijplijnen, bijvoorbeeld: List , Seq en Array .

Welke datapijplijn heeft de voorkeur vanuit geheugengebruik en prestatieperspectief?

Om dit te beantwoorden, vergelijken we prestaties en geheugengebruik met behulp van verschillende pijplijnen.

Gegevenspijplijn

Om de overhead te meten, gebruiken we een datapijplijn met lage cpu-kosten per verwerkte artikelen:

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

We zullen gelijkwaardige pijpleidingen maken voor alle alternatieven en deze vergelijken.

We zullen de grootte van n variëren, maar laten het totale aantal werk hetzelfde zijn.

Alternatieve datapijplijn

We zullen de volgende alternatieven vergelijken:

  1. Gebiedende wijs
  2. Array (niet lui)
  3. Lijst (niet lui)
  4. LINQ (Lazy pull stream)
  5. Seq (Lazy pull stream)
  6. Nessos (Lazy pull / push stream)
  7. PullStream (Simplistische pull-stream)
  8. PushStream (Simplistische push-stream)

Hoewel het geen gegevenspijplijn is, zullen we vergelijken met Imperative code, omdat die het beste overeenkomt met hoe de CPU code uitvoert. Dat zou die snelst mogelijke manier moeten zijn om het resultaat te berekenen, zodat we de prestatieoverhead van datapijplijnen kunnen meten.

Array en List berekenen een volledige Array / List bij elke stap, dus we verwachten geheugenoverhead.

LINQ en Seq zijn beide gebaseerd op IEnumerable<'T> dat is een luie pull-stroom (pull betekent dat de consumentenstroom gegevens uit de producentenstroom haalt). We verwachten daarom dat de prestaties en het geheugengebruik identiek zijn.

Nessos is een krachtige stream-bibliotheek die zowel push & pull ondersteunt (zoals Java Stream ).

PullStream en PushStream zijn simplistische implementaties van Pull & Push streams.

Prestaties Resultaten van het draaien op: F # 4.0 - .NET 4.6.1 - x64

Prestaties Resultaten van het draaien op: F # 4.0 - .NET 4.6.1 - x64

De balken geven de verstreken tijd aan, lager is beter. De totale hoeveelheid nuttig werk is hetzelfde voor alle tests, dus de resultaten zijn vergelijkbaar. Dit betekent ook dat weinig runs grotere datasets impliceert.

Zoals gebruikelijk bij het meten zie je interessante resultaten.

  1. List prestaties slecht is in vergelijking met andere alternatieven voor grote datasets. Dit kan komen door GC of slechte cache-locatie.
  2. Array prestaties beter dan verwacht.
  3. LINQ presteert beter dan Seq , dit is onverwacht omdat beide gebaseerd zijn op IEnumerable<'T> . Seq intern gebaseerd op een generieke impementatie voor alle algoritmen, terwijl LINQ gespecialiseerde algoritmen gebruikt.
  4. Push presteert beter dan Pull . Dit wordt verwacht omdat de push-gegevenspijplijn minder controles heeft
  5. De simplistische Push gegevenspijplijnen presteren vergelijkbaar met Nessos . Nessos ondersteunt echter pull en parallellisme.
  6. Voor kleine Nessos verslechteren de prestaties van Nessos mogelijk omdat pijpleidingen overhead opzetten.
  7. Zoals verwacht presteerde de Imperative code het beste

GC Collection telt vanaf: F # 4.0 - .NET 4.6.1 - x64

GC Collection telt vanaf: F # 4.0 - .NET 4.6.1 - x64

De balken tonen het totale aantal GC verzameltellingen tijdens de test, lager is beter. Dit is een meting van hoeveel objecten door de gegevenspijplijn zijn gemaakt.

Zoals gebruikelijk bij het meten zie je interessante resultaten.

  1. List maakt naar verwachting meer objecten dan Array omdat een List in wezen een enkele gekoppelde lijst met knooppunten is. Een array is een continu geheugengebied.
  2. Kijkend naar de onderliggende getallen dwingen beide List & Array collecties van 2 generaties. Dit soort collecties zijn duur.
  3. Seq verrassend veel collecties teweeg. Het is in dit opzicht verrassend nog erger dan List .
  4. LINQ , Nessos , Push en Pull triggeren geen collecties voor enkele runs. Er worden echter objecten toegewezen, zodat de GC uiteindelijk moet worden uitgevoerd.
  5. Zoals verwacht, omdat de Imperative geen objecten toewijst, zijn er geen GC collecties geactiveerd.

Conclusie

Alle datapijplijnen doen evenveel nuttig werk in alle testgevallen, maar we zien aanzienlijke verschillen in prestaties en geheugengebruik tussen de verschillende pijplijnen.

Bovendien merken we dat de overhead van datapijplijnen verschilt, afhankelijk van de grootte van de verwerkte gegevens. Voor kleine maten presteert Array bijvoorbeeld behoorlijk goed.

Houd er rekening mee dat de hoeveelheid werk die wordt uitgevoerd in elke stap in de pijplijn erg klein is om de overhead te meten. In "echte" situaties doet de overhead van Seq misschien niet toe, omdat het eigenlijke werk meer tijd kost.

Van groter belang zijn de verschillen in geheugengebruik. GC is niet gratis en het is gunstig voor langlopende applicaties om de GC druk GC te houden.

Voor F# -ontwikkelaars die zich zorgen maken over prestaties en geheugengebruik, is het raadzaam om Nessos Streams te bekijken .

Als u topprestaties nodig heeft, is strategisch geplaatste Imperative code het overwegen waard.

Ten slotte, als het gaat om prestaties, maak geen aannames. Meten en verifiëren.

Volledige broncode:

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

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

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

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

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

module PullStream =

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

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

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

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

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

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

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

module PerfTest =

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

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

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

    let v                 = a ()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow