F#
F # Consejos y trucos de rendimiento
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:
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:
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:
- Código imperativo
- Array (no perezoso)
- Lista (No perezoso)
- LINQ (Lazy pull stream)
- Seq (corriente de extracción perezosa)
- Nessos (Lazy pull / push stream)
- PullStream (flujo de extracción simplista)
- 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
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.
-
List
rendimiento de laList
pobre se compara con otras alternativas para grandes conjuntos de datos. Esto puede ser debido aGC
o mala ubicación de caché. -
Array
rendimiento mejor de lo esperado. -
LINQ
desempeña mejor queSeq
, esto es inesperado porque ambos se basan enIEnumerable<'T>
. Sin embargo,Seq
internamente se basa en una mejora genérica para todos los algoritmos, mientras queLINQ
utiliza algoritmos especializados. -
Push
realiza mejor quePull
. Esto se espera ya que el flujo de datos de inserción tiene menos comprobaciones - Las tuberías de datos
Push
simplistas tienen unPush
comparable al deNessos
. Sin embargo,Nessos
soporta tirón y paralelismo. - Para pequeñas tuberías de datos, el rendimiento de
Nessos
degrada debido a la sobrecarga de configuración de las tuberías. - 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
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.
- Se espera que la
List
esté creando más objetos queArray
porque unaList
es esencialmente una única lista de nodos vinculados. Una matriz es un área de memoria continua. - Al observar los números subyacentes, tanto
List
Array
obligan a las colecciones de 2 generaciones. Este tipo de colección son caras. -
Seq
está provocando una sorprendente cantidad de colecciones. Es sorprendentemente incluso peor que laList
en este sentido. -
LINQ
,Nessos
,Push
yPull
no activan colecciones para pocas ejecuciones. Sin embargo, los objetos se asignan para que elGC
finalmente tenga que ejecutarse. - Como era de esperar, ya que el código
Imperative
no asigna objetos, no se activaron coleccionesGC
.
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