Jaki jest najszybszy sposób tworzenia sumy kontrolnej dla dużych plików w C #

130

Muszę synchronizować duże pliki na niektórych komputerach. Pliki mogą mieć rozmiar do 6 GB. Synchronizacja będzie wykonywana ręcznie co kilka tygodni. Nie mogę brać pod uwagę nazw plików, ponieważ mogą się one zmienić w dowolnym momencie.

Mój plan polega na utworzeniu sum kontrolnych na komputerze docelowym i na komputerze źródłowym, a następnie skopiowanie wszystkich plików z sumą kontrolną, które nie znajdują się jeszcze w miejscu docelowym, do miejsca docelowego. Moja pierwsza próba wyglądała mniej więcej tak:

using System.IO;
using System.Security.Cryptography;

private static string GetChecksum(string file)
{
    using (FileStream stream = File.OpenRead(file))
    {
        SHA256Managed sha = new SHA256Managed();
        byte[] checksum = sha.ComputeHash(stream);
        return BitConverter.ToString(checksum).Replace("-", String.Empty);
    }
}

Problem
dotyczył czasu wykonania: - z SHA256 z plikiem 1,6 GB -> 20 minut
- z MD5 z plikiem 1,6 GB -> 6,15 minuty

Czy istnieje lepszy - szybszy - sposób uzyskania sumy kontrolnej (może z lepszą funkcją mieszającą)?

crono
źródło
2
Czy naprawdę potrzebujesz sprawdzić sumę kontrolną? Jak kopiujesz pliki? Jeśli korzystasz z systemu Windows, użyłbym najnowszej wersji Robocopy ...
Mesh
6
Dobra wskazówka, aby zawracać sobie głowę haszowaniem tylko wtedy, gdy rozmiary plików są różne między 2 plikami kandydatów stackoverflow.com/a/288756/74585
Matthew Lock

Odpowiedzi:

119

Problem polega na tym, że SHA256Managedodczytuje jednocześnie 4096 bajtów (dziedziczenie z FileStreami nadpisywanie, Read(byte[], int, int)aby zobaczyć, ile odczytuje ze strumienia pliku), co jest zbyt małym buforem dla operacji we / wy dysku.

Do prędkości rzeczy w górę (2 minuty do mieszania pliku 2 GB na moim komputerze z SHA256, 1 minuta do MD5) opasania FileStreamw BufferedStreami ustawić rozsądnie wielkości rozmiar bufora (próbowałem buforem ~ 1 MB):

// Not sure if BufferedStream should be wrapped in using block
using(var stream = new BufferedStream(File.OpenRead(filePath), 1200000))
{
    // The rest remains the same
}
Anton Gogolev
źródło
4
OK - to spowodowało różnicę - haszowanie pliku 1,6 GB za pomocą MD5 zajęło 5,2 sekundy na moim pudełku (QuadCode @ 2,6 GHz, 8 GB Ram) - nawet szybciej niż natywna implementacja ...
crono
4
nie rozumiem. właśnie wypróbowałem tę sugestię, ale różnica jest minimalna lub zerowa. Plik 1024 MB bez buforowania 12-14 sekund, z buforowaniem również 12-14 sekund - rozumiem, że odczytanie setek bloków 4k da więcej operacji we / wy, ale zadaję sobie pytanie, czy framework lub natywne API poniżej frameworka już tego nie obsługują ..
Christian Casutt,
13
Trochę późno na imprezę, ale w przypadku FileStreams nie ma już potrzeby zawijania strumienia w BufferedStream, tak jak obecnie jest to już robione w samym FileStream. Źródło
Reyhn,
Właśnie przechodziłem przez ten problem z mniejszymi plikami (<10 MB, ale zdobycie MD5 zajmowało wieczność). Mimo że używam .Net 4.5, przełączenie się na tę metodę za pomocą BufferedStream skróciło czas mieszania z około 8,6 sekundy do <300 ms dla pliku
8,6 MB
Użyłem BufferedStream / w 512 kB zamiast 1024 kB. Plik 1,8 GB został rozwiązany w 30 sekund.
Hugo Woesthuis
64

Nie sumuj całego pliku, twórz sumy kontrolne co około 100 MB, aby każdy plik miał kolekcję sum kontrolnych.

Następnie porównując sumy kontrolne, możesz przestać porównywać po pierwszej innej sumie kontrolnej, wyjść wcześnie i zaoszczędzić na przetwarzaniu całego pliku.

W przypadku identycznych plików zajmie to cały czas.

Binary Worrier
źródło
2
Podoba mi się ten pomysł, ale w moim scenariuszu nie zadziała, ponieważ z biegiem czasu będę miał wiele niezmienionych plików.
crono
1
jak obliczyć sumę kontrolną co 100 MB pliku?
Smith,
1
Nie jest to dobry pomysł przy używaniu sumy kontrolnej ze względów bezpieczeństwa, ponieważ atakujący może po prostu zmienić wykluczone bajty.
b.kiener
2
+1 To doskonały pomysł, gdy porównujesz jeden do jednego. Niestety, używam skrótu MD5 jako indeksu do wyszukiwania unikalnych plików wśród wielu duplikatów (kontrole wiele do wielu).
Nathan Goings
1
@ b.kiener Żaden bajt nie jest wykluczony. Źle go zrozumiałeś.
Soroush Falahati
49

Jak zauważył Anton Gogolev , FileStream odczytuje domyślnie 4096 bajtów naraz, ale możesz określić dowolną inną wartość za pomocą konstruktora FileStream:

new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 16 * 1024 * 1024)

Zauważ, że Brad Abrams z firmy Microsoft napisał w 2004 roku:

nie ma żadnej korzyści z zawijania BufferedStream wokół FileStream. Skopiowaliśmy logikę buforowania BufferedStream do FileStream około 4 lata temu, aby zachęcić do lepszej domyślnej wydajności

źródło

Tal Aloni
źródło
22

Wywołaj port systemu Windows programu md5sum.exe . To około dwa razy szybciej niż implementacja .NET (przynajmniej na moim komputerze z plikiem 1,2 GB)

public static string Md5SumByProcess(string file) {
    var p = new Process ();
    p.StartInfo.FileName = "md5sum.exe";
    p.StartInfo.Arguments = file;            
    p.StartInfo.UseShellExecute = false;
    p.StartInfo.RedirectStandardOutput = true;
    p.Start();
    p.WaitForExit();           
    string output = p.StandardOutput.ReadToEnd();
    return output.Split(' ')[0].Substring(1).ToUpper ();
}
Christian Birkl
źródło
3
WOW - użycie md5sums.exe z pc-tools.net/win32/md5sums sprawia, że ​​jest to naprawdę szybkie. 1681457152 bajtów, 8672 ms = 184,91 MB / s -> 1,6 GB ~ 9 sekund To wystarczy do moich celów.
crono
16

Ok - dziękuję wszystkim - podsumuję:

  1. użycie "natywnego" exe do wykonania haszowania zajęło czas od 6 minut do 10 sekund, co jest ogromne.
  2. Zwiększenie bufora było jeszcze szybsze - plik 1,6GB zajmował 5,2 sekundy przy użyciu MD5 w .Net, więc pójdę z tym rozwiązaniem - jeszcze raz dziękuję
crono
źródło
10

Zrobiłem testy z rozmiarem bufora, uruchamiając ten kod

using (var stream = new BufferedStream(File.OpenRead(file), bufferSize))
{
    SHA256Managed sha = new SHA256Managed();
    byte[] checksum = sha.ComputeHash(stream);
    return BitConverter.ToString(checksum).Replace("-", String.Empty).ToLower();
}

Testowałem z plikiem o rozmiarze 29½ GB i wyniki były takie

  • 10.000: 369,24s
  • 100.000: 362,55s
  • 1.000.000: 361,53s
  • 10.000.000: 434,15s
  • 100.000.000: 435,15s
  • 1.000.000.000: 434,31s
  • I 376,22 s przy używaniu oryginalnego kodu bez buforowania.

Używam procesora i5 2500K, 12 GB pamięci RAM i dysku SSD OCZ Vertex 4 256 GB.

Pomyślałem więc, co ze standardowym dyskiem twardym o pojemności 2 TB. A wyniki były takie

  • 10.000: 368,52s
  • 100 000: 364,15 s
  • 1.000.000: 363,06s
  • 10.000.000: 678,96s
  • 100.000.000: 617,89s
  • 1.000.000.000: 626,86s
  • I dla żadnego buforowanego 368,24

Dlatego zalecałbym albo brak bufora, albo bufor o maksymalnej wartości 1 miliona.

Anders
źródło
Nie rozumiem. Jak ten test może zaprzeczać przyjętej odpowiedzi Antona Gogolewa?
buddybubble
Czy możesz dodać opis każdego pola w swoich danych?
videoguy
3

Wiem, że spóźniłem się na imprezę, ale przed wdrożeniem rozwiązania przeprowadziłem test.

Przeprowadziłem test z wbudowaną klasą MD5, a także md5sum.exe . W moim przypadku wbudowana klasa zajęła 13 sekund, gdzie md5sum.exe zbyt około 16-18 sekund w każdym uruchomieniu.

    DateTime current = DateTime.Now;
    string file = @"C:\text.iso";//It's 2.5 Gb file
    string output;
    using (var md5 = MD5.Create())
    {
        using (var stream = File.OpenRead(file))
        {
            byte[] checksum = md5.ComputeHash(stream);
            output = BitConverter.ToString(checksum).Replace("-", String.Empty).ToLower();
            Console.WriteLine("Total seconds : " + (DateTime.Now - current).TotalSeconds.ToString() + " " + output);
        }
    }
Romil Kumar Jain
źródło
2

Robisz coś źle (prawdopodobnie za mały bufor odczytu). Na maszynie w niedoszłym wieku (Athlon 2x1800MP z 2002 roku), która ma DMA na dysku, prawdopodobnie nie działa (6,6 M / s jest cholernie powolny podczas odczytu sekwencyjnego):

Utwórz plik 1G z „losowymi” danymi:

# dd if=/dev/sdb of=temp.dat bs=1M count=1024    
1073741824 bytes (1.1 GB) copied, 161.698 s, 6.6 MB/s

# time sha1sum -b temp.dat
abb88a0081f5db999d0701de2117d2cb21d192a2 *temp.dat

1m 5,299s

# time md5sum -b temp.dat
9995e1c1a704f9c1eb6ca11e7ecb7276 *temp.dat

1m58.832s

To też jest dziwne, md5 jest dla mnie konsekwentnie wolniejsze niż sha1 (kilkakrotnie powtórzone).

Pasi Savolainen
źródło
Tak - spróbuję zwiększyć bufor - jak sugerował Anton Gogolev. Przepuściłem go przez "natywny" plik MD5.exe, który zajął 9 sekund z plikiem 1,6 GB.
crono