2011-02-02 10 views
7

Ya hay muchas discusiones sobre este tema, pero me refiero a azotar caballos muertos, particularmente cuando descubro que todavía pueden estar respirando.F # vs. C# performance Firmas con código de ejemplo

Estaba trabajando en el análisis del formato de archivo inusual y exótico que es el CSV, y por diversión decidí caracterizar el rendimiento frente a los 2 lenguajes .net que conozco, C# y F #.

Los resultados fueron ... inquietantes. F # ganó, por un amplio margen, un factor de 2 o más (y de hecho creo que se parece más a .5n, pero obtener referencias reales está resultando difícil ya que estoy probando contra hardware IO).

Las características de rendimiento divergente en algo tan común como leer un CSV me resultan sorprendentes (tenga en cuenta que el coeficiente significa que C# gana en archivos muy pequeños. Cuantas más pruebas hago, más se siente que C# empeora, lo cual ambos sorprendentes y preocupantes, ya que probablemente significa que lo estoy haciendo mal).

Algunas notas: Core 2 duo laptop, disco de husillo 80 gigas, 3 gigas ddr 800 de memoria, Windows 7 64 bit premium, .Net 4, no hay opciones de energía activadas.

30.000 líneas 5 de ancho 1 frase 10 caracteres o menos me da un factor de 3 a favor de la recursividad llamada de cola después de la primera carrera (que aparece en caché el archivo)

300.000 (mismos datos repetidos) es un factor de 2 para la recursión de llamadas de cola con la implementación mutable de F # ganando un poco, pero las firmas de rendimiento sugieren que estoy golpeando el disco y no destruyendo todo el archivo, lo que causa picos de rendimiento semi aleatorios.

F # Código

//Module used to import data from an arbitrary CSV source 
module CSVImport 
open System.IO 

//imports the data froma path into a list of strings and an associated value 
let ImportData (path:string) : List<string []> = 

    //recursively rips through the file grabbing a line and adding it to the 
    let rec readline (reader:StreamReader) (lines:List<string []>) : List<string []> = 
     let line = reader.ReadLine() 
     match line with 
     | null -> lines 
     | _ -> readline reader (line.Split(',')::lines) 

    //grab a file and open it, then return the parsed data 
    use chaosfile = new StreamReader(path) 
    readline chaosfile [] 

//a recreation of the above function using a while loop 
let ImportDataWhile (path:string) : list<string []> = 
    use chaosfile = new StreamReader(path) 
    //values ina loop construct must be mutable 
    let mutable retval = [] 
    //loop 
    while chaosfile.EndOfStream <> true do 
     retval <- chaosfile.ReadLine().Split(',')::retval 
    //return retval by just declaring it 
    retval 

let CSVlines (path:string) : string seq= 
    seq { use streamreader = new StreamReader(path) 
      while not streamreader.EndOfStream do 
      yield streamreader.ReadLine() } 

let ImportDataSeq (path:string) : string [] list = 
    let mutable retval = [] 
    let sequencer = CSVlines path 
    for line in sequencer do 
     retval <- line.Split()::retval 
    retval 

C# Código

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.IO; 
using System.Text; 

namespace CSVparse 
{ 
    public class CSVprocess 
    { 
     public static List<string[]> ImportDataC(string path) 
     { 
      List<string[]> retval = new List<string[]>(); 
      using(StreamReader readfile = new StreamReader(path)) 
      { 
       string line = readfile.ReadLine(); 
       while (line != null) 
       { 
        retval.Add(line.Split()); 
        line = readfile.ReadLine(); 
       } 
      } 

      return retval; 
     } 

     public static List<string[]> ImportDataReadLines(string path) 
     { 
      List<string[]> retval = new List<string[]>(); 
      IEnumerable<string> toparse = File.ReadLines(path); 

      foreach (string split in toparse) 
      { 
       retval.Add(split.Split()); 
      } 
      return retval; 
     } 
    } 

} 

nota la variedad de implementaciones allí. Usando iteradores, usando secuencias, usando optimizaciones de llamadas de cola, mientras bucles en 2 idiomas ...

Un problema importante es que estoy golpeando el disco, y algunas idiosincrasias pueden explicarse por eso, tengo la intención de volver a escribir esto código para leer desde una secuencia de memoria (que debería ser más consistente asumiendo que no empiezo a intercambiar)

Pero todo lo que me enseñan/leen dice que aunque los bucles loops/for son más rápidos que las optimizaciones/recursivas de cola, y cada punto de referencia real que corro es decir lo contrario de eso.

Así que supongo que mi pregunta es, ¿debería cuestionar la sabiduría convencional?

¿La recursión de la cola de cola es realmente mejor que la de los bucles en el ecosistema .net?

¿Cómo funciona esto en Mono?

+1

Lo probé por mí mismo y no puedo reproducir sus resultados. Mis pruebas dan como resultado que C# sea un 50% más rápido, vea mi respuesta a continuación ... – MartinStettner

+0

resulta que los puntos de referencia estaban buscando en el lugar equivocado, esto es CPU enlazada a .split, no io bound, Y fue aproximadamente 3.5 segundos para todos de las implementaciones para leer en los datos, excepto .ReadLines que fue de aproximadamente 10 segundos. El .Split varió, pero también estuvo en general en el mismo estadio de béisbol (aproximadamente 20 segundos). – Snark

Respuesta

5

Creo que la diferencia puede surgir de diferentes List s en F # y C#. F # utiliza listas unidas individualmente (consulte http://msdn.microsoft.com/en-us/library/dd233224.aspx) mientras que en C# System.Collections.Generic.List se usa, que se basa en matrices.

La concatenación es mucho más rápida para listas unidas individualmente, especialmente cuando se analizan archivos grandes (es necesario asignar/copiar toda la lista de arreglos de vez en cuando).

Trate de usar un LinkedList en el código C#, tengo curiosidad por saber los resultados ... :)

PS: También, este sería un buen ejemplo de cuándo utilizar un generador de perfiles. Desde aquí se puede encontrar el "punto caliente" del código C# ...

EDITAR

tanto, he intentado por mí mismo: He utilizado dos archivos idénticos con el fin de prevenir los efectos de almacenamiento en caché. Los archivos eran 3.000.000 líneas con 10 veces 'abcdef', separados por comas.

El programa principal se ve así:

static void Main(string[] args) { 
    var dt = DateTime.Now; 
    CSVprocess.ImportDataC("test.csv"); // C# implementation 
    System.Console.WriteLine("Time {0}", DateTime.Now - dt); 
    dt = DateTime.Now; 
    CSVImport.ImportData("test1.csv"); // F# implementation 
    System.Console.WriteLine("Time {0}", DateTime.Now - dt); 
} 

(también probé con la primera ejecución de la F # aplicación y luego el C# ...)

El resultado es:

  • C#: 3.7 segundos
  • F #: 7.6 segundos

Ejecutando la solución C# después de la solución F # ofrece el mismo rendimiento para la versión F # pero 4.7 segundos para C# (supongo que debido a la asignación de memoria pesada por la solución F #). Ejecutar cada solución solo no cambia los resultados anteriores.

El uso de un archivo con 6.000.000 líneas da ~ 7 segundos para la solución C#, # la solución F produce una OutOfMemoryException (Me estoy quedando esto en un mecanizado con 12 GB de RAM ...)

Así que para mí parece que el 'conocimiento' convencional es verdadero y C# usando un bucle simple es más rápido para este tipo de tareas ...

+1

Tengo un generador de perfiles activo, de ahí la razón por la que conozco las características reales de rendimiento. Si hay una forma de determinar la cantidad de tiempo invertida en "list.add", sería bueno, pero en realidad no sé cómo hacerlo ya que no está en mi código. Además, este no es el punto de acceso, lo sé por hecho. Esto solo me está confundiendo. – Snark

+1

El uso de system.generic.collections.list en el código F # provocó una penalización de rendimiento del ~ 10%, el uso de una lista vinculada en el código C# provocó una penalización de rendimiento de ~ 5%. – Snark

+0

Ok, eso es gracioso. Creo que voy a intentar esto por mí mismo. – MartinStettner

2

Noto que parece que su F # está usando la lista F # mientras que C# está usando .Net List. Podría intentar cambiar F # para usar otro tipo de lista para más datos.

5

Realmente, realmente , realmente, realmente no debe estar leyendo cualquier cosa en estos resultados - ya sea de referencia de todo el sistema como una forma de prueba del sistema, o quitar el disco I/O de el punto de referencia. Solo va a confundir las cosas. Probablemente sea una mejor práctica tomar un parámetro TextReader en lugar de una ruta física para evitar encadenar la implementación a archivos físicos.

Además, como microbenchmark su prueba tiene algunos otros defectos:

  • Se define numerosas funciones que no son llamadas durante el índice de referencia. ¿Está probando ImportDataC o ImportDataReadLines? Elija y elija para mayor claridad, y en aplicaciones reales, no duplique las implementaciones, pero tenga en cuenta las similitudes y defina una en términos de la otra.
  • Está llamando al .Split(',') en F # pero .Split() en C# - ¿tiene la intención de dividir en coma o en espacios en blanco?
  • Está reinventando la rueda, al menos compare su implementación con las versiones mucho más cortas que utilizan funciones de orden superior (también conocido como LINQ).