Sök…


Använda svansrekursion för effektiv iteration

Kommer från nödvändiga språk många utvecklare undrar hur man skriver en for-loop som kommer ut tidigt eftersom F# inte stöder break , continue eller return . Svaret i F# är att använda svansrekursion som är ett flexibelt och idiomatiskt sätt att iterera samtidigt som det fortfarande ger utmärkta prestanda.

Säg att vi vill implementera tryFind för List . Om F# stöds return skulle vi skriva tryFind lite så här:

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

Detta fungerar inte i F# . Istället skriver vi funktionen med svansrekursion:

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

Svansrekursion är performant i F# eftersom när F# -kompilatorn upptäcker att en funktion är svansrekursiv skriver den om rekursionen till en effektiv while-loop . Använda ILSpy vi kan se att detta är sant för vår funktion loop :

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

Bortsett från några onödiga uppdrag (som förhoppningsvis eliminerar JIT-er) är detta i huvudsak en effektiv slinga.

Dessutom är svansrekursion idiomatisk för F# eftersom det tillåter oss att undvika muterbara tillstånd. Tänk på en sum som summerar alla element i en List . Ett uppenbart första försök skulle vara detta:

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

Om vi skriver om slingan till svansrekursion kan vi undvika det muterbara tillståndet:

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

För effektivitet förvandlar F# -kompilern detta till en while-loop som använder ett muterbara tillstånd.

Mät och verifiera dina antaganden om prestanda

Detta exempel är skrivet med F# i åtanke men idéerna är tillämpliga i alla miljöer

Den första regeln när man optimerar för prestanda är att inte lita på antagandet; Mät och verifiera alltid dina antaganden.

Eftersom vi inte skriver maskinkod direkt är det svårt att förutsäga hur kompilatorn och JIT: er förvandlar ditt program till maskinkod. Det är därför vi måste mäta körningstiden för att se att vi får den prestandaförbättring vi förväntar oss och verifiera att det verkliga programmet inte innehåller något doldt omkostnad.

Verifiering är den ganska enkla processen som innebär omvänd konstruktion av den körbara med exempelvis verktyg som ILSpy . JIT: er komplicerar verifiering genom att det är svårt att se själva maskinkoden men genomförbart. Men vanligtvis att granska IL-code ger de stora vinsterna.

Det svårare problemet är att mäta; svårare eftersom det är svårt att ställa in realistiska situationer som gör det möjligt att mäta förbättringar i koden. Att mäta är fortfarande ovärderligt.

Analysera enkla F # -funktioner

Låt oss undersöka några enkla F# -funktioner som samlar alla heltal i 1..n skrivna på olika olika sätt. Eftersom intervallet är en enkel aritmetisk serie kan resultatet beräknas direkt men för detta exempel upprepas vi över intervallet.

Först definierar vi några användbara funktioner för att mäta tiden en funktion tar:

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

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

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

  e - b, v

time kör en åtgärd upprepade gånger vi måste köra testerna i några hundra millisekunder för att minska variansen.

Sedan definierar vi några funktioner som samlar alla heltal på 1..n på olika sätt.

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

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

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

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

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

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

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

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

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

Vi antar att resultatet är detsamma (med undantag för en av funktionerna som använder steget 2 ) men är det skillnad i prestanda. För att mäta detta definieras följande funktion:

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

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

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

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

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

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

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

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

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

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

Testresultatet när det körs på .NET 4.5.2 x64:

Testresultat på .NET 4.5.2 x64

Vi ser dramatisk skillnad och några av resultaten är oväntat dåliga.

Låt oss titta på de dåliga fallen:

Lista

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

Vad som händer här är en fullständig lista som innehåller alla heltal 1..n skapas och reduceras med en summa. Detta borde vara dyrare än att bara iterera och ackumuleras över intervallet, det verkar ungefär ~ 42x långsammare än för slingan.

Dessutom kan vi se att GC körde cirka 100 gånger under testkörningen eftersom koden tilldelade många objekt. Detta kostar också CPU.

Seq

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

Seq versionen tilldelar inte en fullständig List så det är lite överraskande att denna ~ 270x är långsammare än för slingan. Dessutom ser vi att GC har kört 661x.

Seq är ineffektiv när mängden arbete per artikel är mycket litet (i detta fall sammanförande två heltal).

Poängen är att aldrig använda Seq . Poängen är att mäta.

( manofstick-redigering: Seq.init är den skyldige i denna allvarliga prestationsfråga. Det är mycket effektivare att använda uttrycket { 0 .. n } istället för Seq.init (n+1) id . Detta kommer att bli mycket effektivare fortfarande när denna PR slås samman och släpps. Även efter utgivningen kommer den ursprungliga Seq.init ... |> Seq.sum fortfarande att vara långsam, men något Seq.init ... |> Seq.map id |> Seq.sum , Seq.init ... |> Seq.map id |> Seq.sum kommer att vara ganska snabbt. Detta var för att upprätthålla bakåtkompatibilitet med Seq.init implementering, som inte beräknar Current initialt, utan snarare lindar dem i ett Lazy objekt - även om detta också borde fungera lite bättre på grund av denna PR . Notera till redaktör: ledsen att det här är en typ av pratande anteckningar, men jag vill inte att folk ska skjutas upp Seq när förbättringar är precis runt hörnet ... När den tiden kommer kommer det vara bra att uppdatera diagrammen som finns på den här sidan. )

förhandsuttryck över listan

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

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

Skillnaden mellan dessa två funktioner är väldigt subtil, men prestationsskillnaden är inte, ungefär ~ 76x. Varför? Låt oss omvända den dåliga koden:

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

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

accumulateUsingForEach implementeras som en effektiv while loop men for i in [1..n] omvandlas till:

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

Detta innebär att vi först skapar en Seq över 1..n och slutligen ringer ToList .

Dyr.

föruttryckningsökning av 2

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

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

Återigen är skillnaden mellan dessa två funktioner subtil men prestationsskillnaden är brutal: ~ 25x

Låt oss köra ILSpy :

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

En Seq skapas över 1..2..n och sedan itereras över Seq hjälp av enumerator.

Vi förväntade oss att F# skulle skapa något liknande:

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

Men F# -kompileraren stöder bara effektiva för slingor över int32-intervall som ökar med en. För alla andra fall faller det tillbaka på Operators.OperatorIntrinsics.RangeInt32 . Vilket kommer att förklara nästa överraskande resultat

föruttryck över 64 bitar

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

Detta utför ~ 47x långsammare än för slingan, den enda skillnaden är att vi itererar över 64 bitars heltal. ILSpy visar varför:

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

F# stöder bara effektiva för slingor för int32 nummer, den måste använda fallback Operators.OperatorIntrinsics.RangeInt64 .

De andra fallen fungerar ungefär lika:

Testresultat på .NET 4.5.2 x64

Anledningen till att prestandan försämras för större testkörningar är att omkostningen för att åberopa action växer när vi gör mindre och mindre arbete i action .

Om du går mot 0 kan det ibland ge prestandafördelar eftersom det kan spara ett CPU-register, men i detta fall har CPU-register att spara så det verkar inte göra någon skillnad.

Slutsats

Mätning är viktig eftersom vi annars skulle kunna tro att alla dessa alternativ är likvärdiga men vissa alternativ är ~ 270x långsammare än andra.

Verifieringssteget innebär omvänd konstruktion som den körbara hjälper oss förklara varför vi gjorde eller inte fick de resultat vi förväntade oss. Verifiering kan dessutom hjälpa oss att förutsäga prestanda i de fall det är för svårt att göra en korrekt mätning.

Det är svårt att förutsäga prestanda där alltid Mät, alltid verifiera dina prestandaantaganden.

Jämförelse av olika F #-dataledningar

I F# finns det många alternativ för att skapa datapipelines, till exempel: List , Seq och Array .

Vilken datapipeline är att föredra utifrån minnesanvändning och prestandaperspektiv?

För att besvara detta jämför vi prestanda och minnesanvändning med olika pipelines.

Datapipeline

För att mäta omkostnaderna kommer vi att använda en datapipeline med låg cpu-kostnad per bearbetade objekt:

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

Vi kommer att skapa likvärdiga rörledningar för alla alternativ och jämföra dem.

Vi kommer att variera storleken på n men låt det totala antalet arbeten vara detsamma.

Data Pipeline Alternativ

Vi kommer att jämföra följande alternativ:

  1. Imperativ kod
  2. Array (icke-lat)
  3. Lista (icke-lat)
  4. LINQ (Lazy pull stream)
  5. Seq (Lazy pull stream)
  6. Nessos (Lazy pull / push stream)
  7. PullStream (Simplistic pull stream)
  8. PushStream (Simplistic push stream)

Även om det inte är en datapipeline kommer vi att jämföra med Imperative , eftersom den bäst matchar hur CPU: n kör kod. Det borde vara det snabbaste möjliga sättet att beräkna resultatet så att vi kan mäta prestandakostnaderna för dataledningar.

Array och List beräknar en fullständig Array / List i varje steg så att vi förväntar oss minnesomkostnader.

LINQ och Seq är båda baserade kring IEnumerable<'T> som är lat dragström (pull innebär att konsumentströmmen drar data ur producentströmmen). Vi förväntar oss därför att prestandan och minnesanvändningen är identiska.

Nessos är ett högpresterande strömbibliotek som stöder både push & pull (som Java Stream ).

PullStream och PushStream är förenklade implementeringar av Pull & Push strömmar.

Prestanda Resultat från körning på: F # 4.0 - .NET 4.6.1 - x64

Prestanda Resultat från körning på: F # 4.0 - .NET 4.6.1 - x64

Stängerna visar tiden som gått, lägre är bättre. Den totala mängden användbart arbete är densamma för alla tester så resultaten är jämförbara. Detta innebär också att få körningar innebär större datasätt.

Som vanligt när man mäter en ser intressanta resultat.

  1. List dålig prestanda jämförs med andra alternativ för stora datamängder. Detta kan bero på GC eller dålig cache-lokalitet.
  2. Array prestanda bättre än väntat.
  3. LINQ presterar bättre än Seq , detta är oväntat eftersom båda är baserade kring IEnumerable<'T> . Emellertid är Seq internt baserat på en generisk impementering för alla algoritmer medan LINQ använder specialiserade algoritmer.
  4. Push fungerar bättre än Pull . Detta förväntas eftersom push-dataledningen har färre kontroller
  5. De förenklade Push dataledningarna fungerar jämförbara med Nessos . Nessos stöder dock drag och parallellitet.
  6. För små Nessos försämras Nessos prestanda möjligt eftersom rörledningar installerar över huvudet.
  7. Som förväntat fungerade Imperative koden bäst

GC Collection räknar från att köra på: F # 4.0 - .NET 4.6.1 - x64

GC Collection räknar från att köra på: F # 4.0 - .NET 4.6.1 - x64

Stängerna visar det totala antalet GC samlingar under testet, lägre är bättre. Detta är en mätning av hur många objekt som skapas av datapipeline.

Som vanligt när man mäter en ser intressanta resultat.

  1. List skapar förväntat fler objekt än Array eftersom en List huvudsak är en enda länkad noderlista. En matris är ett kontinuerligt minnesområde.
  2. När man tittar på de underliggande siffrorna tvingar både List & Array 2-generationssamlingar. Den här typen av samling är dyra.
  3. Seq utlöser en överraskande mängd samlingar. Det är förvånansvärt ännu värre än List i detta avseende.
  4. LINQ , Nessos , Push and Pull utlöser inga samlingar för få körningar. Men objekt tilldelas så att GC så småningom måste köra.
  5. Som förväntat sedan den Imperative koden allokerar inga objekt utlöste inga GC samlingar.

Slutsats

Alla dataledningar utför samma mängd användbart arbete i alla testfall, men vi ser betydande skillnader i prestanda och minnesanvändning mellan de olika rörledningarna.

Dessutom märker vi att omkostnaderna för datapipelinjer skiljer sig beroende på storleken på behandlade data. Till exempel, Array presterar ganska bra för små storlekar.

Man bör komma ihåg hur mycket arbete som utförs i varje steg i rörledningen är mycket litet för att mäta omkostnaderna. I "riktiga" situationer overhead Seq kanske ingen roll eftersom det faktiska arbetet är mer tidskrävande.

Mer oro är skillnaderna i minnesanvändning. GC är inte gratis och det är fördelaktigt för applikationer med lång drift att hålla GC trycket nere.

För F# -utvecklare som är bekymrade över prestanda och minnesanvändning rekommenderas att kolla in Nessos Streams .

Om du behöver högsta prestanda strategiskt placerad är Imperative kod värt att överväga.

Slutligen, när det gäller prestanda gör inte antaganden. Mät och verifiera.

Full källkod:

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

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

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

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

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

module PullStream =

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

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

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

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

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

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

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

module PerfTest =

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

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

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

    let v                 = a ()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow