Как разобрать текстовый файл в C # и быть связанным? - PullRequest
7 голосов
/ 23 августа 2011

Известно, что если вы читаете данные с диска, вы связаны с вводом-выводом и можете обрабатывать / анализировать прочитанные данные гораздо быстрее, чем вы можете прочитать их с диска.

Но эта общая мудрость (миф?) Не отражена в моих испытаниях. Когда я читаю текстовый файл с двойными и и int в каждой строке, разделенными пробелом, я намного медленнее, чем скорость моего физического диска (фактор 6). Текстовый файл выглядит так

1,1 0
2,1 1
3,1 2

Обновление Я включил производительность PInvoke, когда я делаю ReadFile с полным буфером за одно чтение, чтобы получить «реальную» производительность.

  • Производительность чтения файла - ReadFileIntoByteBuffer
  • Производительность StringReader.ReadLine - CountLines
  • StringReader.Readline небезопасных перфораций - ParseLinesUnsafe
  • StringReader.Читать небезопасный символ buf - ParseLinesUnsafeCharBuf
  • StringReader.ReadLine + Производительность анализа - ParseLines

Результаты

Did native read 179,0MB in                    0,4s, 484,2MB/s
Did read 10.000.000 lines in                  1,6s, 112,7MB/s
Did parse and read unsafe 179,0MB in          2,3s,  76,5MB/s
Did parse and read unsafe char buf 179,0MB in 2,8s,  63,5MB/s
Did read and parse 179,0MB in                 9,3s,  19,3MB/s

Несмотря на то, что я пытался пропустить накладные расходы на конструкцию строки в ParseLinesUnsafeCharBuf, она все еще намного медленнее, чем версия, которая каждый раз выделяет новую строку. Это все еще намного лучше, чем оригинальные 20 МБ с самым простым решением, но я думаю, что .NET должен быть в состоянии работать лучше. Если удалить логику для анализа строк, я получу 258,8 МБ / с, что очень хорошо и почти на родной скорости. Но я не вижу способа использовать небезопасный код, чтобы сделать мой анализ намного проще. Мне приходится иметь дело с неполными строками, что делает его довольно сложным.

Обновление Из цифр видно, что простой string.split уже стоит слишком дорого. Но StringReader также стоит довольно дорого. Как будет выглядеть высоко оптимизированное решение, приближающееся к реальной скорости диска? Я пробовал много способов с небезопасными кодами и буферами символов, но прирост производительности был, возможно, на 30%, но ничего такого, что мне понадобилось бы. Я был бы в порядке со скоростью разбора 100 МБ / с. Это должно быть достижимо с помощью управляемого кода или я ошибаюсь?

Разве с C # невозможно проанализировать быстрее, чем я могу прочитать с моего жесткого диска? Это Intel Postville X25M. Процессор есть и старше Intel Dual Core. У меня 3 ГБ оперативной памяти Windows 7 .NET 3.5 SP1 и .NET 4.

Но я видел те же результаты и на обычных жестких дисках. Линейная скорость чтения может достигать 400 МБ / с на современных жестких дисках. Означает ли это, что я должен реструктурировать свои приложения так, чтобы они считывали данные по требованию, когда они действительно необходимы, вместо того, чтобы активно читать их в память за счет более высоких времен GC из-за увеличенного графа объектов, который делает циклы GC намного более длинными.

Я заметил , что если мое управляемое приложение использует более 500 МБ памяти, оно становится намного менее отзывчивым. Основной способствующий фактор кажется сложностью графа объекта. Поэтому может быть лучше прочитать данные, когда это необходимо. По крайней мере, это мой вывод из текущих данных.

Вот код

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
using System.ComponentModel;

namespace IOBound
{
    class Program
    {
        static void Main(string[] args)
        {
            string data = @"C:\Source\IOBound\NumericData.txt";
            if (!File.Exists(data))
            {
                CreateTestData(data);
            }

            int MB = (int) (new FileInfo(data).Length/(1024*1024));

            var sw = Stopwatch.StartNew();
            uint bytes = ReadFileIntoByteBuffer(data);
            sw.Stop();
            Console.WriteLine("Did native read {0:F1}MB in {1:F1}s, {2:F1}MB/s",
                bytes/(1024*1024), sw.Elapsed.TotalSeconds, MB / sw.Elapsed.TotalSeconds);

            sw = Stopwatch.StartNew();
            int n = CountLines(data);
            sw.Stop();
            Console.WriteLine("Did read {0:N0} lines in {1:F1}s, {2:F1}MB/s",
                n, sw.Elapsed.TotalSeconds, MB / sw.Elapsed.TotalSeconds);

            sw = Stopwatch.StartNew();
            ParseLinesUnsafe(data);
            sw.Stop();
            Console.WriteLine("Did parse and read unsafe {0:F1}MB in {1:F1}s, {2:F1}MB/s",
                MB, sw.Elapsed.TotalSeconds, MB / sw.Elapsed.TotalSeconds);

            sw = Stopwatch.StartNew();
            ParseLinesUnsafeCharBuf(data);
            sw.Stop();
            Console.WriteLine("Did parse and read unsafe char buf {0:F1}MB in {1:F1}s, {2:F1}MB/s",
                MB, sw.Elapsed.TotalSeconds, MB / sw.Elapsed.TotalSeconds);

            sw = Stopwatch.StartNew();
            ParseLines(data);
            sw.Stop();
            Console.WriteLine("Did read and parse {0:F1}MB in {1:F1}s, {2:F1}MB/s",
                MB, sw.Elapsed.TotalSeconds, MB / sw.Elapsed.TotalSeconds);

        }

        private unsafe static uint ReadFileIntoByteBuffer(string data)
        {
            using(var stream = new FileStream(data, FileMode.Open))
            {
                byte[] buf = new byte[200 * 1024 * 1024];
                fixed(byte* pBuf = &buf[0])
                {
                    uint dwRead = 0;
                    if (ReadFile(stream.SafeFileHandle, pBuf, 200 * 1000 * 1000, out dwRead, IntPtr.Zero) == 0)
                    {
                        throw new Win32Exception();
                    }
                    return dwRead;
                }

            }
        }

        private static int CountLines(string data)
        {
            using (var reader = new StreamReader(data))
            {
                string line;
                int count = 0;
                while ((line = reader.ReadLine()) != null)
                {
                    count++;
                }

                return count;
            }
        }

        unsafe private static void ParseLinesUnsafeCharBuf(string data)
        {
            var dobules = new List<double>();
            var ints = new List<int>();

            using (var reader = new StreamReader(data))
            {
                double d = 0;
                long a = 0, b = 0;
                int i = 0;
                char[] buffer = new char[10*1000*1000];
                int readChars = 0;
                int startIdx = 0;

                fixed(char *ln = buffer)
                {
                    while ((readChars = reader.Read(buffer, startIdx, buffer.Length - startIdx)) != 0)
                    {
                        char* pEnd = ln + readChars + startIdx;
                        char* pCur = ln;
                        char* pLineStart = null;

                        while (pCur != pEnd)
                        {
                            a = 0;
                            b = 0;

                            while (pCur != pEnd && *pCur == '\r' || *pCur == '\n')
                            {
                                pCur++;
                            }
                            pLineStart = pCur;

                            while(pCur != pEnd && char.IsNumber(*pCur))
                            {
                                a = a * 10 + (*pCur++ - '0');
                            }
                            if (pCur == pEnd || *pCur == '\r')
                            {
                                goto incompleteLine;
                            }

                            if (*pCur++ == ',')
                            {
                                long div = 1;
                                while (pCur != pEnd && char.IsNumber(*pCur))
                                {
                                    b += b * 10 + (*pCur++ - '0');
                                    div *= 10;
                                }
                                if (pCur == pEnd || *pCur == '\r')
                                {
                                    goto incompleteLine;
                                }
                                d = a + ((double)b) / div;
                            }
                            else
                            {
                                goto skipRest;
                            }

                            while (pCur != pEnd && char.IsWhiteSpace(*pCur))
                            {
                                pCur++;
                            }
                            if (pCur == pEnd || *pCur == '\r')
                            {
                                goto incompleteLine;
                            }

                            i = 0;
                            while (pCur != pEnd && char.IsNumber(*pCur))
                            {
                                i = i * 10 + (*pCur++ - '0');
                            }
                            if (pCur == pEnd)
                            {
                                goto incompleteLine;
                            }

                            dobules.Add(d);
                            ints.Add(i);

                            continue;

incompleteLine:
                            startIdx = (int)(pEnd - pLineStart);
                            Buffer.BlockCopy(buffer, (int)(pLineStart - ln) * 2, buffer, 0, 2 * startIdx);
                            break;
skipRest:
                            while (pCur != pEnd && *pCur != '\r')
                            {
                                pCur++;   
                            }
                            continue;
                        }
                    }
                }
            }
        }

        unsafe private static void ParseLinesUnsafe(string data)
        {
            var dobules = new List<double>();
            var ints = new List<int>();

            using (var reader = new StreamReader(data))
            {
                string line;
                double d=0;
                long a = 0, b = 0;
                int ix = 0;
                while ((line = reader.ReadLine()) != null)
                {
                    int len = line.Length;
                    fixed (char* ln = line)
                    {
                        while (ix < len && char.IsNumber(ln[ix]))
                        { 
                            a = a * 10 + (ln[ix++] - '0');
                        }

                        if (ln[ix] == ',')
                        {
                            ix++;
                            long div = 1;
                            while (ix < len && char.IsNumber(ln[ix]))
                            {
                                b += b * 10 + (ln[ix++] - '0');
                                div *= 10;
                            }
                            d = a + ((double)b) / div;
                        }

                        while (ix < len && char.IsWhiteSpace(ln[ix]))
                        {
                            ix++;
                        }

                        int i = 0;
                        while (ix < len && char.IsNumber(ln[ix]))
                        { 
                            i = i * 10 + (ln[ix++] - '0');
                        }

                        dobules.Add(d);
                        ints.Add(ix);
                    }
                }
            }
        }



        private static void ParseLines(string data)
        {
            var dobules = new List<double>();
            var ints = new List<int>();

            using (var reader = new StreamReader(data))
            {
                string line;
                char[] sep  = new char[] { ' ' };
                while ((line = reader.ReadLine()) != null)
                {
                    var parts = line.Split(sep);
                    if (parts.Length == 2)
                    {
                        dobules.Add( double.Parse(parts[0]));
                        ints.Add( int.Parse(parts[1]));
                    }
                }
            }
        }

        static void CreateTestData(string fileName)
        {
            FileStream fstream = new FileStream(fileName, FileMode.Create);
            using (StreamWriter writer = new StreamWriter(fstream, Encoding.UTF8))
            {
                for (int i = 0; i < 10 * 1000 * 1000; i++)
                {
                    writer.WriteLine("{0} {1}", 1.1d + i, i);
                }
            }
        }

        [DllImport("kernel32.dll", SetLastError = true)]
        unsafe static extern uint ReadFile(SafeFileHandle hFile, [Out] byte* lpBuffer, uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, IntPtr lpOverlapped);

    }
}

Ответы [ 7 ]

4 голосов
/ 23 августа 2011

Итак, здесь есть пара проблем.Другие уже прокомментировали кеширование ввода-вывода в Windows, а также фактический аппаратный кеш, поэтому я оставлю это в покое.

Другая проблема заключается в том, что вы измеряете комбинированные операции read () + parse ()и сравнивая это со скоростью только что прочитанного ().По сути, вам нужно осознать тот факт, что A + B всегда будет больше, чем A (при условии, что он не отрицательный).

Поэтому, чтобы выяснить, связаны ли вы с IO, вам необходимо выяснить, сколько времени это займет.прочитать файл.Вы сделали это.На моей машине ваш тест выполняется примерно на 220 мс для чтения файла.

Теперь вам нужно измерить, сколько нужно времени, чтобы проанализировать столько разных строк.Это немного сложнее изолировать.Итак, давайте просто скажем, что мы оставляем их вместе и вычитаем время, необходимое для чтения из времени разбора.Кроме того, мы не пытаемся измерить, что вы делаете с данными, а просто анализируете, поэтому выкиньте List и List и давайте просто проанализируем.Запуск этого на моей машине дает около 1000 мс, меньше 220 мс для чтения, ваш код синтаксического анализа занимает около 780 мс на 1 млн строк.

Так почему же он такой медленный (в 3-4 раза медленнее, чем чтение)?Опять давайте уберем некоторые вещи.Комментируем int.Parse и double.Parse и запускаем снова.Это намного лучше, 460 мс меньше, чем время чтения 220, теперь у нас 240 мсек для анализа.Конечно, parse вызывает только string.Split ().Hrmmm выглядит как string.Split будет стоить вам столько же, сколько и дисковый ввод-вывод, что неудивительно, если учесть, как .NET работает со строками.

Так может ли C # анализировать так быстро или быстрее, чем чтение с диска?Ну, да, может, но вам придется стать противным.Вы видите int.Parse и double.Parse страдают от того, что они осведомлены о культуре.Из-за этого и того факта, что эти процедуры синтаксического анализа имеют дело со многими форматами, они несколько дорогостоящие в зависимости от вашего примера.Я имею в виду, что мы анализируем double и int каждую микросекунду (миллионную долю секунды), что обычно неплохо.

Таким образом, чтобы соответствовать скорости чтения с диска (и, следовательно, быть привязанным к вводу-выводу), нам нужно переписать способ обработки текстовой строки.Вот неприятный пример, но он работает для вашего примера ...

int len = line.Length;
fixed (char* ln = line)
{
    double d;
    long a = 0, b = 0;
    int ix = 0;
    while (ix < len && char.IsNumber(ln[ix]))
        a = a * 10 + (ln[ix++] - '0');
    if (ln[ix] == '.')
    {
        ix++;
        long div = 1;
        while (ix < len && char.IsNumber(ln[ix]))
        {
            b += b * 10 + (ln[ix++] - '0');
            div *= 10;
        }
        d = a + ((double)b)/div;
    }

    while (ix < len && char.IsWhiteSpace(ln[ix]))
        ix++;

    int i = 0;
    while (ix < len && char.IsNumber(ln[ix]))
        i = i * 10 + (ln[ix++] - '0');
}

Запуск этого дерьмового кода дает время выполнения около 450 мс, или примерно 2n времени чтения.Итак, притворившись на мгновение, что вы думаете, что приведенный выше фрагмент кода является приемлемым (что, боже, я надеюсь, вы не делаете), вы можете иметь один поток, читающий строки, а другой - синтаксический анализ, и вы будете близки к ограничению ввода-вывода.Поместите два потока на разборе, и вы будете связаны IO. Если вы сделаете это, это еще один вопрос.

Итак, давайте вернемся к вашему первоначальному вопросу:

Известно, что если вы читаетеданные с диска связаны с вводом-выводом, и вы можете обрабатывать / анализировать прочитанные данные гораздо быстрее, чем вы можете прочитать их с диска.

Но это обычная мудрость (миф?)

Ну нет, я бы не назвал это мифом.На самом деле, я бы поспорил, что ваш оригинальный код все еще является IO Bound.Вы проводите свой тест изолированно, поэтому воздействие будет небольшим в 1/6 от времени, затраченного на чтение с устройства.Но подумайте, что произойдет, если этот диск занят?Что если ваш антивирусный сканер просматривает каждый файл?Проще говоря, ваша программа будет замедляться из-за повышенной активности жесткого диска, и она может стать IO Bound.

ИМХО, причина этой "общей мудрости" заключается в следующем:

Легче связать ввод-вывод при записи, чем при чтении.

Запись на устройство занимает больше времени и, как правило, обходится дороже, чем получение данных. Если вы хотите увидеть IO Bound в действии, посмотрите на ваш метод «CreateTestData». Ваш метод CreateTestData занимает в 2 раза больше времени для записи данных на диск, чем просто вызов String.Format (...). И это при полном кешировании. Отключите кэширование ( FileOptions.WriteThrough ) и попробуйте снова ... теперь CreateTestData работает в 3–4 раза медленнее. Попробуйте сами с помощью следующих методов:

static int CreateTestData(string fileName)
{
    FileStream fstream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough);
    using (StreamWriter writer = new StreamWriter(fstream, Encoding.UTF8))
    {
        for (int i = 0; i < linecount; i++)
        {
            writer.WriteLine("{0} {1}", 1.1d + i, i);
        }
    }
    return linecount;
}
static int PrintTestData(string fileName)
{
    for (int i = 0; i < linecount; i++)
    {
        String.Format("{0} {1}", 1.1d + i, i);
    }
    return linecount;
}

Это только для начала, если вы действительно хотите связать ввод-вывод, начните использовать прямой ввод-вывод. См. Документацию по CreateFile с использованием FILE_FLAG_NO_BUFFERING. Запись становится намного медленнее, когда вы начинаете обходить аппаратные кэши и ждете завершения ввода-вывода. Это одна из основных причин, по которой традиционная база данных очень медленно записывается. Они должны заставить аппаратное обеспечение завершить запись и ждать его. Только тогда они могут назвать транзакцию «совершенной», данные находятся в файле на физическом устройстве.

ОБНОВЛЕНО

Хорошо, Алоис, кажется, ты просто ищешь, как быстро ты можешь идти. Чтобы двигаться быстрее, вам нужно перестать иметь дело со строками и символами и убрать выделение, чтобы идти быстрее. Следующий код улучшает синтаксический анализатор строк / символов, описанный выше, примерно на порядок (прибавляя около 30 мс по сравнению с простым подсчетом строк), выделяя при этом только один буфер в куче.

ПРЕДУПРЕЖДЕНИЕ Вы должны понимать, что я демонстрирую, что можно сделать быстро. Я не , советую вам пойти по этому пути. Этот код имеет некоторые серьезные ограничения и / или ошибки. Например, что происходит, когда вы ударяете дубль в форме «1.2589E + 19»? Честно говоря, я думаю, что вы должны придерживаться своего исходного кода и не беспокоиться о том, чтобы пытаться оптимизировать его так сильно Либо так, либо измените формат файла на двоичный вместо текстового (см. BinaryWriter ). Если вы используете двоичный файл, вы можете использовать вариант следующего кода с BitConvert.ToDouble / ToInt32 , и это будет еще быстрее.

private static unsafe int ParseFast(string data)
{
    int count = 0, valid = 0, pos, stop, temp;
    byte[] buffer = new byte[ushort.MaxValue];

    const byte Zero = (byte) '0';
    const byte Nine = (byte) '9';
    const byte Dot = (byte)'.';
    const byte Space = (byte)' ';
    const byte Tab = (byte) '\t';
    const byte Line = (byte) '\n';

    fixed (byte *ptr = buffer)
    using (Stream reader = File.OpenRead(data))
    {
        while (0 != (temp = reader.Read(buffer, valid, buffer.Length - valid)))
        {
            valid += temp;
            pos = 0;
            stop = Math.Min(buffer.Length - 1024, valid);
            while (pos < stop)
            {
                double d;
                long a = 0, b = 0;
                while (pos < valid && ptr[pos] >= Zero && ptr[pos] <= Nine)
                    a = a*10 + (ptr[pos++] - Zero);
                if (ptr[pos] == Dot)
                {
                    pos++;
                    long div = 1;
                    while (pos < valid && ptr[pos] >= Zero && ptr[pos] <= Nine)
                    {
                        b += b*10 + (ptr[pos++] - Zero);
                        div *= 10;
                    }
                    d = a + ((double) b)/div;
                }
                else
                    d = a;

                while (pos < valid && (ptr[pos] == Space || ptr[pos] == Tab))
                    pos++;

                int i = 0;
                while (pos < valid && ptr[pos] >= Zero && ptr[pos] <= Nine)
                    i = i*10 + (ptr[pos++] - Zero);

                DoSomething(d, i);

                while (pos < stop && ptr[pos] != Line)
                    pos++;
                while (pos < stop && !(ptr[pos] >= Zero && ptr[pos] <= Nine))
                    pos++;
            }

            if (pos < valid)
                Buffer.BlockCopy(buffer, pos, buffer, 0, valid - pos);
            valid -= pos;
        }
    }
    return count;
}
1 голос
/ 23 августа 2011

Бросив на него профилировщик, похоже, что большую часть времени действительно тратится на разбор файла.

Я попытался передать ваш пример в MemoryStream с уже прочитанным byte[] вместо пути к методу ParseLines, и разница между анализом по пути к файлу и анализом из байтов памяти была незначительной.

Другими словами, обработка выполняется, а не чтение занимает значительное время.

Я бросил этот профилировщик на это: http://code.google.com/p/slimtune/

И оттуда я вижу, что метод ParseLines при вызове в потоке памяти имел следующие временные характеристики:

25,34% в System.Io.StreamReader.ReadLine()
23,86% в System.Double.Parse
21,72% в System.Number.ParseInt32
20,91% в System.String.Split
- Некоторые другие не очень важные методы -

Итак, это говорит мне о том, что даже чтение строк из потока памяти несколько медленное, как и большинство строковых операций.

1 голос
/ 23 августа 2011

Существует только одна «общая мудрость» в отношении производительности - решите, насколько быстрым должен быть процесс, и оцените все, действуйте на собранных данных и повторите.

На данный момент ваш код, вероятно, делает 10-кратное выделение памяти для каждого прочитанного байта (имеется много уровней считывателей, считывателей строк и т. П.). Если вам нужна абсолютная скорость, вам, вероятно, потребуется устранить большинство перераспределений. Сначала я переписал бы код, чтобы полностью использовать один считыватель, и измерил, если производительность достаточно хорошая.

1 голос
/ 23 августа 2011

Если вы работаете с большими файлами, быстрее всего вы получите AFAIK, класс MemoryMappedFile (новый в .NET 4).

Таким образом вы в основном используете кеш-менеджер OS FileSystem.непосредственно на страницы памяти ... если это не достаточно быстро, вам нужно написать свою собственную файловую систему ...

Что касается GC, поведение может быть настроено через app.config - например:

<runtime>
   <gcServer enabled="true" />
</runtime>

Параметры настройки ГХ см. http://msdn.microsoft.com/en-us/library/6bs4szyc.aspx - esp.<gcConcurrent> / <gcServer>.

1 голос
/ 23 августа 2011

Всего пара предложений (если вы хотите жить с более сложной реализацией) ...

  • Вы можете использовать LinkedList вместо List, чтобы избежать перераспределения / копирования приAdd -ing.
  • Заменить Split рукописным кодом, который ищет разделитель и затем использует Substring для извлечения «полей».Вам нужно только найти один разделитель, и количество полей известно заранее, поэтому Split является слишком общим для вас.
  • Используйте Read вместо ReadLine, чтобы вы могли повторно использоватьтот же буфер и избегайте выделения новой строки для каждой строки.
  • Если вы действительно добросовестны в производительности, разделите анализ на одновременные Task с.Пока вы занимаетесь этим, поместите чтение файла в свою собственную задачу.
1 голос
/ 23 августа 2011

Как уже упоминалось в других ответах / комментариях, вы, вероятно, все равно читаете файл из кэша, поэтому скорость диска / SDRAM не является ограничивающим фактором.

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

Вы можете анализировать быстрее, чем вы можете читать с диска, но анализаторы оптимизированы для скоростиотличаются осторожностью при использовании памяти.

0 голосов
/ 23 августа 2011

Я подозреваю, что кеширование искажает измерения, которые вы делаете. Жесткий диск имеет оперативную память, которую он использует для кэширования недавно использованных данных, а также операционная система.

Для лучшего теста вам нужно будет перезагрузиться между тестами. Я предлагаю полностью отключить питание, чтобы гарантировать, что RAM диска также очищена.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...