Come confrontare velocemente 2 file usando .NET?


Risposte:


117

Molto probabilmente un confronto di checksum sarà più lento di un confronto byte per byte.

Per generare un checksum, devi caricare ogni byte del file ed eseguirne l'elaborazione. Dovrai quindi farlo sul secondo file. L'elaborazione sarà quasi sicuramente più lenta del controllo di confronto.

Per quanto riguarda la generazione di un checksum: puoi farlo facilmente con le classi di crittografia. Ecco un breve esempio di generazione di un checksum MD5 con C #.

Tuttavia, un checksum può essere più rapido e più sensato se è possibile pre-calcolare il checksum del caso "test" o "base". Se hai un file esistente e stai verificando se un nuovo file è uguale a quello esistente, pre-calcolare il checksum sul tuo file "esistente" significherebbe dover fare il DiskIO una sola volta, sul nuovo file. Questo sarebbe probabilmente più veloce di un confronto byte per byte.


30
Assicurati di prendere in considerazione la posizione dei tuoi file. Se stai confrontando i file locali con un backup a metà strada in tutto il mondo (o su una rete con orribile larghezza di banda), potresti essere meglio prima di eseguire l'hash e inviare un checksum sulla rete invece di inviare un flusso di byte a confrontare.
Kim,

@ReedCopsey: sto riscontrando un problema simile, dal momento che devo archiviare i file di input / output prodotti da diverse elaborazioni che dovrebbero contenere molte duplicazioni. Ho pensato di usare l'hash precompilato, ma pensi di poter ragionevolmente presumere che se l'hash 2 (ad es. MD5) è uguale, i 2 file sono uguali ed evitano ulteriori confronti byte-2-byte? Per quanto ne so MD5 / SHA1 ecc. Le collisioni sono davvero improbabili ...
digEmAll

1
@digEmAll La probabilità di collisione è bassa - puoi sempre fare un hash più forte - cioè: usa SHA256 invece di SHA1, il che ridurrà ulteriormente la probabilità di collisioni.
Reed Copsey,

grazie per la tua risposta - sto solo entrando in .net. suppongo che se si utilizza la tecnica hashcode / check sum, gli hash della cartella principale verranno memorizzati in modo persistente da qualche parte? per curiosità come lo memorizzeresti per un'applicazione WPF - cosa faresti? (attualmente sto esaminando xml, file di testo o database).
BKSpurgeon,

139

Il metodo più lento possibile è confrontare due file byte per byte. Il più veloce che sono stato in grado di inventare è un confronto simile, ma invece di un byte alla volta, useresti un array di byte dimensionato su Int64, quindi confronterai i numeri risultanti.

Ecco cosa mi è venuto in mente:

    const int BYTES_TO_READ = sizeof(Int64);

    static bool FilesAreEqual(FileInfo first, FileInfo second)
    {
        if (first.Length != second.Length)
            return false;

        if (string.Equals(first.FullName, second.FullName, StringComparison.OrdinalIgnoreCase))
            return true;

        int iterations = (int)Math.Ceiling((double)first.Length / BYTES_TO_READ);

        using (FileStream fs1 = first.OpenRead())
        using (FileStream fs2 = second.OpenRead())
        {
            byte[] one = new byte[BYTES_TO_READ];
            byte[] two = new byte[BYTES_TO_READ];

            for (int i = 0; i < iterations; i++)
            {
                 fs1.Read(one, 0, BYTES_TO_READ);
                 fs2.Read(two, 0, BYTES_TO_READ);

                if (BitConverter.ToInt64(one,0) != BitConverter.ToInt64(two,0))
                    return false;
            }
        }

        return true;
    }

Nei miei test, sono stato in grado di vedere questo sovraperformare uno scenario semplice ReadByte () di quasi 3: 1. Con una media di oltre 1000 esecuzioni, ho ottenuto questo metodo a 1063 ms e il metodo seguente (semplice confronto byte per byte) a 3031 ms. L'hashing è sempre tornato al secondo secondo a circa 865 ms. Questo test è stato eseguito con un file video di ~ 100 MB.

Ecco i metodi di lettura e hashing che ho usato, a scopo di confronto:

    static bool FilesAreEqual_OneByte(FileInfo first, FileInfo second)
    {
        if (first.Length != second.Length)
            return false;

        if (string.Equals(first.FullName, second.FullName, StringComparison.OrdinalIgnoreCase))
            return true;

        using (FileStream fs1 = first.OpenRead())
        using (FileStream fs2 = second.OpenRead())
        {
            for (int i = 0; i < first.Length; i++)
            {
                if (fs1.ReadByte() != fs2.ReadByte())
                    return false;
            }
        }

        return true;
    }

    static bool FilesAreEqual_Hash(FileInfo first, FileInfo second)
    {
        byte[] firstHash = MD5.Create().ComputeHash(first.OpenRead());
        byte[] secondHash = MD5.Create().ComputeHash(second.OpenRead());

        for (int i=0; i<firstHash.Length; i++)
        {
            if (firstHash[i] != secondHash[i])
                return false;
        }
        return true;
    }

1
Mi hai semplificato la vita. Grazie
anindis il

2
@anindis: Per completezza, si consiglia di leggere sia la risposta @ Lars e @ risposta di RandomInsano . Sono contento che abbia aiutato tanti anni dopo! :)
chsh

1
Il FilesAreEqual_Hashmetodo dovrebbe avere un usingflusso su entrambi i flussi come il ReadBytemetodo, altrimenti si bloccherà su entrambi i file.
Ian Mercer,

2
Si noti che FileStream.Read()potrebbe effettivamente leggere meno byte rispetto al numero richiesto. Dovresti usare StreamReader.ReadBlock()invece.
Palec,

2
Nella versione Int64 quando la lunghezza del flusso non è un multiplo di Int64, l'ultima iterazione sta confrontando i byte non riempiti usando il riempimento dell'iterazione precedente (che dovrebbe anche essere uguale quindi va bene). Inoltre, se la lunghezza del flusso è inferiore a sizeof (Int64), i byte non riempiti sono 0 poiché C # inizializza le matrici. IMO, il codice dovrebbe probabilmente commentare queste stranezze.
crokusek,

46

Se tu non decide che si ha realmente bisogno di un confronto pieno di byte per byte (vedi altre risposte per la discussione di hashing), allora la soluzione più semplice è:


• per System.IO.FileInfoesempio:

public static bool AreFileContentsEqual(FileInfo fi1, FileInfo fi2) =>
    fi1.Length == fi2.Length &&
    (fi1.Length == 0 || File.ReadAllBytes(fi1.FullName).SequenceEqual(
                        File.ReadAllBytes(fi2.FullName)));


• per i System.Stringnomi dei percorsi:

public static bool AreFileContentsEqual(String path1, String path2) =>
                   AreFileContentsEqual(new FileInfo(path1), new FileInfo(path2));


A differenza di altre risposte postate, questo è definitivamente corretto per qualsiasi tipo di file: binario, testo, media, eseguibile, ecc., Ma come un confronto binario completo , file che differiscono solo in modi "non importanti" (come BOM , riga -ending , codifica dei caratteri , metadati multimediali, spazi bianchi, riempimento, commenti del codice sorgente, ecc.) saranno sempre considerati non uguali .

Questo codice carica completamente entrambi i file in memoria, quindi non dovrebbe essere usato per confrontare file veramente giganteschi . Al di là di questo importante avvertimento, il caricamento completo non è in realtà una penalità dato il design di .NET GC (perché è fondamentalmente ottimizzato per mantenere allocazioni di piccole dimensioni e di breve durata estremamente economiche ), e in effetti potrebbe anche essere ottimale quando sono attese dimensioni del file essere inferiore a 85 KB , poiché l'utilizzo di un minimo di codice utente (come mostrato qui) implica la delega massima dei problemi di prestazioni dei file alCLR , BCLeJIT di beneficiare di (ad esempio) l'ultima tecnologia di progettazione, codice di sistema, e le ottimizzazioni runtime adattivi.

Inoltre, per tali scenari di lavoro quotidiano, le preoccupazioni sulle prestazioni del confronto byte per byte tramite LINQenumeratori (come mostrato qui) sono controverse, poiché colpire il disco a̲t̲ a̲l̲l̲ per l'I / O dei file diminuirà, di diversi ordini di grandezza, i vantaggi delle varie alternative di confronto della memoria. Ad esempio, anche se SequenceEqual non di fatto darci la "ottimizzazione" di abbandonare il primo mancata corrispondenza , questo poco importa dopo aver già recuperato il contenuto dei file, ognuna completamente necessario confermare la partita ..


3
questo non sembra buono per file di grandi dimensioni. non buono per l'uso della memoria poiché leggerà entrambi i file fino alla fine prima di iniziare a confrontare l'array di byte. Ecco perché preferirei scegliere uno streamreader con un buffer.
Krypto_47

3
@ Krypto_47 Ho discusso di questi fattori e dell'uso appropriato nel testo della mia risposta.
Glenn Slayden,

33

Oltre alla risposta di Reed Copsey :

  • Il caso peggiore è dove i due file sono identici. In questo caso è meglio confrontare i file byte per byte.

  • Se i due file non sono identici, puoi velocizzare un po 'le cose rilevando prima che non sono identici.

Ad esempio, se i due file hanno una lunghezza diversa, sai che non possono essere identici e non devi nemmeno confrontare il loro contenuto reale.


10
Per essere completo: l'altro grande guadagno si arresta non appena i byte in 1 posizione sono diversi.
Henk Holterman,

6
@Henk: pensavo fosse troppo ovvio :-)
dtb

1
Un buon punto per aggiungere questo. Per me era ovvio, quindi non l'ho incluso, ma è bene menzionarlo.
Reed Copsey,

16

Sta diventando ancora più veloce se non leggi in piccoli blocchi da 8 byte ma fai un giro in giro, leggendo un pezzo più grande. Ho ridotto il tempo medio di confronto a 1/4.

    public static bool FilesContentsAreEqual(FileInfo fileInfo1, FileInfo fileInfo2)
    {
        bool result;

        if (fileInfo1.Length != fileInfo2.Length)
        {
            result = false;
        }
        else
        {
            using (var file1 = fileInfo1.OpenRead())
            {
                using (var file2 = fileInfo2.OpenRead())
                {
                    result = StreamsContentsAreEqual(file1, file2);
                }
            }
        }

        return result;
    }

    private static bool StreamsContentsAreEqual(Stream stream1, Stream stream2)
    {
        const int bufferSize = 1024 * sizeof(Int64);
        var buffer1 = new byte[bufferSize];
        var buffer2 = new byte[bufferSize];

        while (true)
        {
            int count1 = stream1.Read(buffer1, 0, bufferSize);
            int count2 = stream2.Read(buffer2, 0, bufferSize);

            if (count1 != count2)
            {
                return false;
            }

            if (count1 == 0)
            {
                return true;
            }

            int iterations = (int)Math.Ceiling((double)count1 / sizeof(Int64));
            for (int i = 0; i < iterations; i++)
            {
                if (BitConverter.ToInt64(buffer1, i * sizeof(Int64)) != BitConverter.ToInt64(buffer2, i * sizeof(Int64)))
                {
                    return false;
                }
            }
        }
    }
}

13
In generale, il controllo count1 != count2non è corretto. Stream.Read()può restituire un importo inferiore al conteggio fornito, per vari motivi.
porges,

1
Per garantire che il buffer terrà un numero pari di Int64blocchi, si consiglia di calcolare la dimensione in questo modo: const int bufferSize = 1024 * sizeof(Int64).
Jack A.

14

L'unica cosa che potrebbe rendere un confronto di checksum leggermente più veloce di un confronto byte per byte è il fatto che stai leggendo un file alla volta, riducendo in qualche modo il tempo di ricerca per la testina del disco. Questo leggero guadagno può tuttavia essere ben consumato dal tempo aggiuntivo di calcolo dell'hash.

Inoltre, un confronto di checksum ovviamente ha solo la possibilità di essere più veloce se i file sono identici. In caso contrario, un confronto byte per byte terminerebbe alla prima differenza, rendendolo molto più veloce.

Dovresti anche considerare che un confronto del codice hash ti dice solo che è molto probabile che i file siano identici. Per essere certi al 100% devi fare un confronto byte per byte.

Se il codice hash, ad esempio, è di 32 bit, sei sicuro al 99.99999998% che i file sono identici se i codici hash corrispondono. Questo è vicino al 100%, ma se hai davvero bisogno del 100% di certezza, non è così.


Usa un hash più grande e puoi ottenere le probabilità di un falso positivo ben al di sotto delle probabilità che il computer ha commesso durante il test.
Loren Pechtel,

Non sono d'accordo sul tempo di hash vs tempo di ricerca. Puoi fare molti calcoli durante una singola ricerca della testa. Se le probabilità sono alte che i file corrispondano userei un hash con molti bit. Se c'è una ragionevole possibilità di una partita, li confronterei un blocco alla volta, per esempio blocchi da 1 MB. (Scegli una dimensione del blocco che 4k divide uniformemente per assicurarti di non dividere mai i settori.)
Loren Pechtel,

1
Per spiegare la cifra del 99,9999998% di @ Guffa, deriva dall'informatica 1 - (1 / (2^32)), che è la probabilità che ogni singolo file abbia un dato hash a 32 bit. La probabilità che due file diversi abbiano lo stesso hash è la stessa, perché il primo file fornisce il valore hash "dato" e dobbiamo solo considerare se l'altro file corrisponde a quel valore. Le probabilità con hashing a 64 e 128 bit diminuiscono a 99.999999999999999994% e 99.99999999999999999999999999999999999999997% (rispettivamente), come se ciò contasse con tali numeri insondabili.
Glenn Slayden,

... In effetti, il fatto che questi numeri siano più difficili da comprendere per la maggior parte della nozione putativamente semplice, sebbene vera, di "infiniti file che si scontrano nello stesso codice hash" può spiegare perché gli umani sono irragionevolmente sospettosi di accettare l'hash-as- uguaglianza.
Glenn Slayden,

13

Modifica: questo metodo no funzionerebbe per il confronto di file binari!

In .NET 4.0, la Fileclasse ha i seguenti due nuovi metodi:

public static IEnumerable<string> ReadLines(string path)
public static IEnumerable<string> ReadLines(string path, Encoding encoding)

Ciò significa che potresti usare:

bool same = File.ReadLines(path1).SequenceEqual(File.ReadLines(path2));

1
@dtb: non funziona per i file binari. Probabilmente stavi già scrivendo il commento quando me ne sono reso conto e ho aggiunto la modifica nella parte superiore del mio post. : o
Sam Harwell,

@ 280Z28: non ho detto nulla ;-)
dtb

Non avresti anche bisogno di memorizzare entrambi i file in memoria?
Casuale Insano,

Nota che File ha anche la funzione ReadAllBytes che può usare anche SequenceEquals, quindi usa quello invece che funzionerebbe su tutti i file. E come ha detto @RandomInsano, questo è archiviato in memoria, quindi mentre è perfettamente perfetto da usare per file di piccole dimensioni, starei attento a usarlo con file di grandi dimensioni.
DedaloAlpha,

1
@DaedalusAlpha Restituisce un enumerabile, quindi le righe verranno caricate su richiesta e non verranno archiviate in memoria per tutto il tempo. ReadAllBytes, d'altra parte, restituisce l'intero file come un array.
IllidanS4 vuole che Monica ritorni il

7

Onestamente, penso che tu debba potare il più possibile il tuo albero di ricerca.

Cose da controllare prima di passare byte per byte:

  1. Le dimensioni sono uguali?
  2. L'ultimo byte nel file A è diverso dal file B

Inoltre, leggere blocchi di grandi dimensioni alla volta sarà più efficiente poiché le unità leggono i byte sequenziali più rapidamente. L'andare byte per byte provoca non solo molte più chiamate di sistema, ma fa sì che la testina di lettura di un disco rigido tradizionale cerchi avanti e indietro più spesso se entrambi i file si trovano sulla stessa unità.

Leggi il blocco A e il blocco B in un buffer di byte e confrontali (NON utilizzare Array.Equals, vedi commenti). Ottimizza la dimensione dei blocchi fino a quando non colpisci quello che ritieni sia un buon compromesso tra memoria e prestazioni. Puoi anche eseguire il multi-thread del confronto, ma non multi-thread le letture del disco.


L'uso di Array.Equals è una cattiva idea perché confronta l'intero array. È probabile che almeno un blocco letto non riempia l'intero array.
Doug Clutter,

Perché confrontare l'intera matrice è una cattiva idea? Perché un blocco letto non riempirebbe l'array? C'è sicuramente un buon punto di regolazione, ma è per questo che giochi con le dimensioni. Punti extra per fare il confronto in un thread separato.
RandomInsano,

Quando si definisce un array di byte, avrà una lunghezza fissa. (es. - var buffer = new byte [4096]) Quando si legge un blocco dal file, è possibile che venga restituito l'intero 4096 byte. Ad esempio, se il file è lungo solo 3000 byte.
Doug Clutter,

Ah, ora capisco! La buona notizia è che la lettura restituirà il numero di byte caricati nell'array, quindi se l'array non può essere riempito, ci saranno dati. Poiché stiamo testando l'uguaglianza, i vecchi dati del buffer non avranno importanza. Documenti: msdn.microsoft.com/en-us/library/9kstw824(v=vs.110).aspx
RandomInsano

Anche importante, la mia raccomandazione di usare il metodo Equals () è una cattiva idea. In Mono, fanno un confronto della memoria poiché gli elementi sono contigui nella memoria. Microsoft tuttavia non lo ignora, invece fa solo un confronto di riferimento che qui sarebbe sempre falso.
RandomInsano

4

La mia risposta è una derivata di @lars ma risolve il bug nella chiamata a Stream.Read. Aggiungo anche un po 'di percorso rapido verificando che avevano altre risposte e immetto la convalida. In breve, questa dovrebbe essere la risposta:

using System;
using System.IO;

namespace ConsoleApp4
{
    class Program
    {
        static void Main(string[] args)
        {
            var fi1 = new FileInfo(args[0]);
            var fi2 = new FileInfo(args[1]);
            Console.WriteLine(FilesContentsAreEqual(fi1, fi2));
        }

        public static bool FilesContentsAreEqual(FileInfo fileInfo1, FileInfo fileInfo2)
        {
            if (fileInfo1 == null)
            {
                throw new ArgumentNullException(nameof(fileInfo1));
            }

            if (fileInfo2 == null)
            {
                throw new ArgumentNullException(nameof(fileInfo2));
            }

            if (string.Equals(fileInfo1.FullName, fileInfo2.FullName, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }

            if (fileInfo1.Length != fileInfo2.Length)
            {
                return false;
            }
            else
            {
                using (var file1 = fileInfo1.OpenRead())
                {
                    using (var file2 = fileInfo2.OpenRead())
                    {
                        return StreamsContentsAreEqual(file1, file2);
                    }
                }
            }
        }

        private static int ReadFullBuffer(Stream stream, byte[] buffer)
        {
            int bytesRead = 0;
            while (bytesRead < buffer.Length)
            {
                int read = stream.Read(buffer, bytesRead, buffer.Length - bytesRead);
                if (read == 0)
                {
                    // Reached end of stream.
                    return bytesRead;
                }

                bytesRead += read;
            }

            return bytesRead;
        }

        private static bool StreamsContentsAreEqual(Stream stream1, Stream stream2)
        {
            const int bufferSize = 1024 * sizeof(Int64);
            var buffer1 = new byte[bufferSize];
            var buffer2 = new byte[bufferSize];

            while (true)
            {
                int count1 = ReadFullBuffer(stream1, buffer1);
                int count2 = ReadFullBuffer(stream2, buffer2);

                if (count1 != count2)
                {
                    return false;
                }

                if (count1 == 0)
                {
                    return true;
                }

                int iterations = (int)Math.Ceiling((double)count1 / sizeof(Int64));
                for (int i = 0; i < iterations; i++)
                {
                    if (BitConverter.ToInt64(buffer1, i * sizeof(Int64)) != BitConverter.ToInt64(buffer2, i * sizeof(Int64)))
                    {
                        return false;
                    }
                }
            }
        }
    }
}

O se vuoi essere super-fantastico, puoi usare la variante asincrona:

using System;
using System.IO;
using System.Threading.Tasks;

namespace ConsoleApp4
{
    class Program
    {
        static void Main(string[] args)
        {
            var fi1 = new FileInfo(args[0]);
            var fi2 = new FileInfo(args[1]);
            Console.WriteLine(FilesContentsAreEqualAsync(fi1, fi2).GetAwaiter().GetResult());
        }

        public static async Task<bool> FilesContentsAreEqualAsync(FileInfo fileInfo1, FileInfo fileInfo2)
        {
            if (fileInfo1 == null)
            {
                throw new ArgumentNullException(nameof(fileInfo1));
            }

            if (fileInfo2 == null)
            {
                throw new ArgumentNullException(nameof(fileInfo2));
            }

            if (string.Equals(fileInfo1.FullName, fileInfo2.FullName, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }

            if (fileInfo1.Length != fileInfo2.Length)
            {
                return false;
            }
            else
            {
                using (var file1 = fileInfo1.OpenRead())
                {
                    using (var file2 = fileInfo2.OpenRead())
                    {
                        return await StreamsContentsAreEqualAsync(file1, file2).ConfigureAwait(false);
                    }
                }
            }
        }

        private static async Task<int> ReadFullBufferAsync(Stream stream, byte[] buffer)
        {
            int bytesRead = 0;
            while (bytesRead < buffer.Length)
            {
                int read = await stream.ReadAsync(buffer, bytesRead, buffer.Length - bytesRead).ConfigureAwait(false);
                if (read == 0)
                {
                    // Reached end of stream.
                    return bytesRead;
                }

                bytesRead += read;
            }

            return bytesRead;
        }

        private static async Task<bool> StreamsContentsAreEqualAsync(Stream stream1, Stream stream2)
        {
            const int bufferSize = 1024 * sizeof(Int64);
            var buffer1 = new byte[bufferSize];
            var buffer2 = new byte[bufferSize];

            while (true)
            {
                int count1 = await ReadFullBufferAsync(stream1, buffer1).ConfigureAwait(false);
                int count2 = await ReadFullBufferAsync(stream2, buffer2).ConfigureAwait(false);

                if (count1 != count2)
                {
                    return false;
                }

                if (count1 == 0)
                {
                    return true;
                }

                int iterations = (int)Math.Ceiling((double)count1 / sizeof(Int64));
                for (int i = 0; i < iterations; i++)
                {
                    if (BitConverter.ToInt64(buffer1, i * sizeof(Int64)) != BitConverter.ToInt64(buffer2, i * sizeof(Int64)))
                    {
                        return false;
                    }
                }
            }
        }
    }
}

il bit di bitconverter non sarebbe migliore di `` `per (var i = 0; i <count; i + = sizeof (long)) {if (BitConverter.ToInt64 (buffer1, i)! = BitConverter.ToInt64 (buffer2, i)) {return false; }} ``
Simon

2

I miei esperimenti dimostrano che aiuta sicuramente a chiamare Stream.ReadByte () meno volte, ma l'uso di BitConverter per pacchetto di byte non fa molta differenza rispetto al confronto di byte in un array di byte.

Quindi è possibile sostituire quel ciclo "Math.Ceiling and iterations" nel commento sopra con il più semplice:

            for (int i = 0; i < count1; i++)
            {
                if (buffer1[i] != buffer2[i])
                    return false;
            }

Immagino che abbia a che fare con il fatto che BitConverter.ToInt64 ha bisogno di fare un po 'di lavoro (controlla gli argomenti e quindi esegui lo spostamento dei bit) prima di confrontare e che finisce per essere la stessa quantità di lavoro di confrontare 8 byte in due array .


1
Array.Equals approfondisce il sistema, quindi sarà probabilmente molto più veloce di andare byte per byte in C #. Non posso parlare per Microsoft, ma in fondo Mono usa il comando memcpy () di C per l'uguaglianza dell'array. Non posso andare molto più veloce di così.
Casuale Insano

2
@RandomInsano indovina che intendi memcmp (), non memcpy ()
SQL Police

1

Se i file non sono troppo grandi, puoi usare:

public static byte[] ComputeFileHash(string fileName)
{
    using (var stream = File.OpenRead(fileName))
        return System.Security.Cryptography.MD5.Create().ComputeHash(stream);
}

Sarà possibile confrontare gli hash solo se gli hash sono utili da memorizzare.

(Modificato il codice su qualcosa di molto più pulito.)


1

Un altro miglioramento su file di grandi dimensioni con identica lunghezza potrebbe essere quello di non leggere i file in sequenza, ma piuttosto di confrontare blocchi più o meno casuali.

È possibile utilizzare più thread, iniziando da diverse posizioni nel file e confrontando avanti o indietro.

In questo modo è possibile rilevare le modifiche a metà / fine del file, più velocemente di quanto ci si aspetterebbe utilizzando un approccio sequenziale.


1
Il blocco del disco causerebbe problemi qui?
Casuale Insano

Unità disco fisiche sì, le SSD gestiranno questo.
TheLegendaryCopyCoder

1

Se hai solo bisogno di confrontare due file, immagino che il modo più veloce sarebbe (in C, non so se sia applicabile a .NET)

  1. aprire entrambi i file f1, f2
  2. ottenere la rispettiva lunghezza del file l1, l2
  3. se l1! = l2 i file sono diversi; fermare
  4. mmap () entrambi i file
  5. usa memcmp () sui file ed mmap ()

OTOH, se hai bisogno di scoprire se ci sono file duplicati in un set di N file, il modo più veloce è senza dubbio l'uso di un hash per evitare confronti N-way bit per bit.


1

Qualcosa (si spera) ragionevolmente efficiente:

public class FileCompare
{
    public static bool FilesEqual(string fileName1, string fileName2)
    {
        return FilesEqual(new FileInfo(fileName1), new FileInfo(fileName2));
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="file1"></param>
    /// <param name="file2"></param>
    /// <param name="bufferSize">8kb seemed like a good default</param>
    /// <returns></returns>
    public static bool FilesEqual(FileInfo file1, FileInfo file2, int bufferSize = 8192)
    {
        if (!file1.Exists || !file2.Exists || file1.Length != file2.Length) return false;

        var buffer1 = new byte[bufferSize];
        var buffer2 = new byte[bufferSize];

        using (var stream1 = file1.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
        {
            using (var stream2 = file2.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
            {

                while (true)
                {
                    var bytesRead1 = stream1.Read(buffer1, 0, bufferSize);
                    var bytesRead2 = stream2.Read(buffer2, 0, bufferSize);

                    if (bytesRead1 != bytesRead2) return false;
                    if (bytesRead1 == 0) return true;
                    if (!ArraysEqual(buffer1, buffer2, bytesRead1)) return false;
                }
            }
        }
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="array1"></param>
    /// <param name="array2"></param>
    /// <param name="bytesToCompare"> 0 means compare entire arrays</param>
    /// <returns></returns>
    public static bool ArraysEqual(byte[] array1, byte[] array2, int bytesToCompare = 0)
    {
        if (array1.Length != array2.Length) return false;

        var length = (bytesToCompare == 0) ? array1.Length : bytesToCompare;
        var tailIdx = length - length % sizeof(Int64);

        //check in 8 byte chunks
        for (var i = 0; i < tailIdx; i += sizeof(Int64))
        {
            if (BitConverter.ToInt64(array1, i) != BitConverter.ToInt64(array2, i)) return false;
        }

        //check the remainder of the array, always shorter than 8 bytes
        for (var i = tailIdx; i < length; i++)
        {
            if (array1[i] != array2[i]) return false;
        }

        return true;
    }
}

1

Ecco alcune funzioni di utilità che consentono di determinare se due file (o due flussi) contengono dati identici.

Ho fornito una versione "veloce" che è multi-thread in quanto confronta array di byte (ogni buffer riempito da ciò che è stato letto in ciascun file) in thread diversi usando Attività.

Come previsto, è molto più veloce (circa 3 volte più veloce) ma consuma più CPU (perché è multi-thread) e più memoria (perché ha bisogno di buffer di array a due byte per thread di confronto).

    public static bool AreFilesIdenticalFast(string path1, string path2)
    {
        return AreFilesIdentical(path1, path2, AreStreamsIdenticalFast);
    }

    public static bool AreFilesIdentical(string path1, string path2)
    {
        return AreFilesIdentical(path1, path2, AreStreamsIdentical);
    }

    public static bool AreFilesIdentical(string path1, string path2, Func<Stream, Stream, bool> areStreamsIdentical)
    {
        if (path1 == null)
            throw new ArgumentNullException(nameof(path1));

        if (path2 == null)
            throw new ArgumentNullException(nameof(path2));

        if (areStreamsIdentical == null)
            throw new ArgumentNullException(nameof(path2));

        if (!File.Exists(path1) || !File.Exists(path2))
            return false;

        using (var thisFile = new FileStream(path1, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
        {
            using (var valueFile = new FileStream(path2, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                if (valueFile.Length != thisFile.Length)
                    return false;

                if (!areStreamsIdentical(thisFile, valueFile))
                    return false;
            }
        }
        return true;
    }

    public static bool AreStreamsIdenticalFast(Stream stream1, Stream stream2)
    {
        if (stream1 == null)
            throw new ArgumentNullException(nameof(stream1));

        if (stream2 == null)
            throw new ArgumentNullException(nameof(stream2));

        const int bufsize = 80000; // 80000 is below LOH (85000)

        var tasks = new List<Task<bool>>();
        do
        {
            // consumes more memory (two buffers for each tasks)
            var buffer1 = new byte[bufsize];
            var buffer2 = new byte[bufsize];

            int read1 = stream1.Read(buffer1, 0, buffer1.Length);
            if (read1 == 0)
            {
                int read3 = stream2.Read(buffer2, 0, 1);
                if (read3 != 0) // not eof
                    return false;

                break;
            }

            // both stream read could return different counts
            int read2 = 0;
            do
            {
                int read3 = stream2.Read(buffer2, read2, read1 - read2);
                if (read3 == 0)
                    return false;

                read2 += read3;
            }
            while (read2 < read1);

            // consumes more cpu
            var task = Task.Run(() =>
            {
                return IsSame(buffer1, buffer2);
            });
            tasks.Add(task);
        }
        while (true);

        Task.WaitAll(tasks.ToArray());
        return !tasks.Any(t => !t.Result);
    }

    public static bool AreStreamsIdentical(Stream stream1, Stream stream2)
    {
        if (stream1 == null)
            throw new ArgumentNullException(nameof(stream1));

        if (stream2 == null)
            throw new ArgumentNullException(nameof(stream2));

        const int bufsize = 80000; // 80000 is below LOH (85000)
        var buffer1 = new byte[bufsize];
        var buffer2 = new byte[bufsize];

        var tasks = new List<Task<bool>>();
        do
        {
            int read1 = stream1.Read(buffer1, 0, buffer1.Length);
            if (read1 == 0)
                return stream2.Read(buffer2, 0, 1) == 0; // check not eof

            // both stream read could return different counts
            int read2 = 0;
            do
            {
                int read3 = stream2.Read(buffer2, read2, read1 - read2);
                if (read3 == 0)
                    return false;

                read2 += read3;
            }
            while (read2 < read1);

            if (!IsSame(buffer1, buffer2))
                return false;
        }
        while (true);
    }

    public static bool IsSame(byte[] bytes1, byte[] bytes2)
    {
        if (bytes1 == null)
            throw new ArgumentNullException(nameof(bytes1));

        if (bytes2 == null)
            throw new ArgumentNullException(nameof(bytes2));

        if (bytes1.Length != bytes2.Length)
            return false;

        for (int i = 0; i < bytes1.Length; i++)
        {
            if (bytes1[i] != bytes2[i])
                return false;
        }
        return true;
    }

0

Penso che ci siano applicazioni in cui "l'hash" è più veloce del confronto byte per byte. Se devi confrontare un file con altri o avere un'anteprima di una foto che può cambiare. Dipende da dove e come sta usando.

private bool CompareFilesByte(string file1, string file2)
{
    using (var fs1 = new FileStream(file1, FileMode.Open))
    using (var fs2 = new FileStream(file2, FileMode.Open))
    {
        if (fs1.Length != fs2.Length) return false;
        int b1, b2;
        do
        {
            b1 = fs1.ReadByte();
            b2 = fs2.ReadByte();
            if (b1 != b2 || b1 < 0) return false;
        }
        while (b1 >= 0);
    }
    return true;
}

private string HashFile(string file)
{
    using (var fs = new FileStream(file, FileMode.Open))
    using (var reader = new BinaryReader(fs))
    {
        var hash = new SHA512CryptoServiceProvider();
        hash.ComputeHash(reader.ReadBytes((int)file.Length));
        return Convert.ToBase64String(hash.Hash);
    }
}

private bool CompareFilesWithHash(string file1, string file2)
{
    var str1 = HashFile(file1);
    var str2 = HashFile(file2);
    return str1 == str2;
}

Qui puoi ottenere qual è il più veloce.

var sw = new Stopwatch();
sw.Start();
var compare1 = CompareFilesWithHash(receiveLogPath, logPath);
sw.Stop();
Debug.WriteLine(string.Format("Compare using Hash {0}", sw.ElapsedTicks));
sw.Reset();
sw.Start();
var compare2 = CompareFilesByte(receiveLogPath, logPath);
sw.Stop();
Debug.WriteLine(string.Format("Compare byte-byte {0}", sw.ElapsedTicks));

Facoltativamente, possiamo salvare l'hash in un database.

Spero che questo possa aiutare


0

Ancora un'altra risposta, derivata da @chsh. MD5 con utilizzi e collegamenti per lo stesso file, il file non esiste e le diverse lunghezze:

/// <summary>
/// Performs an md5 on the content of both files and returns true if
/// they match
/// </summary>
/// <param name="file1">first file</param>
/// <param name="file2">second file</param>
/// <returns>true if the contents of the two files is the same, false otherwise</returns>
public static bool IsSameContent(string file1, string file2)
{
    if (file1 == file2)
        return true;

    FileInfo file1Info = new FileInfo(file1);
    FileInfo file2Info = new FileInfo(file2);

    if (!file1Info.Exists && !file2Info.Exists)
       return true;
    if (!file1Info.Exists && file2Info.Exists)
        return false;
    if (file1Info.Exists && !file2Info.Exists)
        return false;
    if (file1Info.Length != file2Info.Length)
        return false;

    using (FileStream file1Stream = file1Info.OpenRead())
    using (FileStream file2Stream = file2Info.OpenRead())
    { 
        byte[] firstHash = MD5.Create().ComputeHash(file1Stream);
        byte[] secondHash = MD5.Create().ComputeHash(file2Stream);
        for (int i = 0; i < firstHash.Length; i++)
        {
            if (i>=secondHash.Length||firstHash[i] != secondHash[i])
                return false;
        }
        return true;
    }
}

Tu dici if (i>=secondHash.Length ...In quali circostanze sarebbe due MD5 hash essere diverse lunghezze?
frogpelt,

-1

Ho scoperto che funziona bene confrontando prima la lunghezza senza leggere i dati e quindi confrontando la sequenza di byte letti

private static bool IsFileIdentical(string a, string b)
{            
   if (new FileInfo(a).Length != new FileInfo(b).Length) return false;
   return (File.ReadAllBytes(a).SequenceEqual(File.ReadAllBytes(b)));
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.