2011-02-02 13 views
7

Jest już wiele dyskusji na ten temat, ale ja jestem wszystkim o chłostaniu martwych koni, szczególnie gdy odkrywam, że wciąż mogą oddychać.F # vs. C# performance Sygnatury z próbnym kodem

Pracowałem nad parsowaniem nietypowego i egzotycznego formatu pliku, jakim jest plik CSV, a dla zabawy postanowiłem scharakteryzować wydajność w porównaniu z dwoma znanymi mi językami .net, C# i F #.

Wyniki były ... niepokojące. F # wygrał, z dużym marginesem, o współczynniku 2 lub więcej (i uważam, że jest bardziej podobny do .5n, ale uzyskanie prawdziwych testów porównawczych okazuje się trudne, ponieważ testuję sprzętowo IO).

Rozbieżne cechy wydajności w coś tak powszechnego, jak czytanie CSV jest dla mnie zaskakujące (należy zauważyć, że współczynnik ten oznacza, że ​​C# wygrywa na bardzo małych plikach. Im więcej testów robię, tym bardziej czuje się, że skala C# jest gorsza, co jest zarówno zaskakujące, jak i niepokojące, ponieważ prawdopodobnie oznacza to, że robię to źle).

Kilka uwag: laptop Core 2 duo, dysk wrzeciona 80 gigabajtów, 3 gigabajty pamięci ddr 800, Windows 7 64-bit premium, .Net 4, brak opcji zasilania włączone.

30000 linie 5 szerokości 1 fraza 10 chars lub mniej daje mi współczynnik 3 na korzyść rekursji połączeń ogon po pierwszym biegu (wydaje się buforować plik)

300000 (powtarzane te same dane) jest współczynnikiem 2 dla rekurencji ogłaszania ogonowego z użyciem zmiennego wdrożenia F #, ale wyniki sygnalizują, że trafiam na dysk, a nie na cały dysk, co powoduje pół-losowe skoki wydajności.

F # kod

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

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

} 

Uwaga Różnorodność implementacje tam. Używanie iteratorów, używanie sekwencji, używanie optymalizacji ogłaszania połączenia, pętle w 2 językach ...

Głównym problemem jest to, że trafiam na dysk, a więc niektóre idiosynkracje mogą być przez to rozumiane, zamierzam przepisać to na nowo kod do odczytu ze strumienia pamięci (który powinien być bardziej spójny zakładając, że nie zaczynam się zamieniać)

Ale wszystko, czego uczono/czytałem, mówi, że pętle/pętle są szybsze niż optymalizacje wywołania/rekursja ogłaszania, oraz każdy rzeczywisty benchmark, który prowadzę, mówi, że jest to przeciwieństwo.

Sądzę, że moje pytanie brzmi, czy powinienem kwestionować konwencjonalną mądrość?

Czy rekursja połączenia ogonowego jest naprawdę lepsza niż podczas pętli w ekosystemie .net?

Jak to działa na Mono?

+1

Próbowałem go dla siebie i nie mogę odtworzyć wyników. Moje testy powodują, że C# jest o 50% szybsze, zobacz moją odpowiedź poniżej ... – MartinStettner

+0

Okazuje się, że benchmarki szukały w niewłaściwym miejscu, to jest procesor związany na .split, nie związany, I to było około 3,5 sekundy dla wszystkich implementacji do odczytu w danych, z wyjątkiem linii .ReadLines, która trwała około 10 sekund. The .Split był zróżnicowany, ale ogólnie znajdował się w tym samym parku (około 20 sekund). – Snark

Odpowiedz

5

Myślę, że różnica może wynikać z różnych List s w F # i C#. F # używa pojedynczo połączonych list (patrz http://msdn.microsoft.com/en-us/library/dd233224.aspx), podczas gdy w C# System.Collections.Generic.List jest używany, który jest oparty na tablicach.

Łączenie jest znacznie szybsze dla pojedynczo połączonych list, szczególnie podczas analizowania dużych plików (należy od czasu do czasu przydzielić/skopiować całą listę tablic).

Spróbuj użyć LinkedList w kodzie C#, jestem ciekaw wyników :) ...

PS: Również, to byłoby dobrym przykładem na kiedy używać profilera. Można łatwo znaleźć „hot spot” kodu C# ...

EDIT

Tak, próbowałem to dla siebie: Kiedyś dwa identyczne pliki w celu uniknięcia skutków buforowania. Pliki były 3.000.000 linii z 10 razy "abcdef", oddzielone przecinkiem.

Główny program wygląda następująco:

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

(ja też próbowałem go z pierwszym wykonaniem # realizację F, a następnie C# ...)

Wynikiem jest:

  • C#: 3,7 sekundy
  • F #: 7,6 sekundy

Uruchamianie rozwiązania C# po rozwiązanie F # daje taką samą wydajność dla wersji F #, ale 4,7 sekundy dla C# (Zakładam, ze względu na dużą alokację pamięci przez rozwiązanie F #). Samo uruchomienie każdego rozwiązania nie zmienia powyższych wyników.

pomocą pliku z 6.000.000 linii daje ~ 7 sekund w roztworze C#, przyciski # rozwiązanie F wytwarza OutOfMemoryException (biegnę to na maching z 12GB Ram ...)

Więc dla mnie wydaje się, że konwencjonalna "mądrość" jest prawdziwa, a C# za pomocą prostej pętli jest szybsze dla tego rodzaju zadań ...

+1

Mam profilera aktywny, stąd powód wiem rzeczywiste cechy wydajności. Jeśli istnieje sposób określenia czasu spędzonego na "list.add", byłoby miło, ale tak naprawdę nie wiem, jak to zrobić, ponieważ nie ma go w moim kodzie. Poza tym to nie jest hotspot, wiem o tym za fakt. To mnie po prostu dezorientuje. – Snark

+1

Użycie pliku system.generic.collections.list w kodzie F # spowodowało obniżenie wydajności o 10%, użycie listy połączonej w kodzie C# spowodowało obniżenie wydajności o ~ 5%. – Snark

+0

OK, to zabawne. Myślę, że spróbuję tego dla siebie. – MartinStettner

2

Zauważam, że twoje F # używa listy F #, a C# używa Listy .Net. Może spróbować zmienić F #, aby użyć innego typu listy, aby uzyskać więcej danych.

5

Naprawdę, naprawdę, naprawdę, naprawdę nie należy czytać niczego do tych wyników - albo porównawczych cały system jako formę testu systemu lub wyjąć dysk I/O z benchmark. Po prostu będzie to mylić sprawy. Lepszą praktyką jest raczej stosowanie parametru TextReader niż fizycznej, aby uniknąć powiązania implementacji z plikami fizycznymi.

Dodatkowo jako microbenchmark Twój badanie ma kilka innych wad:

  • zdefiniować wiele funkcji, które nie są nazywane podczas benchmarku. Czy testujesz ImportDataC lub ImportDataReadLines? Wybierz i wybierz dla jasności - w prawdziwych aplikacjach nie duplikuj implementacji, ale rozróżniaj podobieństwa i definiuj je pod względem innych.
  • Nazywasz się .Split(',') w języku F #, ale .Split() w języku C# - czy chcesz podzielić na przecinki lub na białych znakach?
  • Wymyślasz na nowo koło - przynajmniej porównaj swoją implementację z znacznie krótszymi wersjami, używając funkcji wyższego rzędu (np. LINQ).