Najlepszy sposób na odczytanie dużego pliku do tablicy bajtów w C #?


391

Mam serwer WWW, który odczytuje duże pliki binarne (kilka megabajtów) do tablic bajtów. Serwer może odczytywać kilka plików jednocześnie (różne żądania stron), więc szukam najbardziej zoptymalizowanego sposobu na zrobienie tego bez nadmiernego obciążania procesora. Czy poniższy kod jest wystarczająco dobry?

public byte[] FileToByteArray(string fileName)
{
    byte[] buff = null;
    FileStream fs = new FileStream(fileName, 
                                   FileMode.Open, 
                                   FileAccess.Read);
    BinaryReader br = new BinaryReader(fs);
    long numBytes = new FileInfo(fileName).Length;
    buff = br.ReadBytes((int) numBytes);
    return buff;
}

60
Twój przykład może być skrócony byte[] buff = File.ReadAllBytes(fileName).
Jesse C. Slicer

3
Dlaczego jest to usługa zewnętrzna innej firmy, co oznacza, że ​​plik musi być w całości w pamięci RAM przed wysłaniem do usługi internetowej, a nie przesyłany strumieniowo? Usługa internetowa nie pozna różnicy.
Brian

@Brian, Niektórzy klienci nie wiedzą, jak obsługiwać strumień .NET, na przykład Java. W takim przypadku wszystko, co można zrobić, to odczytać cały plik w tablicy bajtów.
sjeffrey,

4
@sjeffrey: Powiedziałem, że dane powinny być przesyłane strumieniowo, a nie przekazywane jako strumień .NET. Klienci i tak nie poznają różnicy.
Brian

Odpowiedzi:


776

Po prostu zamień całość na:

return File.ReadAllBytes(fileName);

Jednakże, jeśli jesteś zaniepokojony zużycie pamięci, należy nie przeczytać cały plik do pamięci wszystkie naraz w ogóle. Powinieneś to zrobić w kawałkach.


40
ta metoda jest ograniczona do plików 2 ^ 32 bajtowych (4,2 GB)
Mahmoud Farahat,

11
File.ReadAllBytes zgłasza OutOfMemoryException z dużymi plikami (przetestowane z plikiem 630 MB i nie powiodło się)
sakito,

6
@ juanjo.arana Tak, cóż ... oczywiście zawsze będzie coś, co nie mieści się w pamięci, w takim przypadku nie ma odpowiedzi na pytanie. Zasadniczo powinieneś przesyłać strumieniowo plik i nie przechowywać go całkowicie w pamięci. Być może warto przyjrzeć się temu w celu zatrzymania: msdn.microsoft.com/en-us/library/hh285054%28v=vs.110%29.aspx
Mehrdad Afshari

4
W .NET istnieje limit wielkości tablicy, ale w .NET 4.5 można włączyć obsługę dużych tablic (> 2 GB) za pomocą specjalnej opcji konfiguracji, patrz msdn.microsoft.com/en-us/library/hh285054.aspx
nielegalne -imigrant

3
@harag Nie, i nie o to pyta pytanie.
Mehrdad Afshari

72

Mógłbym argumentować, że odpowiedź tutaj ogólnie jest „nie”. O ile absolutnie nie potrzebujesz wszystkich danych naraz, rozważ użycie Streamopartego na interfejsie API (lub innego wariantu czytnika / iteratora). Jest to szczególnie ważne, gdy masz wiele równoległych operacji (jak sugeruje pytanie), aby zminimalizować obciążenie systemu i zmaksymalizować przepustowość.

Na przykład, jeśli przesyłasz strumieniowo dane do dzwoniącego:

Stream dest = ...
using(Stream source = File.OpenRead(path)) {
    byte[] buffer = new byte[2048];
    int bytesRead;
    while((bytesRead = source.Read(buffer, 0, buffer.Length)) > 0) {
        dest.Write(buffer, 0, bytesRead);
    }
}

3
Aby dodać do Twojej instrukcji, sugeruję nawet rozważenie asynchronicznych programów obsługi ASP.NET, jeśli masz operację związaną z We / Wy, taką jak przesyłanie strumieniowe pliku do klienta. Jeśli jednak z jakiegoś powodu musisz przeczytać cały plik byte[], sugeruję, aby unikać korzystania ze strumieni lub czegokolwiek innego, a jedynie użyć interfejsu API dostarczonego przez system.
Mehrdad Afshari

@Mehrdad - uzgodniono; ale pełny kontekst nie jest jasny. Podobnie MVC ma dla tego wyniki działania.
Marc Gravell

Tak, potrzebuję wszystkich danych jednocześnie. Przechodzi do zewnętrznego serwisu internetowego.
Tony_Henrich

Co to jest interfejs API dostarczany przez system?
Tony_Henrich

1
@Tony: stwierdziłem w mojej odpowiedzi: File.ReadAllBytes.
Mehrdad Afshari

32

Pomyślałbym tak:

byte[] file = System.IO.File.ReadAllBytes(fileName);

3
Zauważ, że może się to zatrzymać podczas pobierania naprawdę dużych plików.
vapcguy,

28

Twój kod może być uwzględniony w tym (zamiast File.ReadAllBytes):

public byte[] ReadAllBytes(string fileName)
{
    byte[] buffer = null;
    using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))
    {
        buffer = new byte[fs.Length];
        fs.Read(buffer, 0, (int)fs.Length);
    }
    return buffer;
} 

Zwróć uwagę na Integer.MaxValue - ograniczenie wielkości pliku wprowadzone metodą Read. Innymi słowy, możesz odczytać tylko 2 GB fragmentu jednocześnie.

Zauważ również, że ostatnim argumentem FileStream jest rozmiar bufora.

Sugerowałbym również przeczytanie o FileStream i BufferedStream .

Jak zawsze najkorzystniejszy będzie prosty przykładowy program do profilowania, który jest najszybszy.

Również podstawowy sprzęt będzie miał duży wpływ na wydajność. Czy używasz dysków twardych opartych na serwerze z dużymi pamięciami podręcznymi i kartą RAID z wbudowaną pamięcią podręczną? A może używasz standardowego napędu podłączonego do portu IDE?


Dlaczego rodzaj sprzętu miałby coś zmienić? Więc jeśli jest to IDE, używasz metody .NET, a jeśli jest to RAID, używasz innej?
Tony_Henrich

@Tony_Henrich - Nie ma to nic wspólnego z połączeniami wykonywanymi z języka programowania. Istnieją różne typy dysków twardych. Na przykład dyski Seagate są klasyfikowane jako „AS” lub „NS”, przy czym NS to serwer oparty na dużej pamięci podręcznej, podczas gdy dysk „AS” jest dyskiem przeznaczonym dla użytkowników domowych. Prędkości wyszukiwania i wewnętrzne szybkości przesyłania również wpływają na szybkość odczytywania danych z dysku. Macierze RAID mogą znacznie poprawić wydajność odczytu / zapisu poprzez buforowanie. Być może będziesz w stanie odczytać plik naraz, ale decydujący jest nadal sprzęt.

2
Ten kod zawiera krytyczny błąd. Odczyt jest wymagany tylko w celu zwrócenia co najmniej 1 bajtu.
mafu

Upewnij się, że zawiniemy long to int cast w sprawdzoną konstrukcję w następujący sposób: sprawdzone ((int) fs.Length)
tzup

Zrobiłbym to var binaryReader = new BinaryReader(fs); fileData = binaryReader.ReadBytes((int)fs.Length);w tym usingoświadczeniu. Ale to faktycznie tak jak OP, po prostu wyciąłem wiersz kodu, rzutując fs.Lengthna intzamiast zamiast longwartości FileInfodługości i konwertując to.
vapcguy

9

W zależności od częstotliwości operacji, wielkości plików i liczby plików, które przeglądasz, należy wziąć pod uwagę inne problemy z wydajnością. Jedną rzeczą do zapamiętania jest to, że każda z twoich tablic bajtów zostanie zwolniona na łaskę śmieciarza. Jeśli nie buforujesz żadnych z tych danych, możesz stworzyć dużo śmieci i stracić większość wydajności na % Time w GC. Jeśli porcje są większe niż 85 KB, alokujesz do sterty dużych obiektów (LOH), która będzie wymagała kolekcji wszystkich pokoleń, aby się zwolnić (jest to bardzo kosztowne, a na serwerze przestanie się wykonywać podczas pracy ). Dodatkowo, jeśli masz mnóstwo obiektów na LOH, możesz skończyć z fragmentacją LOH (LOH nigdy nie jest kompaktowany), co prowadzi do niskiej wydajności i wyjątków braku pamięci. Możesz odzyskać ten proces, gdy osiągniesz określony punkt, ale nie wiem, czy to najlepsza praktyka.

Chodzi o to, że powinieneś wziąć pod uwagę pełny cykl życia aplikacji, zanim koniecznie po prostu wczytasz wszystkie bajty do pamięci najszybciej, jak to możliwe, lub możesz handlować wydajnością krótkoterminową dla ogólnej wydajności.


kod źródłowy C # o tym, za zarządzanie garbage collector, chunks, wydajność, liczniki zdarzeń ...
PreguntonCojoneroCabrón

6

Powiedziałbym, że BinaryReaderjest w porządku, ale można to zmienić, zamiast tych wszystkich wierszy kodu służących do uzyskania długości bufora:

public byte[] FileToByteArray(string fileName)
{
    byte[] fileData = null;

    using (FileStream fs = File.OpenRead(fileName)) 
    { 
        using (BinaryReader binaryReader = new BinaryReader(fs))
        {
            fileData = binaryReader.ReadBytes((int)fs.Length); 
        }
    }
    return fileData;
}

Powinno być lepsze niż używanie .ReadAllBytes(), ponieważ widziałem w komentarzach na górze odpowiedzi, która zawiera, .ReadAllBytes()że jeden z komentujących miał problemy z plikami> 600 MB, ponieważ BinaryReaderjest przeznaczony do tego rodzaju rzeczy. Ponadto, wprowadzenie go w usingoświadczeniu zapewnia, że FileStreami BinaryReadersą zamykane i usuwane.


W przypadku C # należy użyć „using (FileStream fs = File.OpenRead (fileName))” zamiast „using (FileStream fs = new File.OpenRead (fileName))” jak podano powyżej. Właśnie usunąłem nowe słowo kluczowe przed File.OpenRead ()
Syed Mohamed

@Syed Powyższy kod został napisany dla C #, ale masz rację, że newnie był tam potrzebny. Oddalony.
vapcguy

1

W przypadku, gdy „duży plik” ma przekraczać limit 4 GB, moja następująca logika kodu jest odpowiednia. Kluczową kwestią, na którą należy zwrócić uwagę, jest typ danych LONG używany w metodzie SEEK. Jako LONG może wskazywać poza 2 ^ 32 granice danych. W tym przykładzie kod przetwarza najpierw przetwarzanie dużego pliku w porcjach 1 GB, po przetworzeniu dużych całych porcji 1 GB przetwarzane są pozostałe (<1 GB) bajty. Używam tego kodu do obliczania CRC plików przekraczających rozmiar 4 GB. (używając https://crc32c.machinezoo.com/ do obliczeń crc32c w tym przykładzie)

private uint Crc32CAlgorithmBigCrc(string fileName)
{
    uint hash = 0;
    byte[] buffer = null;
    FileInfo fileInfo = new FileInfo(fileName);
    long fileLength = fileInfo.Length;
    int blockSize = 1024000000;
    decimal div = fileLength / blockSize;
    int blocks = (int)Math.Floor(div);
    int restBytes = (int)(fileLength - (blocks * blockSize));
    long offsetFile = 0;
    uint interHash = 0;
    Crc32CAlgorithm Crc32CAlgorithm = new Crc32CAlgorithm();
    bool firstBlock = true;
    using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))
    {
        buffer = new byte[blockSize];
        using (BinaryReader br = new BinaryReader(fs))
        {
            while (blocks > 0)
            {
                blocks -= 1;
                fs.Seek(offsetFile, SeekOrigin.Begin);
                buffer = br.ReadBytes(blockSize);
                if (firstBlock)
                {
                    firstBlock = false;
                    interHash = Crc32CAlgorithm.Compute(buffer);
                    hash = interHash;
                }
                else
                {
                    hash = Crc32CAlgorithm.Append(interHash, buffer);
                }
                offsetFile += blockSize;
            }
            if (restBytes > 0)
            {
                Array.Resize(ref buffer, restBytes);
                fs.Seek(offsetFile, SeekOrigin.Begin);
                buffer = br.ReadBytes(restBytes);
                hash = Crc32CAlgorithm.Append(interHash, buffer);
            }
            buffer = null;
        }
    }
    //MessageBox.Show(hash.ToString());
    //MessageBox.Show(hash.ToString("X"));
    return hash;
}

0

Użyj klasy BufferedStream w języku C #, aby poprawić wydajność. Bufor to blok bajtów w pamięci używany do buforowania danych, co zmniejsza liczbę wywołań systemu operacyjnego. Bufory poprawiają wydajność odczytu i zapisu.

Poniżej znajduje się przykład kodu i dodatkowe wyjaśnienie: http://msdn.microsoft.com/en-us/library/system.io.bufferedstream.aspx


Jaki jest sens używania, BufferedStreamgdy czytasz całą rzecz na raz?
Mehrdad Afshari

Poprosił o najlepszą wydajność, aby nie czytać pliku na raz.
Todd Moses

9
Wydajność jest mierzalna w kontekście operacji. Dodatkowe buforowanie strumienia, który odczytujesz sekwencyjnie, naraz, do pamięci, raczej nie skorzysta z dodatkowego bufora.
Mehrdad Afshari

0

Użyj tego:

 bytesRead = responseStream.ReadAsync(buffer, 0, Length).Result;

2
Witamy w Stack Overflow! Ponieważ wyjaśnienia są ważną częścią odpowiedzi na tej platformie, prosimy o wyjaśnienie kodu i sposobu rozwiązania problemu w pytaniu oraz dlaczego może być lepszy niż inne odpowiedzi. Nasz przewodnik Jak napisać dobrą odpowiedź może być dla Ciebie pomocny. Dzięki
David

-4

Polecam wypróbować Response.TransferFile()metodę a następnie Response.Flush()i Response.End()do obsługi dużych plików.


-7

Jeśli masz do czynienia z plikami powyżej 2 GB, przekonasz się, że powyższe metody zawiodły.

O wiele łatwiej jest po prostu przekazać strumień do MD5 i pozwolić ci na podzielenie pliku za ciebie:

private byte[] computeFileHash(string filename)
{
    MD5 md5 = MD5.Create();
    using (FileStream fs = new FileStream(filename, FileMode.Open))
    {
        byte[] hash = md5.ComputeHash(fs);
        return hash;
    }
}

11
Nie rozumiem, w jaki sposób kod ma znaczenie dla pytania (ani co sugerujesz w tekście pisanym)
Vojtech B
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.