Buscar..


Usando la recursión de cola para una iteración eficiente

Viniendo de lenguajes imperativos, muchos desarrolladores se preguntan cómo escribir un for-loop que salga temprano ya que F# no admite break , continue o return . La respuesta en F# es usar la recursión de la cola, que es una forma flexible e idiomática de iterar, a la vez que proporciona un rendimiento excelente.

Digamos que queremos implementar tryFind for List . Si F# apoyara el return , escribiríamos tryFind un poco así:

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

Esto no funciona en F# . En su lugar, escribimos la función usando la recursión de cola:

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

La recursión de cola se realiza en F# porque cuando el compilador de F# detecta que una función es recursiva de cola, vuelve a escribir la recursión en un while-loop eficiente while-loop . Usando ILSpy podemos ver que esto es cierto para nuestro loop función:

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;
}

Aparte de algunas tareas innecesarias (que, con suerte, el JIT-er elimina), esto es esencialmente un bucle eficiente.

Además, la recursión de la cola es idiomática para F# ya que nos permite evitar el estado mutable. Considere una función de sum que suma todos los elementos en una List . Un primer intento obvio sería este:

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

Si reescribimos el bucle en la recursión de la cola, podemos evitar el estado mutable:

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

Por eficiencia, el compilador F# transforma en un while-loop que usa un estado mutable.

Mide y verifica tus suposiciones de desempeño

Este ejemplo está escrito con F# en mente, pero las ideas son aplicables en todos los entornos

La primera regla para optimizar el rendimiento es no basarse en suposiciones; siempre mida y verifique sus suposiciones.

Como no estamos escribiendo el código de máquina directamente, es difícil predecir cómo el compilador y JIT: er transforman su programa en código de máquina. Es por eso que debemos medir el tiempo de ejecución para ver que obtenemos la mejora de rendimiento que esperamos y verificar que el programa real no contenga ningún gasto general oculto.

La verificación es un proceso bastante simple que involucra la ingeniería inversa del ejecutable usando, por ejemplo, herramientas como ILSpy . El JIT: er complica la verificación en que ver el código de máquina real es complicado pero factible. Sin embargo, normalmente el examen del IL-code da grandes ganancias.

El problema más difícil es la medición; más difícil porque es difícil configurar situaciones realistas que permitan medir mejoras en el código. Todavía la medición es invaluable.

Analizando funciones simples de F #

Examinemos algunas funciones F# simples que acumulan todos los enteros en 1..n escritos de varias maneras diferentes. Como el rango es una serie aritmética simple, el resultado se puede calcular directamente, pero para el propósito de este ejemplo, iteramos sobre el rango.

Primero definimos algunas funciones útiles para medir el tiempo que toma una función:

// 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 ejecuta una acción repetidamente, necesitamos ejecutar las pruebas durante unos pocos cientos de milisegundos para reducir la varianza.

Luego definimos algunas funciones que acumulan todos los enteros en 1..n de diferentes maneras.

// 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

Asumimos que el resultado es el mismo (a excepción de una de las funciones que usa un incremento de 2 ), pero existe una diferencia en el rendimiento. Para medir esto se define la siguiente función:

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

El resultado de la prueba mientras se ejecuta en .NET 4.5.2 x64:

Resultados de la prueba en .NET 4.5.2 x64

Vemos una diferencia dramática y algunos de los resultados son inesperadamente malos.

Veamos los casos malos:

Lista

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

Lo que sucede aquí es una lista completa que contiene todos los enteros 1..n se crea y reduce utilizando una suma. Esto debería ser más costoso que solo iterar y acumular en el rango, parece aproximadamente 42 veces más lento que el bucle for.

Además, podemos ver que el GC se ejecutó aproximadamente 100 veces durante la ejecución de prueba porque el código asignó muchos objetos. Esto también cuesta CPU.

Seq

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

La versión Seq no asigna una List completa, por lo que es un poco sorprendente que este ~ 270x sea más lento que el bucle for. Además, vemos que el GC ha ejecutado 661x.

Seq es ineficiente cuando la cantidad de trabajo por artículo es muy pequeña (en este caso, agregando dos enteros).

El punto es no usar nunca la Seq . El punto es medir.

( edición de manofstick: Seq.init es el culpable de este grave problema de rendimiento. Es mucho más eficaz usar la expresión { 0 .. n } lugar de Seq.init (n+1) id . Esto será mucho más eficiente aún cuando este PR se fusiona y se libera. Incluso después del lanzamiento, el Seq.init ... |> Seq.sum seguirá siendo lento, pero de manera algo intuitiva, Seq.init ... |> Seq.map id |> Seq.sum será bastante rápido. Esto fue para mantener la compatibilidad hacia atrás con la implementación de Seq.init , que inicialmente no calcula la Current , sino que la envuelve en un objeto Lazy , aunque esto también debería funcionar un poco mejor debido a Nota de PR para el editor: disculpe, es una especie de notas entrecortadas, pero no quiero que la gente se desanime Seq cuando la mejora está a la vuelta de la esquina ... Cuando llegue ese momento, sería bueno actualizar los gráficos que están en esta página. )

foreach-expresión sobre lista

// 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 diferencia entre estas dos funciones es muy sutil, pero la diferencia de rendimiento no lo es, aproximadamente ~ 76x. ¿Por qué? Vamos a aplicar ingeniería inversa al código malo:

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 se implementa como un bucle while eficiente, pero for i in [1..n] se convierte en:

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

Esto significa que primero creamos una Seq sobre 1..n finalmente ToList .

Costoso.

foreach-expresión incremento 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

Una vez más, la diferencia entre estas dos funciones es sutil, pero la diferencia de rendimiento es brutal: ~ 25x

Una vez más vamos a ejecutar 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;
}

Se crea una Seq sobre 1..2..n y luego 1..2..n sobre Seq usando el enumerador.

Esperábamos que F# creara algo como esto:

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

Sin embargo, F# compilador F# solo es eficaz para bucles en rangos int32 que se incrementan en uno. Para todos los demás casos, recurre a Operators.OperatorIntrinsics.RangeInt32 . Lo que explicará el próximo resultado sorprendente.

Foreach-expresión más de 64 bits

// 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

Esto realiza ~ 47x más lento que el bucle for, la única diferencia es que iteramos más de 64 bits enteros. ILSpy nos muestra por qué:

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# solo es eficiente para bucles para números int32 , tiene que usar los Operators.OperatorIntrinsics.RangeInt64 opositores.OperatorIntrinsics.RangeInt64.

Los otros casos realizan aproximadamente similares:

Resultados de la prueba en .NET 4.5.2 x64

La razón por la que el rendimiento se degrada para ejecuciones de prueba más grandes es que la sobrecarga de invocar la action está creciendo a medida que hacemos menos y menos trabajo en action .

El bucle hacia 0 veces puede dar beneficios de rendimiento, ya que podría guardar un registro de CPU, pero en este caso la CPU tiene registros de sobra, por lo que no parece hacer una diferencia.

Conclusión

Medir es importante porque de lo contrario podríamos pensar que todas estas alternativas son equivalentes, pero algunas alternativas son ~ 270x más lentas que otras.

El paso de Verificación involucra ingeniería inversa, el ejecutable nos ayuda a explicar por qué obtuvimos o no el rendimiento que esperábamos. Además, la verificación puede ayudarnos a predecir el rendimiento en los casos en los que es demasiado difícil realizar una medición adecuada.

Es difícil predecir el rendimiento allí siempre Medida, siempre Verifique sus suposiciones de rendimiento.

Comparación de diferentes tuberías de datos F #

En F# hay muchas opciones para crear canales de datos, por ejemplo: List , Seq y Array .

¿Qué canalización de datos es preferible desde el uso de la memoria y la perspectiva del rendimiento?

Para responder a esto, compararemos el rendimiento y el uso de la memoria utilizando diferentes tuberías.

Tubería de datos

Para medir la sobrecarga, utilizaremos un flujo de datos con un bajo costo de CPU por cada artículo procesado:

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

Crearemos tuberías equivalentes para todas las alternativas y las compararemos.

Variaremos el tamaño de n pero dejaremos que el número total de trabajos sea el mismo.

Alternativas de la tubería de datos

Vamos a comparar las siguientes alternativas:

  1. Código imperativo
  2. Array (no perezoso)
  3. Lista (No perezoso)
  4. LINQ (Lazy pull stream)
  5. Seq (corriente de extracción perezosa)
  6. Nessos (Lazy pull / push stream)
  7. PullStream (flujo de extracción simplista)
  8. PushStream (secuencia de empuje simplista)

Aunque no es un flujo de datos, compararemos con el código Imperative , ya que coincide más estrechamente con la forma en que la CPU ejecuta el código. Esa debería ser la forma más rápida posible de calcular el resultado que nos permite medir la sobrecarga de rendimiento de las tuberías de datos.

Array y la List calculan una Array / List en cada paso, por lo que esperamos una sobrecarga de memoria.

LINQ y Seq se basan en IEnumerable<'T> que es un flujo perezoso de extracción (extracción significa que el flujo del consumidor está extrayendo datos del productor). Por lo tanto, esperamos que el rendimiento y el uso de la memoria sean idénticos.

Nessos es una biblioteca de secuencias de alto rendimiento que admite tanto push & pull (como Java Stream ).

PullStream y PushStream son implementaciones simplistas de las secuencias de Pull & Push .

Los resultados de rendimiento se ejecutan en: F # 4.0 - .NET 4.6.1 - x64

Los resultados de rendimiento se ejecutan en: F # 4.0 - .NET 4.6.1 - x64

Las barras muestran el tiempo transcurrido, menor es mejor. La cantidad total de trabajo útil es la misma para todas las pruebas, por lo que los resultados son comparables. Esto también significa que pocas ejecuciones implican conjuntos de datos más grandes.

Como es habitual, al medir uno se ven resultados interesantes.

  1. List rendimiento de la List pobre se compara con otras alternativas para grandes conjuntos de datos. Esto puede ser debido a GC o mala ubicación de caché.
  2. Array rendimiento mejor de lo esperado.
  3. LINQ desempeña mejor que Seq , esto es inesperado porque ambos se basan en IEnumerable<'T> . Sin embargo, Seq internamente se basa en una mejora genérica para todos los algoritmos, mientras que LINQ utiliza algoritmos especializados.
  4. Push realiza mejor que Pull . Esto se espera ya que el flujo de datos de inserción tiene menos comprobaciones
  5. Las tuberías de datos Push simplistas tienen un Push comparable al de Nessos . Sin embargo, Nessos soporta tirón y paralelismo.
  6. Para pequeñas tuberías de datos, el rendimiento de Nessos degrada debido a la sobrecarga de configuración de las tuberías.
  7. Como era de esperar el código Imperative realizó el mejor

El recuento de GC Collection se ejecuta en: F # 4.0 - .NET 4.6.1 - x64

El recuento de GC Collection se ejecuta en: F # 4.0 - .NET 4.6.1 - x64

Las barras muestran el número total de recuentos de recolección de GC durante la prueba, cuanto más bajo, mejor. Esta es una medida de cuántos objetos son creados por la tubería de datos.

Como es habitual, al medir uno se ven resultados interesantes.

  1. Se espera que la List esté creando más objetos que Array porque una List es esencialmente una única lista de nodos vinculados. Una matriz es un área de memoria continua.
  2. Al observar los números subyacentes, tanto List Array obligan a las colecciones de 2 generaciones. Este tipo de colección son caras.
  3. Seq está provocando una sorprendente cantidad de colecciones. Es sorprendentemente incluso peor que la List en este sentido.
  4. LINQ , Nessos , Push y Pull no activan colecciones para pocas ejecuciones. Sin embargo, los objetos se asignan para que el GC finalmente tenga que ejecutarse.
  5. Como era de esperar, ya que el código Imperative no asigna objetos, no se activaron colecciones GC .

Conclusión

Todas las tuberías de datos realizan la misma cantidad de trabajo útil en todos los casos de prueba, pero vemos diferencias significativas en el rendimiento y el uso de la memoria entre las distintas tuberías.

Además, observamos que la sobrecarga de las tuberías de datos varía según el tamaño de los datos procesados. Por ejemplo, para tamaños pequeños, Array está funcionando bastante bien.

Uno debe tener en cuenta que la cantidad de trabajo realizado en cada paso en la tubería es muy pequeña para medir la sobrecarga. En situaciones "reales", la sobrecarga de Seq puede no importar porque el trabajo real lleva más tiempo.

De mayor preocupación son las diferencias de uso de memoria. GC no es gratis y es beneficioso para las aplicaciones de ejecución prolongada para mantener baja la presión del GC .

Para los desarrolladores de F# preocupados por el rendimiento y el uso de la memoria, se recomienda revisar Nessos Streams .

Si necesita un rendimiento Imperative código Imperative estratégicamente ubicado merece la pena considerarlo.

Finalmente, cuando se trata de rendimiento, no hagas suposiciones. Medir y verificar.

Código fuente 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
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow