F # против производительности C # Подписи с примером кода - PullRequest
7 голосов
/ 02 февраля 2011

На эту тему уже много дискуссий, но я все о порке мертвых лошадей, особенно когда обнаруживаю, что они все еще дышат.

Я работал над синтаксическим анализом необычного и экзотического формата файлов, который представляет собой CSV, и для интереса я решил охарактеризовать производительность в сравнении с двумя известными мне языками .net, C # и F #.

Результаты были ... тревожными. F # выиграл, с большим отрывом, в 2 раза или более (и я на самом деле думаю, что это больше похоже на .5n, но получить реальные тесты сложно, так как я тестирую аппаратный ввод / вывод).

Отличающиеся характеристики производительности в чем-то столь же распространенном, как чтение CSV, меня удивляют (обратите внимание, что коэффициент означает, что C # побеждает на очень маленьких файлах. Чем больше я тестирую, тем больше ощущается, что C # хуже масштабируется, что одновременно удивительно и волнующе, поскольку это, вероятно, означает, что я делаю это неправильно).

Некоторые примечания: ноутбук Core 2 duo, диск шпинделя 80 гигабайт, 3 гигабайта памяти ddr 800, windows 7 64-битная премия, .Net 4, опции питания не включены.

30 000 строк, 5 в ширину, 1 фраза, 10 символов или меньше, дает мне коэффициент 3 в пользу рекурсии хвостового вызова после первого запуска (похоже, для кэширования файла)

300 000 (повторяются те же данные) в 2 раза больше для рекурсии хвостового вызова с изменчивой реализацией F #, выигравшей немного, но сигнатуры производительности предполагают, что я бью по диску, а не разрываю оперативную память всего файла, что вызывает полу случайные скачки производительности.

F # код

//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 # код

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

}

Обратите внимание на разнообразие реализаций. Использование итераторов, использование последовательностей, оптимизация хвостовых вызовов, циклы в двух языках ...

Основная проблема заключается в том, что я бью диск, и поэтому некоторые специфические особенности могут быть объяснены этим, я намерен переписать этот код для чтения из потока памяти (что должно быть более последовательным, если я не начну своп)

Но все, чему меня учат / читают, говорит о том, что хотя циклы / циклы быстрее, чем оптимизация / рекурсия хвостовых вызовов, и каждый фактический тест, который я запускаю, говорит о полной противоположности этому.

Итак, я думаю, мой вопрос, должен ли я ставить под сомнение общепринятую мудрость?

Действительно ли рекурсия хвостового вызова действительно лучше, чем циклы while в экосистеме .net?

Как это работает на Mono?

Ответы [ 3 ]

5 голосов
/ 03 февраля 2011

Вы действительно, действительно , действительно , действительно не должны ничего читать в этих результатах - либо оцените всю систему в целомв качестве формы системного теста, или удалите дисковый ввод-вывод из теста.Это только запутает вопросы.Вероятно, лучше использовать параметр TextReader, а не физический путь, чтобы избежать связывания реализации с физическими файлами.

Кроме того, в качестве микробенчмарка у вашего теста есть несколько других недостатков:

  • Вы определяете многочисленные функции, которые не вызываются во время теста.Вы тестируете ImportDataC или ImportDataReadLines?Выбирайте и выбирайте для ясности - и в реальных приложениях не дублируйте реализации, но выделяйте сходства и определяйте одно в терминах другого.
  • Вы звоните .Split(',') в F #, но .Split() вC # - вы собираетесь разделить запятые или пробелы?
  • Вы изобретаете колесо - по крайней мере, сравните вашу реализацию с гораздо более короткими версиями, используя функции более высокого порядка (иначе LINQ).
5 голосов
/ 02 февраля 2011

Я думаю, что разница может возникнуть из-за разных List s в F # и C #.F # использует односвязные списки (см. http://msdn.microsoft.com/en-us/library/dd233224.aspx), тогда как в C # System.Collections.Generic.List ist используется, который основан на массивах.

Конкатенация намного быстрее для односвязных списков, особенно когда вы анализируете большойфайлы (вам нужно время от времени выделять / копировать весь список массивов).

Попробуйте использовать LinkedList в коде C #, мне интересно узнать результаты :) ...

PS: Это также хороший пример того, когда следует использовать профилировщик.Вы могли легко найти «горячую точку» кода C # ...

РЕДАКТИРОВАТЬ

Итак, я попробовал это для себя: я использовал два одинаковых файла по порядкупредотвратить кеширование.Файлы представляли собой 3.000.000 строк с 10-кратным abcdef, разделенными запятой.

Основная программа выглядит следующим образом:

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

(я также пробовал сначала выполнить F #реализация, а затем C # ...)

Результат:

  • C #: 3,7 секунды
  • F #: 7,6 секунды

Запуск решения C # после решение F # дает такую ​​же производительность для версии F #, но 4,7 секунды для C # (я полагаю, из-за большого выделения памяти решением F #).Запуск каждого решения в отдельности не меняет приведенных выше результатов.

Использование файла с 6.000.000 строк дает ~ 7 секунд для решения C #, решение F # создает исключение OutOfMemoryException (я запускаю это на механической обработкес 12 Гб оперативной памяти ...)

Так что для меня кажется, что общепринятая «мудрость» верна и C #, используя простой цикл, быстрее для такого рода задач ...

2 голосов
/ 02 февраля 2011

Замечу, что ваш F # использует список F #, а C # использует .Net List. Попробуйте изменить F #, чтобы использовать другой тип списка для получения дополнительных данных.

...