Como comparar 2 arquivos rapidamente usando o .NET?


Respostas:


117

Uma comparação de soma de verificação provavelmente será mais lenta que uma comparação de byte a byte.

Para gerar uma soma de verificação, você precisará carregar cada byte do arquivo e executar o processamento nele. Você precisará fazer isso no segundo arquivo. Definitivamente, o processamento será mais lento que a verificação de comparação.

Quanto à geração de uma soma de verificação: você pode fazer isso facilmente com as classes de criptografia. Aqui está um pequeno exemplo de como gerar uma soma de verificação MD5 com C #.

No entanto, uma soma de verificação pode ser mais rápida e fazer mais sentido se você puder pré-calcular a soma de verificação do caso "teste" ou "base". Se você possui um arquivo existente e está verificando se um novo arquivo é igual ao existente, pré-calcular a soma de verificação no arquivo "existente" significaria apenas a necessidade de fazer o DiskIO uma vez, no diretório novo arquivo. Isso provavelmente seria mais rápido que uma comparação de byte a byte.


30
Certifique-se de levar em consideração onde seus arquivos estão localizados. Se você estiver comparando arquivos locais a um backup do outro lado do mundo (ou em uma rede com largura de banda horrível), é melhor fazer o hash primeiro e enviar uma soma de verificação pela rede em vez de enviar um fluxo de bytes para comparar.
Kim

@ReedCopsey: Estou tendo um problema semelhante, pois preciso armazenar arquivos de entrada / saída produzidos por várias elaborações que devem conter muitas duplicações. Eu pensei em usar o hash pré-computado, mas você acha que posso razoavelmente supor que se o hash 2 (por exemplo, MD5) for igual, os 2 arquivos serão iguais e evitaremos uma comparação mais byte-2-byte? Até onde eu sei, colisões MD5 / SHA1 etc são realmente improváveis ​​...
digEmAll

1
@digEmAll A chance de colisão é baixa - no entanto, você sempre pode fazer um hash mais forte - por exemplo: use SHA256 em vez de SHA1, o que reduzirá ainda mais a probabilidade de colisões.
Reed Copsey

obrigado pela sua resposta - estou entrando no .net. Estou assumindo que, se alguém estiver usando a técnica de hashcode / check sum, os hashes da pasta principal serão armazenados persistentemente em algum lugar? por curiosidade, como você o armazenaria para um aplicativo WPF - o que você faria? (Atualmente, estou olhando xml, arquivos de texto ou bancos de dados).
BKSpurgeon

139

O método mais lento possível é comparar dois arquivos byte a byte. O mais rápido que consegui fazer é uma comparação semelhante, mas, em vez de um byte por vez, você usaria uma matriz de bytes dimensionada para Int64 e, em seguida, compararia os números resultantes.

Aqui está o que eu vim com:

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

Nos meus testes, eu pude ver isso superar um cenário simples de ReadByte () em quase 3: 1. Em média, mais de 1000 execuções, obtive esse método em 1063ms e o método abaixo (comparação direta de bytes por byte) em 3031ms. O hash sempre voltava em um segundo em torno de uma média de 865ms. Este teste foi realizado com um arquivo de vídeo de ~ 100 MB.

Aqui estão os métodos ReadByte e hash que eu usei, para fins de comparação:

    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
Você facilitou minha vida. Obrigado
anindis

2
@ anindis: Para ser completo, você pode ler as respostas de @Lars e @ de RandomInsano . Ainda bem que ajudou tantos anos! :)
chsh

1
O FilesAreEqual_Hashmétodo deve ter um usingnos dois fluxos de arquivos também como o ReadBytemétodo, caso contrário, ele permanecerá nos dois arquivos.
Ian Mercer

2
Observe que, FileStream.Read()na verdade, pode ler menos bytes que o número solicitado. Você deve usar em seu StreamReader.ReadBlock()lugar.
Palec

2
Na versão Int64, quando o comprimento do fluxo não é múltiplo do Int64, a última iteração está comparando os bytes não preenchidos usando o preenchimento da iteração anterior (que também deve ser igual, para que fique bem). Além disso, se o comprimento do fluxo for menor que sizeof (Int64), os bytes não preenchidos serão 0, pois o C # inicializa matrizes. IMO, o código provavelmente deve comentar essas esquisitices.
crokusek

46

Se você não decidir você realmente precisa de uma comparação completa byte a byte (veja outras respostas para a discussão de hashing), então a solução mais fácil é:


• para System.IO.FileInfoinstâncias:

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


• para System.Stringnomes de caminhos:

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


Diferentemente de outras respostas publicadas, isso é conclusivamente correto para qualquer tipo de arquivo: binário, texto, mídia, executável etc., mas como uma comparação binária completa , arquivos que diferem apenas de maneiras "sem importância" (como BOM , linha -ending , codificação de caracteres , metadados de mídia, espaço em branco, estofamento, comentários de código-fonte, etc.) será sempre considerado não-igual .

Esse código carrega os dois arquivos inteiramente na memória, portanto, não deve ser usado para comparar arquivos verdadeiramente gigantescos . Além dessa ressalva importante, o carregamento completo não é realmente uma penalidade, devido ao design do .NET GC (porque é fundamentalmente otimizado para manter alocações pequenas e de curta duração extremamente baratas ) e, de fato, pode ser ideal quando os tamanhos de arquivo são esperados a ser menos do que 85K , porque usar um mínimo de código de utilizador (como mostrado aqui) implica maximamente delegar problemas de desempenho arquivo para o CLR, BCLe JITpara benefício de (por exemplo) o mais recente projeto, código do sistema, e otimizações de tempo de execução adaptativos.

Além disso, para esses cenários de trabalho, preocupações com o desempenho da comparação de byte a byte através de LINQenumeradores (como mostrado aqui) são discutíveis, já que bater no disco a̲t̲ a̲l̲l̲ para E / S de arquivo diminui, por várias ordens de magnitude, os benefícios das várias alternativas de comparação de memória. Por exemplo, embora SequenceEqual realmente nos dê a "otimização" de abandonar na primeira incompatibilidade , isso dificilmente importa depois de já ter buscado o conteúdo dos arquivos, cada um totalmente necessário para confirmar a correspondência.


3
este não parece bom para arquivos grandes. não é bom para o uso da memória, pois ele lerá os dois arquivos até o final antes de começar a comparar a matriz de bytes. É por isso que eu prefiro usar um leitor de stream com um buffer.
Krypto_47

3
@ Krypto_47 Eu discuti esses fatores e o uso apropriado no texto da minha resposta.
Glenn Slayden

33

Além da resposta de Reed Copsey :

  • O pior caso é onde os dois arquivos são idênticos. Nesse caso, é melhor comparar os arquivos byte a byte.

  • Se os dois arquivos não forem idênticos, você poderá acelerar um pouco as coisas, detectando antes que eles não são idênticos.

Por exemplo, se os dois arquivos tiverem tamanho diferente, você saberá que eles não podem ser idênticos e nem precisará comparar o conteúdo real.


10
Para ser concluído: o outro grande ganho será interrompido assim que os bytes na posição 1 forem diferentes.
Henk Holterman

6
@Henk: Eu achava que isso era muito óbvio :-)
dtb

1
Bom ponto de adicionar isso. Era óbvio para mim, então não o incluí, mas é bom mencionar.
Reed Copsey

16

Está ficando ainda mais rápido se você não ler em pequenos pedaços de 8 bytes, mas fizer um loop, lendo um pedaço maior. Reduzi o tempo médio de comparação para 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
Em geral, a verificação count1 != count2não está correta. Stream.Read()pode retornar menos do que a contagem que você forneceu, por vários motivos.
PORGES

1
Para garantir que o buffer irá realizar um número par de Int64blocos, você pode querer calcular o tamanho da seguinte forma: const int bufferSize = 1024 * sizeof(Int64).
Jack A.

14

A única coisa que pode fazer uma comparação de soma de verificação um pouco mais rápida que uma comparação de byte a byte é o fato de você estar lendo um arquivo de cada vez, reduzindo um pouco o tempo de busca da cabeça do disco. Esse pequeno ganho pode, no entanto, muito bem ser consumido pelo tempo adicional de cálculo do hash.

Além disso, é claro que uma comparação de soma de verificação só tem chance de ser mais rápida se os arquivos forem idênticos. Caso contrário, uma comparação de byte a byte terminaria na primeira diferença, tornando-a muito mais rápida.

Você também deve considerar que uma comparação de código de hash apenas informa que é muito provável que os arquivos sejam idênticos. Para ter 100% de certeza, você precisa fazer uma comparação byte a byte.

Se o código de hash, por exemplo, for de 32 bits, você terá 99,99999998% de certeza de que os arquivos serão idênticos se os códigos de hash corresponderem. Isso é quase 100%, mas se você realmente precisa de 100% de certeza, não é isso.


Use um hash maior e você poderá obter as chances de um falso positivo muito abaixo das chances que o computador errou ao fazer o teste.
Loren Pechtel

Não concordo com o tempo de hash vs o tempo de busca. Você pode fazer muitos cálculos durante uma única busca principal. Se as chances de os arquivos coincidirem, eu usaria um hash com muitos bits. Se houver uma chance razoável de uma partida, eu os compararia um bloco de cada vez, por exemplo, blocos de 1 MB. (Escolha um tamanho de bloco que 4k divida igualmente para garantir que você nunca divida setores.)
Loren Pechtel

1
Para explicar a figura de 99,999999998% de @ Guffa, é proveniente da computação 1 - (1 / (2^32)), que é a probabilidade de que um único arquivo tenha um hash de 32 bits. A probabilidade de dois arquivos diferentes terem o mesmo hash é a mesma, porque o primeiro arquivo fornece o valor de hash "fornecido" e precisamos considerar apenas se o outro arquivo corresponde ou não a esse valor. As chances de hash de 64 e 128 bits diminuem para 99,999999999999999994% e 99,99999999999999999999999999999999997% (respectivamente), como se isso importasse com esses números insondáveis.
Glenn Slayden 19/03/19

... De fato, o fato de que esses números são mais difíceis de entender para a maioria das pessoas do que a noção supostamente simples, embora verdadeira, de "infinitamente muitos arquivos colidindo no mesmo código de hash" pode explicar por que os humanos são irracionalmente suspeitos de aceitar hash-as- igualdade.
Glenn Slayden

13

Edit: Este método não funcionaria para comparar arquivos binários!

No .NET 4.0, a Fileclasse possui os dois novos métodos a seguir:

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

O que significa que você pode usar:

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

1
@ dtb: Não funciona para arquivos binários. Você provavelmente já estava digitando o comentário quando percebi isso e adicionei a edição na parte superior da minha postagem. : o
Sam Harwell

@ 280Z28: Eu não disse nada ;-)
dtb

Você também não precisaria armazenar os dois arquivos na memória?
RandomInsano 27/01

Observe que File também possui a função ReadAllBytes, que também pode usar SequenceEquals; portanto, use-a, pois funcionaria em todos os arquivos. E como o @RandomInsano disse, isso é armazenado na memória, portanto, embora seja perfeitamente bom usá-lo para arquivos pequenos, eu seria cuidadoso ao usá-lo com arquivos grandes.
DaedalusAlpha

1
@DaedalusAlpha Retorna um enumerável, para que as linhas sejam carregadas sob demanda e não armazenadas na memória o tempo todo. ReadAllBytes, por outro lado, retorna o arquivo inteiro como uma matriz.
IllidanS4 quer Monica de volta 17/03/19

7

Honestamente, acho que você precisa reduzir sua árvore de pesquisa o máximo possível.

Itens a serem verificados antes de seguir byte a byte:

  1. Os tamanhos são iguais?
  2. O último byte no arquivo A é diferente do arquivo B

Além disso, a leitura de grandes blocos de cada vez será mais eficiente, pois as unidades leem bytes seqüenciais mais rapidamente. Passar byte a byte causa não apenas muito mais chamadas do sistema, como também faz com que o cabeçote de leitura de um disco rígido tradicional procure e volte com mais frequência se os dois arquivos estiverem na mesma unidade.

Leia o fragmento A e o fragmento B em um buffer de bytes e compare-os (NÃO use Array.Equals, consulte comentários). Ajuste o tamanho dos blocos até atingir o que você acha que é uma boa troca entre memória e desempenho. Você também pode multi-thread a comparação, mas não multi-thread o disco lê.


Usar Array.Equals é uma péssima idéia, pois compara toda a matriz. É provável que pelo menos uma leitura de bloco não preencha toda a matriz.
precisa saber é o seguinte

Por que comparar toda a matriz é uma má ideia? Por que uma leitura de bloco não preenche a matriz? Definitivamente, há um bom ponto de sintonia, mas é por isso que você brinca com os tamanhos. Pontos extras para fazer a comparação em um thread separado.
RandomInsano

Quando você define uma matriz de bytes, ela terá um comprimento fixo. (por exemplo, - var buffer = novo byte [4096]) Quando você lê um bloco do arquivo, ele pode ou não retornar os 4096 bytes completos. Por exemplo, se o arquivo tiver apenas 3000 bytes.
precisa

Ah, agora eu entendo! A boa notícia é que a leitura retornará o número de bytes carregados na matriz; portanto, se a matriz não puder ser preenchida, haverá dados. Como estamos testando a igualdade, os dados antigos do buffer não importam. Docs: msdn.microsoft.com/en-us/library/9kstw824(v=vs.110).aspx
RandomInsano

Também importante, minha recomendação de usar o método Equals () é uma má idéia. Em Mono, eles fazem uma comparação de memória, pois os elementos são contíguos na memória. A Microsoft, no entanto, não a substitui, apenas faz uma comparação de referência que aqui sempre seria falsa.
RandomInsano

4

Minha resposta é derivada de @lars, mas corrige o erro na chamada para Stream.Read. Também adiciono algumas verificações rápidas que outras respostas tiveram e validação de entrada. Em suma, esta deve ser a resposta:

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

Ou, se você quiser ser super incrível, use a variante assíncrona:

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

não seria o bit do conversor de bits melhor que `` for (var i = 0; i <count; i + = sizeof (long)) {if (BitConverter.ToInt64 (buffer1, i)! = BitConverter.ToInt64 (buffer2, i)) { retorna falso; }} `` `
Simon

2

Minhas experiências mostram que definitivamente ajuda a chamar Stream.ReadByte () menos vezes, mas usar o BitConverter para empacotar bytes não faz muita diferença em comparação à comparação de bytes em uma matriz de bytes.

Portanto, é possível substituir o loop "Math.Ceiling e iterações" no comentário acima pelo mais simples:

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

Eu acho que isso tem a ver com o fato de que o BitConverter.ToInt64 precisa fazer um pouco de trabalho (verificar argumentos e depois executar a troca de bits) antes de comparar e isso acaba sendo a mesma quantidade de trabalho que comparar 8 bytes em duas matrizes .


1
Array.Equals vai mais fundo no sistema, portanto, provavelmente será muito mais rápido que passar byte a byte em C #. Não posso falar pela Microsoft, mas, no fundo, o Mono usa o comando memcpy () de C para obter igualdade de matriz. Não pode ficar muito mais rápido que isso.
RandomInsano 27/01

2
@RandomInsano acho que você memcmp média (), não memcpy ()
A polícia SQL

1

Se os arquivos não forem muito grandes, você pode usar:

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

Só será possível comparar hashes se eles forem úteis para armazenar.

(Editou o código para algo muito mais limpo.)


1

Outra melhoria em arquivos grandes com comprimento idêntico pode ser não ler os arquivos sequencialmente, mas comparar blocos mais ou menos aleatórios.

Você pode usar vários threads, iniciando em diferentes posições no arquivo e comparando para frente ou para trás.

Dessa forma, você pode detectar alterações no meio / final do arquivo, mais rapidamente do que você chegaria lá usando uma abordagem seqüencial.


1
A trituração de disco causaria problemas aqui?
RandomInsano 27/01

Unidades de disco físicas sim, os SSDs resolveriam isso.
TheLegendaryCopyCoder

1

Se você precisar comparar apenas dois arquivos, acho que o caminho mais rápido seria (em C, não sei se é aplicável ao .NET)

  1. abra os dois arquivos f1, f2
  2. obter o tamanho do arquivo respectivo l1, l2
  3. se l1! = l2 os arquivos são diferentes; Pare
  4. mmap () ambos os arquivos
  5. use memcmp () nos arquivos mmap () ed

OTOH, se você precisar descobrir se há arquivos duplicados em um conjunto de N arquivos, o caminho mais rápido será, sem dúvida, usar um hash para evitar comparações N-bit bit a bit.


1

Algo (espero) razoavelmente eficiente:

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

Aqui estão algumas funções utilitárias que permitem determinar se dois arquivos (ou dois fluxos) contêm dados idênticos.

Forneci uma versão "rápida" com vários segmentos, pois compara matrizes de bytes (cada buffer preenchido com o que foi lido em cada arquivo) em diferentes segmentos usando Tarefas.

Como esperado, é muito mais rápido (cerca de 3x mais rápido), mas consome mais CPU (por ser multiencadeado) e mais memória (porque precisa de dois buffers de matriz de bytes por thread de comparação).

    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

Eu acho que existem aplicativos em que "hash" é mais rápido do que comparar byte por byte. Se você precisar comparar um arquivo com outras pessoas ou ter uma miniatura de uma foto que possa ser alterada. Depende de onde e como está sendo usado.

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

Aqui, você pode obter o que é mais rápido.

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

Opcionalmente, podemos salvar o hash em um banco de dados.

Espero que isso possa ajudar


0

Outra resposta, derivada de @chsh. MD5 com utilizações e atalhos para o mesmo arquivo, arquivo não existe e comprimentos diferentes:

/// <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;
    }
}

Você diz if (i>=secondHash.Length ...sob que circunstâncias dois hashes MD5 teriam comprimentos diferentes?
Frogpelt

-1

Descobri que funciona bem comparando primeiro o comprimento sem ler os dados e, em seguida, comparando a sequência de bytes lidos

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)));
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.