Pytanie Czytanie dużych plików tekstowych strumieniem w języku C #


Mam piękne zadanie opracowania sposobu obsługi dużych plików ładowanych do edytora skryptów aplikacji (to jest tak VBA dla naszego wewnętrznego produktu dla szybkich makr). Większość plików ma około 300-400 KB, co jest dobrym ładowaniem. Ale kiedy przekroczą 100 MB, proces ten ma ciężki czas (jak można się spodziewać).

Co się dzieje, plik jest odczytywany i wrzucany do RichTextBox, który jest następnie nawigowany - nie przejmuj się zbytnio tą częścią.

Programista, który napisał początkowy kod, po prostu używa StreamReadera i robi

[Reader].ReadToEnd()

co może trochę potrwać.

Moim zadaniem jest złamanie tego fragmentu kodu, odczytanie go w porcjach do bufora i wyświetlenie paska postępu z opcją jego anulowania.

Niektóre założenia:

  • Większość plików ma rozmiar 30-40 MB
  • Zawartość pliku to tekst (nie binarny), niektóre są w formacie uniksowym, inne to DOS.
  • Po pobraniu zawartości sprawdzamy, z jakiego terminatora korzysta.
  • Nikt nie jest zaniepokojony, gdy załadowany jest czas renderowania w richtextbox. To tylko początkowe obciążenie tekstu.

Teraz pytania:

  • Czy mogę po prostu użyć StreamReader, a następnie sprawdzić właściwość Length (tak, aby ProgressMax) i wydać Read dla ustawionego rozmiaru bufora i iterować w pętli while PODCZAS wewnątrz obiektu działającego w tle, więc nie blokuje głównego wątku interfejsu użytkownika? Następnie zwróć konstruktor łańcuchów do głównego wątku po jego zakończeniu.
  • Treść trafi do StringBuilder. czy mogę zainicjować StringBuilder wielkością strumienia, jeśli długość jest dostępna?

Czy są to (w twojej opinii zawodowej) dobre pomysły? W przeszłości miałem kilka problemów z czytaniem treści ze strumieni, ponieważ zawsze będzie brakować ostatnich bajtów lub czegoś, ale zadam inne pytanie, czy tak jest.


76
2018-01-29 12:36


pochodzenie


Pliki skryptowe o wielkości 30-40 MB? Święta makrela! Nie chciałbym mieć przeglądu kodu, który ... - dthorpe
To tylko kilka linii kodu. Zobacz tę bibliotekę używam do odczytu 25 GB i więcej dużych plików, jak również. github.com/Agenty/FileReader - Vicky


Odpowiedzi:


Możesz zwiększyć prędkość odczytu, używając BufferedStream, jak poniżej:

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)
    {

    }
}

Marzec 2013 UPDATE

Niedawno napisałem kod do czytania i przetwarzania (przeszukiwania tekstu) 1-gigabitowych plików tekstowych (znacznie większych niż pliki tutaj zaangażowane) i osiągnięto znaczny wzrost wydajności dzięki zastosowaniu wzorca producent / konsument. Zadanie producenta zostało przeczytane w liniach tekstu za pomocą BufferedStream i przekazał je do oddzielnego zadania konsumenckiego, które przeszukało.

Użyłem tego jako okazji do nauki TPL Dataflow, która bardzo dobrze nadaje się do szybkiego kodowania tego wzorca.

Dlaczego BufferedStream jest szybszy

Bufor to blok pamięci w pamięci wykorzystywanej do buforowania danych, zmniejszając w ten sposób liczbę wywołań do systemu operacyjnego. Bufory poprawiają wydajność odczytu i zapisu. Bufor może być użyty do odczytu lub zapisu, ale nigdy do obu jednocześnie. Metody odczytu i zapisu w BufferedStream automatycznie utrzymują bufor.

Grudzień 2014 AKTUALIZACJA: Twój przebieg może się różnić

Na podstawie komentarzy FileStream powinien używać a BufferedStream wewnętrznie. Kiedy ta odpowiedź była po raz pierwszy dostarczona, zmierzyłem znaczny wzrost wydajności, dodając BufferedStream. W tym czasie kierowałem system .NET 3.x na platformę 32-bitową. Dzisiaj, kierowanie na platformę .NET 4.5 na 64-bitowej platformie, nie widzę żadnej poprawy.

Związane z

Natknąłem się na przypadek, gdy przesyłanie strumieniowe dużego, wygenerowanego pliku CSV do strumienia odpowiedzi z działania ASP.Net MVC było bardzo powolne. Dodanie BufferedStream poprawiło wydajność o 100x w tym przypadku. Aby uzyskać więcej informacji Wyjście niebuforowane bardzo wolne


151
2018-03-10 01:22



Koleś, BufferedStream robi różnicę. +1 :) - Marcus
Koszt żądania danych z podsystemu IO jest kosztowny. W przypadku obracających się dysków może być konieczne poczekanie, aż talerz się zakręci, aby odczytać następny fragment danych, lub, co gorsza, czekać na przesunięcie głowicy dysku. Podczas gdy dyski SSD nie mają części mechanicznych, które spowalniałyby działanie, wciąż istnieje koszt operacji na IO, aby uzyskać do nich dostęp. Buforowane strumienie czytają więcej niż tylko żądania StreamReadera, redukując liczbę wywołań do systemu operacyjnego i ostatecznie liczbę oddzielnych żądań IO. - Eric J.
Naprawdę? To nie ma znaczenia w moim scenariuszu testowym. Według Brad Abrams nie ma korzyści z używania BufferedStream przez FileStream. - Nick Cox
@NickCox: Twoje wyniki mogą się różnić w zależności od podstawowego podsystemu IO. Na obrotowym dysku i kontrolerze dysku, który nie ma danych w pamięci podręcznej (a także danych nie buforowanych przez system Windows), przyspieszenie jest ogromne. Kolumna Brada została napisana w 2004 roku. Ostatnio zmierzyłem rzeczywistą, drastyczną poprawę. - Eric J.
Jest to bezużyteczne zgodnie z: stackoverflow.com/questions/492283/... FileStream już wewnętrznie używa bufora. - Erwin Mayer


Mówisz, że poproszono Cię o wyświetlenie paska postępu podczas wczytywania dużego pliku. Czy to dlatego, że użytkownicy naprawdę chcą zobaczyć dokładny% załadowania pliku, czy tylko dlatego, że chcą wizualnej informacji zwrotnej, że coś się dzieje?

Jeśli to drugie jest prawdziwe, rozwiązanie staje się znacznie prostsze. Po prostu zrób reader.ReadToEnd() w wątku tła i wyświetlać pasek postępu typu markiz zamiast właściwego.

Podnoszę tę kwestię, ponieważ z mojego doświadczenia wynika, że ​​tak się często dzieje. Kiedy piszesz program do przetwarzania danych, użytkownicy z pewnością będą zainteresowani% całkowitą liczbą, ale w przypadku prostych, ale powolnych aktualizacji interfejsu użytkownika, będą raczej chcieli wiedzieć, że komputer się nie zawiesił. :-)


14
2018-01-29 13:03



Ale czy użytkownik może anulować połączenie ReadToEnd? - Tim Scarborough
@Tim, dobrze zauważył. W takim przypadku wracamy do StreamReader pętla. Jednak nadal będzie prostsze, ponieważ nie ma potrzeby czytania z wyprzedzeniem, aby obliczyć wskaźnik postępu. - Christian Hayter


Jeśli czytasz statystyki wydajności i benchmarków na tej stronie, zobaczysz, że najszybszy sposób czytać (ponieważ czytanie, pisanie i przetwarzanie są różne) plik tekstowy jest następującym fragmentem kodu:

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

Wszystkich około 9 różnych metod zostało przetestowanych na stole, ale wydaje się, że większość z nich pojawiła się przed czasem, nawet przy wykonywaniu buforowanego czytnika o czym wspominali inni czytelnicy.


13
2017-09-19 14:21



To zadziałało dobrze dla rozebrania pliku postgres 19 GB, aby przetłumaczyć go na składnię sql w wielu plikach. Dzięki postgresowi, który nigdy nie wykonał poprawnie moich parametrów. /westchnienie - Damon Drake
Wydaje się, że różnica w wydajności jest opłacalna w przypadku naprawdę dużych plików, na przykład większych niż 150 MB (również powinno się używać a StringBuilder do załadowania ich do pamięci, ładuje się szybciej, ponieważ nie tworzy nowego ciągu za każdym razem, gdy dodajesz znaki) - b729sefc


W przypadku plików binarnych najszybszym sposobem ich odczytania jest to.

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

W moich testach jest setki razy szybszy.


7
2017-09-30 12:38



Czy masz jakieś twarde dowody tego? Dlaczego OP powinien wykorzystywać to do jakiejkolwiek innej odpowiedzi? Proszę zagłębić się nieco głębiej i podać nieco więcej szczegółów - Dylan Corriveau


Użyj pracownika pracującego w tle i przeczytaj tylko ograniczoną liczbę linii. Czytaj więcej tylko wtedy, gdy użytkownik przewija.

I staraj się nigdy nie używać ReadToEnd (). Jest to jedna z funkcji, którą myślisz "dlaczego to zrobili?"; to jest dziecko ze skryptu " pomocnik, który dobrze radzi sobie z małymi rzeczami, ale jak widzisz, ssie duże pliki ...

Ci goście, którzy zalecają korzystanie z StringBuilder, muszą częściej czytać MSDN:

Rozważania dotyczące wydajności
Metody Concat i AppendFormat łączą nowe dane z istniejącym obiektem String lub StringBuilder. Operacja konkatenacji obiektu String zawsze tworzy nowy obiekt z istniejącego łańcucha i nowych danych. Obiekt StringBuilder utrzymuje bufor w celu uwzględnienia łączenia nowych danych. Nowe dane są dołączane na końcu bufora, jeśli dostępne jest wolne miejsce; w przeciwnym razie przydzielany jest nowy, większy bufor, dane z oryginalnego bufora są kopiowane do nowego bufora, a następnie nowe dane są dołączane do nowego bufora. Wydajność operacji konkatenacji dla obiektu String lub StringBuilder zależy od częstotliwości przydzielania pamięci.
Operacja konkatenacji ciągów zawsze alokuje pamięć, podczas gdy operacja konkatenacji StringBuilder alokuje pamięć tylko wtedy, gdy bufor obiektu StringBuilder jest zbyt mały, aby pomieścić nowe dane. W konsekwencji klasa String jest preferowana w przypadku operacji konkatenacji, jeśli ustalona liczba obiektów String jest połączona. W takim przypadku poszczególne operacje konkatenacji mogą być połączone w pojedynczą operację przez kompilator. Obiekt StringBuilder jest preferowany w przypadku operacji konkatenacji, jeśli dowolna liczba ciągów jest połączona; na przykład, jeśli pętla łączy losową liczbę ciągów danych wejściowych użytkownika.

To znaczy olbrzymi przydzielanie pamięci, co staje się dużym wykorzystaniem systemu plików wymiany, który symuluje sekcje dysku twardego, aby działały jak pamięć RAM, ale dysk twardy jest bardzo wolny.

Opcja StringBuilder wygląda dobrze dla użytkowników korzystających z systemu jako użytkownicy mono, ale gdy masz dwóch lub więcej użytkowników czytających duże pliki w tym samym czasie, masz problem.


6
2018-01-29 12:42



daleko jesteście super szybcy! Niestety ze względu na sposób działania makra cały strumień musi zostać załadowany. Jak już wspomniałem, nie martw się o część richtext. Jest to początkowe obciążenie, które chcemy poprawić. - Nicole Lee
więc możesz pracować w częściach, przeczytać pierwsze X wiersze, zastosować makro, przeczytać drugie X wierszy, zastosować makro itd. ... jeśli wyjaśnisz, co robią te makra, możemy ci pomóc z większą precyzją - Tufo


To powinno wystarczyć, aby zacząć.

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

5
2018-01-29 12:56



Przesunąłbym "pętlę var = nowy char [1024]" z pętli: za każdym razem nie trzeba tworzyć nowego bufora. Po prostu wstaw to przed "while (count> 0)". - Tommy Carlier


Spójrz na poniższy fragment kodu. Wspomniałeś Most files will be 30-40 MB. To twierdzi, że odczytuje 180 MB w 1,4 sekundy na 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);
        }
    }
}

Oryginalny artykuł


4
2018-01-29 12:52



Tego rodzaju testy są nierzetelne. Po powtórzeniu testu odczytasz dane z pamięci podręcznej systemu plików. Jest to co najmniej jeden rząd wielkości szybszy niż prawdziwy test, który odczytuje dane z dysku. Plik o rozmiarze 180 MB nie może trwać krócej niż 3 sekundy. Uruchom ponownie komputer, uruchom test raz na rzeczywisty numer. - Hans Passant
linia stringBuilder.Append jest potencjalnie niebezpieczna, należy ją zamienić na stringBuilder.Append (fileContents, 0, charsRead); aby upewnić się, że nie dodajesz pełnych 1024 znaków, nawet jeśli strumień zakończył się wcześniej. - Johannes Rudolph


Lepszym rozwiązaniem może być obsługa plików odwzorowanych w pamięci tutaj.. Obsługa plików mapowanych pamięciami będzie dostępna w .NET 4 (myślę, że ... słyszałem to przez kogoś, kto o tym mówi), stąd ten wrapper, który używa p / invokes do wykonania tej samej pracy.

Edytować: Zobacz tutaj MSDN jak to działa, oto jest blog wpis wskazujący, jak to zrobić w nadchodzącym .NET 4, gdy wychodzi jako wersja. Link, który podałem wcześniej, jest opakowaniem wokół pinvoke, aby to osiągnąć. Możesz mapować cały plik do pamięci i przeglądać go jak przesuwane okno podczas przewijania pliku.


3
2018-01-29 12:52