Jak jednocześnie wczytywać i przetwarzać duży plik?

Jak jednocześnie wczytywać i przetwarzać duży plik?
VA
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 2
0

Witam,

mam plik binarny ok 850MB.
Zapisane tam obiekty to struktura zawierająca 4 liczby float.
Aktualnie kod wygląda tak:

Kopiuj
static void Main(string[] args)
{
    string filePath = @"D:\data.bin";

    //Open the stream and read it back.
    using (FileStream fs = File.OpenRead(filePath))
    {
        byte[] b = new byte[16];

        BinaryReader _br = new BinaryReader(fs);

        while ((b = _br.ReadBytes(16)).Length > 0)
        {
            float v1 = BitConverter.ToSingle(b, 0);
            float v2 = BitConverter.ToSingle(b, 4);
            float v3 = BitConverter.ToSingle(b, 8);
            float v4 = BitConverter.ToSingle(b, 12);
            Console.WriteLine("value: " + v1 + " " + v2 + " " + v3 + " " + v4);
        }
    }
}
  1. Pytanie - jak można by przerobić kod aby metoda działała szybciej (jakieś czytanie kawałkami do bufora)?
  2. W związku z tym że plik z danymi jest spory, chciałbym aby po odczytaniu np. 10000 obiektów wywoływać inną metodę, która przetwarzała by odczytane dane.

Pozdrawiam.

elwis
  • Rejestracja: dni
  • Ostatnio: dni
1

Nie znam C#, więc może coś mi umyka, ale ten kod nie wygląda jakby można było wiele w nim poprawić (w ramach C#). Jedno co przychodzi mi do głowy to użyć mapowania pliku do pamięci. Tu jest o tym jak to sie robi w C#:
https://learn.microsoft.com/en-us/dotnet/standard/io/memory-mapped-files

Następnie, zdefiniowałbym strukturę i po prostu robił wskaźnik na dany frament pamięci, żeby nawet konwersji żadnej nie robić, ale obawiam się, że w C# nie da się tego zrobić.

SL
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 1023
2

Dodaj buforowanie, bo częste czytanie z pliku to duży overhead https://learn.microsoft.com/en-us/dotnet/api/system.io.bufferedstream?view=net-8.0

AN
  • Rejestracja: dni
  • Ostatnio: dni
1

Najprościej przerobić na mniej więcej coś takiego:

Kopiuj
        static void Main(string[] args)
        {
            string filePath = @"D:\data.bin";

            int readIteration = 10;

            //Open the stream and read it back.
            using (FileStream fs = File.OpenRead(filePath))
            {
                byte[] b = new byte[16 * readIteration];

                BinaryReader _br = new BinaryReader(fs);

                int b_length = _br.Read(b, 0, 16 * readIteration);
                while (b_length > 0)
                {
                    for (int i = 0; i < b_Length; i += 16)
                    {
                        float v1 = BitConverter.ToSingle(b, 0 + i);
                        float v2 = BitConverter.ToSingle(b, 4 + i);
                        float v3 = BitConverter.ToSingle(b, 8 + i);
                        float v4 = BitConverter.ToSingle(b, 12 + i);
                        Console.WriteLine("value: " + v1 + " " + v2 + " " + v3 + " " + v4);
                    }
                    b_length = _br.Read(b, 0, 16 * readIteration);
                }
            }
        }

Zmienną readIteration dobierz sobie doświadczalnie. Przy małej wartości będzie zabierać mało pamięci i działać wolniej, przy dużej wartości będzie zabierać więcej pamięci i działać szybciej.

Oprócz tego, zamiast ReadBytes użyj Read, bo ta pierwsza tworzy nową tablicę, a ta druga wprowadza odczytane dane do istniejącej i podanej tablicy.

FA
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: warszawa
  • Postów: 315
1

Pytanie - jak można by przerobić kod aby metoda działała szybciej (jakieś czytanie kawałkami do bufora)?
W związku z tym że plik z danymi jest spory, chciałbym aby po odczytaniu np. 10000 obiektów wywoływać inną metodę, która przetwarzała by odczytane dane.

Twoim największym problem jest że wołasz console write line co interacje. Z tego co pamietam on ma ~16ms opóźnienia. Wywal to za pętle lub pisz 100/1000 linii naraz.

Co potrzebujesz robic z tym plikiem bo chyba nie na niego patrzeć. Może klasa/metoda która odczytuje liczby w wybranym zakresie indexów była by rozwiazaniem dla Ciebie?

KE
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 761
0

Koniecznie czytaj większymi kawałkami. Z ciekawości spróbowałem przykład w bardzo "wolnym" Rubim https://stackoverflow.com/questions/1682120/read-a-file-in-chunks-in-ruby i czytając po 1024 bajty randomowy plik 1 giga wczytuje się tak długo że przerwałem. Czytając po 1 MiB wczytuje się w mniej niż 100 ms (zakładając że zawartość jest w cache).

obscurity
  • Rejestracja: dni
  • Ostatnio: dni
3
slsy napisał(a):

Dodaj buforowanie, bo częste czytanie z pliku to duży overhead https://learn.microsoft.com/en-us/dotnet/api/system.io.bufferedstream?view=net-8.0

FileStream jest domyślnie buforowany, nie ma sensu go otaczać w BufferedStream, jedynie można zwiększyć wielkość bufora używając konstruktora

Kopiuj
public FileStream (string path, System.IO.FileMode mode, System.IO.FileAccess access, System.IO.FileShare share, int bufferSize, System.IO.FileOptions options);

zamiast File.OpenRead. Domyślny rozmiar bufora został dobrze wyważony, ale możesz próbować go zmienić.

Jeśli chcesz jednocześnie wczytywać i przetwarzać dane to wykorzystaj wzorzec producer/consumer. Jeden wątek będzie wczytywał dane a drugi je w tym samym czasie przetwarzał. Możesz do tego użyć BlockingCollection (prościej) /System.Threading.Channels (lepiej) lub paczki nugetowej takiej jak System.Threading.Tasks.Dataflow. Szukaj w google pod hasłem "c# producer consumer" i bierz odpowiedzi z ostatnich lat bo można znaleźć wiele przestarzałych metod.

Można tutaj wprowadzić parę optymalizacji odczytu, ale moim zdaniem będą to mikrooptymalizacje które nie będą miały znaczenia gdy wprowadzisz jednoczesne przetwarzanie danych w osobnym wątku. Ta metoda jest głównie ograniczona przez dysk i fragmentację danych, tego nie przeskoczysz. Warto się przyjrzeć dopiero jeśli ta metoda trwa wyraźnie dłużej niż skopiowanie tego pliku na dysku i jeśli jesteś w stanie obrobić dane szybciej niż się wczytują.

W tym kodzie najwolniejsze jest Console.WriteLine, wbrew pozorom wyświetlanie na konsoli jest bardzo powolne. Kod testuj w trybie release bez debugera, podpięty debugger w takich szybkich drobnych operacjach potrafi spowolnić wykonanie nawet kilkusetkrotnie.

A swoją drogą może w ogóle nie potrzebujesz go w całości czytać? Może możesz dodać sobie do niego indeks i skoczyć do konkretnego miejsca w pliku. A idąc dalej może możesz użyć bazy danych jak człowiek która zajmie się tym wszystkim sama. Raz zaimportujesz ten plik i po kłopocie.

VA
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 2
1
andrzejlisek napisał(a):

Najprościej przerobić na mniej więcej coś takiego:

Kopiuj
        static void Main(string[] args)
        {
            string filePath = @"D:\data.bin";

            int readIteration = 10;

            //Open the stream and read it back.
            using (FileStream fs = File.OpenRead(filePath))
            {
                byte[] b = new byte[16 * readIteration];

                BinaryReader _br = new BinaryReader(fs);

                int b_length = _br.Read(b, 0, 16 * readIteration);
                while (b_length > 0)
                {
                    for (int i = 0; i < b_Length; i += 16)
                    {
                        float v1 = BitConverter.ToSingle(b, 0 + i);
                        float v2 = BitConverter.ToSingle(b, 4 + i);
                        float v3 = BitConverter.ToSingle(b, 8 + i);
                        float v4 = BitConverter.ToSingle(b, 12 + i);
                        Console.WriteLine("value: " + v1 + " " + v2 + " " + v3 + " " + v4);
                    }
                    b_length = _br.Read(b, 0, 16 * readIteration);
                }
            }
        }

Zmienną readIteration dobierz sobie doświadczalnie. Przy małej wartości będzie zabierać mało pamięci i działać wolniej, przy dużej wartości będzie zabierać więcej pamięci i działać szybciej.

Oprócz tego, zamiast ReadBytes użyj Read, bo ta pierwsza tworzy nową tablicę, a ta druga wprowadza odczytane dane do istniejącej i podanej tablicy.

Na chwilę obecną zaimplementowałem to rozwiązanie.
Uzyskałem trzykrotny wzrost wydajności (odpowiednio manipulując zmienną readIteration).

somekind
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Wrocław
0
varec napisał(a):

Uzyskałem trzykrotny wzrost wydajności (odpowiednio manipulując zmienną readIteration).

To nie jest żaden wzrost, trzy rzędy wielkości to byłoby coś wartego wysiłku.

Czytając doklejaj tekst do jakiegoś StringBuildera i wypluwaj na konsolę co 10-100 tysięcy iteracji zamiast za każdym razem.

No i to bym przepisał to: "value: " + v1 + " " + v2 + " " + v3 + " " + v4 na interpolowany string.

AN
  • Rejestracja: dni
  • Ostatnio: dni
0
somekind napisał(a):

To nie jest żaden wzrost, trzy rzędy wielkości to byłoby coś wartego wysiłku.

To zależy od przypadku. Trzykrotny wzrost kosztem znacznego skomplikowania i zagmatwania kodu realizującego czynność, to może i nie warto, ale trzykrotny wzrost kosztem wykonania kilku prostych zmian, po których kod nie traci znacznie na przejrzystości, to warto.

Tak samo faktyczny czas czynności. Jeżeli czynność trwa 3 sekundy, to może i nie ma sensu skracać jej do dwóch czy jednej sekundy. A jeżeli czynność trwa godzinę, to skrócenie jej do 20 minut jest odczuwalne. Czy to jest potrzebne i czy ma znaczenie, trzeba sobie samemu odpowiedzieć.

FA
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: warszawa
  • Postów: 315
0

Trzykrotny wzrost kosztem znacznego skomplikowania i zagmatwania kodu realizującego czynność,

Ten kod to jedna metoda. NIC. ŻART. Skomplikowany to moze jakiś algorytm lub logika klepana tygodniami, a nie jedna metoda, sprowadzająca sie do pytania, czy skorzystać z frameworka, poprawnie, czy na odczep.
Całe problem jest powodowany brakiem elementarnej wiedzy,(ale to dobrze że OP pyta) a nie złożonością algorytmu.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.