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