Чтение больших текстовых файлов с потоками на C #

96

У меня есть прекрасная задача разработать, как обрабатывать большие файлы, загружаемые в редактор сценариев нашего приложения (это похоже на VBA для нашего внутреннего продукта для быстрых макросов). Большинство файлов имеют размер около 300-400 КБ, что нормально загружается. Но когда они превышают 100 МБ, у процесса возникают проблемы (как и следовало ожидать).

Что происходит, так это то, что файл читается и помещается в RichTextBox, по которому затем выполняется навигация - не беспокойтесь об этой части.

Разработчик, написавший исходный код, просто использует StreamReader и выполняет

[Reader].ReadToEnd()

что может занять некоторое время.

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

Некоторые предположения:

  • Большинство файлов будут 30-40 МБ
  • Содержимое файла - текстовое (не двоичное), некоторые - формат Unix, некоторые - DOS.
  • Как только содержимое получено, мы выясняем, какой терминатор используется.
  • Когда он загружен, никого не волнует время, необходимое для рендеринга в richtextbox. Это всего лишь начальная загрузка текста.

Теперь вопросы:

  • Могу ли я просто использовать StreamReader, затем проверить свойство Length (так ProgressMax) и выполнить Read для заданного размера буфера и выполнить итерацию в цикле while WHILST внутри фонового рабочего, чтобы он не блокировал основной поток пользовательского интерфейса? Затем верните конструктор строк в основной поток после его завершения.
  • Содержимое будет отправлено в StringBuilder. могу ли я инициализировать StringBuilder размером потока, если длина доступна?

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

Николь Ли
источник
29
Файлы сценария 30-40 МБ? Святая скумбрия! Я бы не хотел, чтобы код ревью был ...
dthorpe 09
Я знаю, что этот вопрос довольно старый, но я нашел его на днях и протестировал рекомендацию для MemoryMappedFile, и это, безусловно, самый быстрый метод. Для сравнения: чтение 7616939 строчного файла размером 345 МБ с помощью метода readline занимает на моем компьютере более 12 часов, в то время как выполнение той же загрузки и чтение через MemoryMappedFile заняло 3 секунды.
csonon
Это всего лишь несколько строк кода. См. Эту библиотеку, которую я использую для чтения файлов размером 25 ГБ и более. github.com/Agenty/FileReader
Викаш Рати

Ответы:

175

Вы можете улучшить скорость чтения, используя BufferedStream, например:

using (FileStream fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (BufferedStream bs = new BufferedStream(fs))
using (StreamReader sr = new StreamReader(bs))
{
    string line;
    while ((line = sr.ReadLine()) != null)
    {

    }
}

ОБНОВЛЕНИЕ за март 2013 г.

Недавно я написал код для чтения и обработки (поиска текста в) текстовых файлов размером 1 ГБ (намного больше, чем файлы, используемые здесь) и добился значительного прироста производительности за счет использования шаблона производитель / потребитель. Задача производителя считывает строки текста с помощью BufferedStreamи передает их отдельной задаче-потребителю, которая выполняет поиск.

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

Почему BufferedStream быстрее

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

ОБНОВЛЕНИЕ ДЕКАБРЯ 2014 ГОДА: Ваш пробег может отличаться

Судя по комментариям, FileStream должен использовать BufferedStream внутри. Когда этот ответ был впервые предоставлен, я измерил значительный прирост производительности, добавив BufferedStream. В то время я ориентировался на .NET 3.x на 32-битной платформе. Сегодня, ориентируясь на .NET 4.5 на 64-битной платформе, я не вижу никаких улучшений.

Связанный

Я столкнулся со случаем, когда потоковая передача большого сгенерированного файла CSV в поток ответов из действия ASP.Net MVC была очень медленной. Добавление BufferedStream в этом случае повысило производительность в 100 раз. Подробнее см. Очень медленный небуферизованный вывод.

Эрик Дж.
источник
12
Чувак, BufferedStream имеет значение. +1 :)
Маркус
2
За запрос данных из подсистемы ввода-вывода взимается плата. В случае вращающихся дисков вам, возможно, придется подождать, пока пластина вернется в положение для чтения следующего блока данных, или, что еще хуже, подождать, пока головка диска переместится. Хотя у SSD нет механических частей, замедляющих работу, доступ к ним по-прежнему взимается за операцию ввода-вывода. Буферизованные потоки читают больше, чем просто запросы StreamReader, уменьшая количество обращений к ОС и, в конечном итоге, количество отдельных запросов ввода-вывода.
Eric J.
4
В самом деле? В моем тестовом сценарии это не имеет значения. По словам Брэда Абрамса, использование BufferedStream вместо FileStream бесполезно.
Nick Cox
2
@NickCox: ваши результаты могут отличаться в зависимости от вашей базовой подсистемы ввода-вывода. На вращающемся диске и контроллере диска, у которого нет данных в кэше (а также данных, не кэшированных Windows), ускорение огромно. Колонка Брэда была написана в 2004 году. Недавно я измерил реальные, радикальные улучшения.
Эрик Дж.
3
Это бесполезно согласно: stackoverflow.com/questions/492283/… FileStream уже использует буфер внутри.
Эрвин Майер
21

Если вы прочитаете статистику производительности и тестов на этом веб-сайте , вы увидите, что самый быстрый способ чтения (поскольку чтение, запись и обработка - все разные) текстовый файл - это следующий фрагмент кода:

using (StreamReader sr = File.OpenText(fileName))
{
    string s = String.Empty;
    while ((s = sr.ReadLine()) != null)
    {
        //do your stuff here
    }
}

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


источник
2
Это хорошо сработало для разделения файла postgres размером 19 ГБ для преобразования его в синтаксис sql в нескольких файлах. Спасибо парню postgres, который никогда не выполнял мои параметры правильно. / вздох
Дэймон Дрейк
Разница в производительности здесь, кажется, окупается для действительно больших файлов, например, размером более 150 МБ (также вам действительно следует использовать a StringBuilderдля загрузки их в память, загружается быстрее, поскольку он не создает новую строку каждый раз, когда вы добавляете символы)
Джошуа Г.
15

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

Если последнее верно, то решение становится намного проще. Просто сделайте reader.ReadToEnd()это в фоновом потоке и отобразите индикатор выполнения в виде маркера вместо правильного.

Я поднимаю этот вопрос, потому что, по моему опыту, это часто бывает. Когда вы пишете программу обработки данных, пользователей определенно заинтересует цифра в% полной, но для простых, но медленных обновлений пользовательского интерфейса они, скорее всего, просто захотят знать, что компьютер не разбился. :-)

Кристиан Хейтер
источник
2
Но может ли пользователь отменить вызов ReadToEnd?
Тим Скарборо
@ Тим, хорошо подмечено. В этом случае мы вернулись к StreamReaderциклу. Тем не менее, это все равно будет проще, потому что нет необходимости заранее читать, чтобы рассчитать индикатор прогресса.
Christian Hayter
8

Для двоичных файлов самый быстрый способ их чтения, который я нашел, - это.

 MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(file);
 MemoryMappedViewStream mms = mmf.CreateViewStream();
 using (BinaryReader b = new BinaryReader(mms))
 {
 }

В моих тестах это в сотни раз быстрее.

Нержавеющая сталь
источник
2
У вас есть веские доказательства этого? Почему OP должен использовать это вместо любого другого ответа? Пожалуйста, копните немного глубже и
расскажите
7

Используйте фоновый воркер и прочтите только ограниченное количество строк. Читать дальше, только когда пользователь прокручивает.

И постарайтесь никогда не использовать ReadToEnd (). Это одна из функций, о которых вы думаете «зачем они это сделали?»; это помощник для детей-скриптов, который отлично справляется с мелочами, но, как видите, с большими файлами он отстой ...

Тем, кто говорит вам использовать StringBuilder, нужно чаще читать MSDN:

Вопросы
производительности Оба метода Concat и AppendFormat объединяют новые данные с существующим объектом String или StringBuilder. Операция конкатенации объектов String всегда создает новый объект из существующей строки и новых данных. Объект StringBuilder поддерживает буфер для объединения новых данных. Новые данные добавляются в конец буфера, если место доступно; в противном случае выделяется новый буфер большего размера, данные из исходного буфера копируются в новый буфер, а затем новые данные добавляются в новый буфер. Производительность операции конкатенации для объекта String или StringBuilder зависит от того, как часто происходит выделение памяти.
Операция конкатенации String всегда выделяет память, тогда как операция конкатенации StringBuilder выделяет память только в том случае, если буфер объекта StringBuilder слишком мал для размещения новых данных. Следовательно, класс String предпочтительнее для операции конкатенации, если конкатенация фиксированного числа объектов String. В этом случае отдельные операции конкатенации могут даже быть объединены компилятором в одну операцию. Объект StringBuilder предпочтительнее для операции конкатенации, если конкатенация произвольного числа строк; например, если цикл объединяет случайное количество строк пользовательского ввода.

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

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

Туфо
источник
далеко вы, ребята, супер быстрые! К сожалению, из-за особенностей работы макроса необходимо загружать весь поток. Как я уже упоминал, не беспокойтесь о части с расширенным текстом. Это начальная загрузка, которую мы хотим улучшить.
Николь Ли
так что вы можете работать по частям, читать первые X строк, применять макрос, читать вторые X строки, применять макрос и так далее ... если вы объясните, что делает этот макрос, мы можем помочь вам с большей точностью
Туфо
5

Этого должно быть достаточно, чтобы вы начали.

class Program
{        
    static void Main(String[] args)
    {
        const int bufferSize = 1024;

        var sb = new StringBuilder();
        var buffer = new Char[bufferSize];
        var length = 0L;
        var totalRead = 0L;
        var count = bufferSize; 

        using (var sr = new StreamReader(@"C:\Temp\file.txt"))
        {
            length = sr.BaseStream.Length;               
            while (count > 0)
            {                    
                count = sr.Read(buffer, 0, bufferSize);
                sb.Append(buffer, 0, count);
                totalRead += count;
            }                
        }

        Console.ReadKey();
    }
}
ХаосПандион
источник
4
Я бы вынес из цикла «var buffer = new char [1024]»: нет необходимости каждый раз создавать новый буфер. Просто поставьте его перед «while (count> 0)».
Tommy Carlier
4

Взгляните на следующий фрагмент кода. Вы упомянули Most files will be 30-40 MB. Это утверждает, что чтение 180 МБ за 1,4 секунды на Intel Quad Core:

private int _bufferSize = 16384;

private void ReadFile(string filename)
{
    StringBuilder stringBuilder = new StringBuilder();
    FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read);

    using (StreamReader streamReader = new StreamReader(fileStream))
    {
        char[] fileContents = new char[_bufferSize];
        int charsRead = streamReader.Read(fileContents, 0, _bufferSize);

        // Can't do much with 0 bytes
        if (charsRead == 0)
            throw new Exception("File is 0 bytes");

        while (charsRead > 0)
        {
            stringBuilder.Append(fileContents);
            charsRead = streamReader.Read(fileContents, 0, _bufferSize);
        }
    }
}

Оригинальная статья

Джеймс
источник
3
Такие тесты заведомо ненадежны. Вы будете читать данные из кеша файловой системы при повторении теста. Это как минимум на порядок быстрее, чем в реальном тесте, считывающем данные с диска. Файл размером 180 МБ не может занять менее 3 секунд. Перезагрузите компьютер, запустите тест один раз для реального числа.
Hans Passant
7
строка stringBuilder.Append потенциально опасна, вам нужно заменить ее на stringBuilder.Append (fileContents, 0, charsRead); чтобы гарантировать, что вы не добавляете полные 1024 символа, даже если поток закончился раньше.
Йоханнес Рудольф
@JohannesRudolph, ваш комментарий только что решил ошибку. Как вы пришли к числу 1024?
OfirD
3

Возможно, вам лучше использовать здесь обработку файлов с отображением в память. Поддержка файлов с отображением в память будет присутствовать в .NET 4 (я думаю ... я слышал это от кого-то другого, говорящего об этом), следовательно, эта оболочка, которая использует p / вызывает то же самое.

Изменить: см. Здесь, в MSDN, как это работает, здесь запись в блоге, показывающая, как это делается в предстоящем .NET 4, когда он выйдет в виде релиза. Ссылка, которую я дал ранее, представляет собой оболочку вокруг pinvoke для достижения этой цели. Вы можете отобразить весь файл в память и просматривать его как скользящее окно при прокрутке файла.

t0mm13b
источник
3

Всем отличных ответов! однако для тех, кто ищет ответ, они кажутся неполными.

Поскольку стандартная строка может иметь только размер X, от 2 ГБ до 4 ГБ в зависимости от вашей конфигурации, эти ответы на самом деле не соответствуют вопросу OP. Один из способов - работать со списком строк:

List<string> Words = new List<string>();

using (StreamReader sr = new StreamReader(@"C:\Temp\file.txt"))
{

string line = string.Empty;

while ((line = sr.ReadLine()) != null)
{
    Words.Add(line);
}
}

Некоторые могут захотеть токенизировать и разделить строку при обработке. Список строк теперь может содержать очень большие объемы текста.

ржавый гвоздь
источник
1

Итератор может быть идеальным для такого типа работы:

public static IEnumerable<int> LoadFileWithProgress(string filename, StringBuilder stringData)
{
    const int charBufferSize = 4096;
    using (FileStream fs = File.OpenRead(filename))
    {
        using (BinaryReader br = new BinaryReader(fs))
        {
            long length = fs.Length;
            int numberOfChunks = Convert.ToInt32((length / charBufferSize)) + 1;
            double iter = 100 / Convert.ToDouble(numberOfChunks);
            double currentIter = 0;
            yield return Convert.ToInt32(currentIter);
            while (true)
            {
                char[] buffer = br.ReadChars(charBufferSize);
                if (buffer.Length == 0) break;
                stringData.Append(buffer);
                currentIter += iter;
                yield return Convert.ToInt32(currentIter);
            }
        }
    }
}

Вы можете вызвать это, используя следующее:

string filename = "C:\\myfile.txt";
StringBuilder sb = new StringBuilder();
foreach (int progress in LoadFileWithProgress(filename, sb))
{
    // Update your progress counter here!
}
string fileData = sb.ToString();

По мере загрузки файла итератор будет возвращать номер выполнения от 0 до 100, который вы можете использовать для обновления индикатора выполнения. После завершения цикла StringBuilder будет содержать содержимое текстового файла.

Кроме того, поскольку вам нужен текст, мы можем просто использовать BinaryReader для чтения символов, что обеспечит правильное выравнивание ваших буферов при чтении любых многобайтовых символов ( UTF-8 , UTF-16 и т. Д.).

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

Крайности
источник