¿Cuál es la forma más rápida de crear una suma de verificación para archivos grandes en C #?

128

Tengo que sincronizar archivos grandes en algunas máquinas. Los archivos pueden tener hasta 6 GB de tamaño. La sincronización se realizará manualmente cada pocas semanas. No puedo tomar en cuenta el nombre del archivo porque pueden cambiar en cualquier momento.

Mi plan es crear sumas de verificación en la PC de destino y en la PC de origen y luego copiar todos los archivos con una suma de verificación, que aún no están en el destino, al destino. Mi primer intento fue algo como esto:

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

El problema fue el tiempo de ejecución:
- con SHA256 con un archivo de 1,6 GB -> 20 minutos
- con MD5 con un archivo de 1,6 GB -> 6,15 minutos

¿Existe una forma mejor, más rápida, de obtener la suma de comprobación (tal vez con una mejor función hash)?

crono
fuente
2
¿Realmente necesitas revisar la suma de control? ¿Cómo estás copiando los archivos? Si estás en Windows, usaría la última versión de Robocopy ...
Mesh
66
Buen consejo aquí para molestar solo el hash si los tamaños de archivo son diferentes entre 2 archivos candidatos stackoverflow.com/a/288756/74585
Matthew Lock

Respuestas:

117

El problema aquí es que SHA256Managedlee 4096 bytes a la vez (hereda FileStreamy anula Read(byte[], int, int)para ver cuánto lee del flujo de archivos), que es un búfer demasiado pequeño para el disco IO.

Para acelerar las cosas (2 minutos para hash de archivo de 2 GB en mi máquina con SHA256, 1 minuto para MD5) envoltura FileStreamen BufferedStreamy configurar el tamaño de búfer de tamaño razonable (probé con tampón ~ 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
fuente
3
OK, esto hizo la diferencia: el hash del archivo de 1.6GB con MD5 tomó 5.2 segundos en mi caja (QuadCode @ 2.6 GHz, 8GB Ram) - incluso más rápido que la implementación nativa ...
crono
44
no lo entiendo Acabo de probar esta sugerencia, pero la diferencia es mínima o nula. Archivo de 1024 MB sin almacenamiento en búfer de 12 a 14 segundos, con almacenamiento en búfer también de 12 a 14 segundos: entiendo que leer cientos de bloques de 4k producirá más E / S, pero me pregunto si el marco o las API nativas debajo del marco ya no manejan esto ..
Christian Casutt
11
Un poco tarde para la fiesta, pero para FileStreams ya no es necesario envolver la transmisión en un BufferedStream, ya que hoy en día ya se hace en FileStream. Fuente
Reyhn
Estaba pasando por este problema con archivos más pequeños (<10 MB, pero me llevó una eternidad obtener un MD5). Aunque uso .Net 4.5, cambiar a este método con BufferedStream redujo el tiempo de hash de aproximadamente 8.6 segundos a <300 ms para un archivo de
8.6MB
Usé un BufferedStream / w de 512 kB en lugar de 1024 kB. El archivo de 1.8 GB se resolvió en 30 segundos.
Hugo Woesthuis
61

No realice una suma de comprobación de todo el archivo, cree sumas de comprobación cada 100 MB aproximadamente, de modo que cada archivo tenga una colección de sumas de comprobación.

Luego, al comparar sumas de verificación, puede dejar de comparar después de la primera suma de verificación diferente, salir temprano y evitar que procese todo el archivo.

Todavía tomará el tiempo completo para archivos idénticos.

Binario Worrier
fuente
2
Me gusta la idea, pero no funcionará en mi escenario porque terminaré con muchos archivos sin cambios con el tiempo.
crono
1
¿Cómo se suma cada 100mb de un archivo?
Smith
1
No es una buena idea cuando se usa la suma de comprobación por razones de seguridad, porque el atacante puede cambiar los bytes que ha excluido.
b.kiener
2
+1 Esta es una excelente idea cuando realiza una comparación uno a uno. Desafortunadamente, estoy usando el hash MD5 como índice para buscar archivos únicos entre muchos duplicados (comprobaciones de muchos a muchos).
Nathan Goings
1
@ b.kiener No se excluye ningún byte. Lo malinterpretaste.
Soroush Falahati el
47

Como señaló Anton Gogolev , FileStream lee 4096 bytes a la vez de forma predeterminada, pero puede especificar cualquier otro valor utilizando el constructor FileStream:

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

Tenga en cuenta que Brad Abrams de Microsoft escribió en 2004:

no hay ningún beneficio en envolver un BufferedStream alrededor de un FileStream. Copiamos la lógica de almacenamiento en búfer de BufferedStream en FileStream hace aproximadamente 4 años para fomentar un mejor rendimiento predeterminado

fuente

Tal Aloni
fuente
22

Invoque el puerto de Windows de md5sum.exe . Es aproximadamente dos veces más rápido que la implementación de .NET (al menos en mi máquina usando un archivo de 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
fuente
3
WOW: el uso de md5sums.exe de pc-tools.net/win32/md5sums lo hace realmente rápido. 1681457152 bytes, 8672 ms = 184.91 MB / seg -> 1,6GB ~ 9 segundos Esto será lo suficientemente rápido para mi propósito.
crono
16

Ok, gracias a todos ustedes, déjenme terminar esto:

  1. El uso de un exe "nativo" para hacer el hashing tomó tiempo de 6 minutos a 10 segundos, lo cual es enorme.
  2. El aumento del búfer fue aún más rápido: el archivo de 1.6GB tardó 5.2 segundos usando MD5 en .Net, así que iré con esta solución, gracias de nuevo
crono
fuente
10

Hice pruebas con el tamaño del búfer, ejecuté este código

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

Y probé con un archivo de 29½ GB de tamaño, los resultados fueron

  • 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
  • Y 376,22s cuando se usa el código original, ninguno almacenado.

Estoy ejecutando una CPU i5 2500K, 12 GB de RAM y una unidad SSD OCZ Vertex 4 de 256 GB.

Entonces pensé, ¿qué pasa con un disco duro estándar de 2TB? Y los resultados fueron así

  • 10.000: 368,52s
  • 100.000: 364,15s
  • 1.000.000: 363,06s
  • 10.000.000: 678,96s
  • 100.000.000: 617,89s
  • 1.000.000.000: 626,86s
  • Y para ninguno amortiguado 368,24

Por lo tanto, recomendaría sin búfer o un búfer de máximo 1 mill.

Anders
fuente
No lo entiendo ¿Cómo puede esta prueba contradecir la respuesta aceptada de Anton Gogolev?
buddybubble
¿Puede agregar una descripción de cada campo en sus datos?
videoguy
2

Estás haciendo algo mal (probablemente un búfer de lectura demasiado pequeño). En una máquina de edad indecente (Athlon 2x1800MP de 2002) que tiene DMA en el disco probablemente fuera de control (6.6M / s es muy lento cuando se hacen lecturas secuenciales):

Cree un archivo 1G con datos "aleatorios":

# 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

1m5.299s

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

1m58.832s

Esto también es extraño, md5 es consistentemente más lento que sha1 para mí (reran varias veces).

Pasi Savolainen
fuente
Sí, intentaré aumentar el búfer, como sugirió Anton Gogolev. Lo ejecuté a través de un MD5.exe "nativo" que tardó 9 segundos con un archivo de 1,6 GB.
crono
2

Sé que llego tarde a la fiesta pero realicé una prueba antes de implementar la solución.

Realicé la prueba contra la clase MD5 incorporada y también md5sum.exe . En mi caso, la clase incorporada tomó 13 segundos, donde md5sum.exe también alrededor de 16-18 segundos en cada ejecución.

    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
fuente