¿La mejor manera de leer un archivo grande en una matriz de bytes en C #?

391

Tengo un servidor web que leerá archivos binarios grandes (varios megabytes) en conjuntos de bytes. El servidor podría estar leyendo varios archivos al mismo tiempo (solicitudes de página diferentes), por lo que estoy buscando la forma más optimizada para hacerlo sin gravar demasiado la CPU. ¿El código a continuación es lo suficientemente bueno?

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;
}
Tony_Henrich
fuente
6060
Su ejemplo se puede abreviar a byte[] buff = File.ReadAllBytes(fileName).
Jesse C. Slicer
3
¿Por qué ser un servicio web de terceros implica que el archivo debe estar completamente en RAM antes de enviarse al servicio web, en lugar de transmitirse? El servicio web no sabrá la diferencia.
Brian
@Brian, algunos clientes no saben cómo manejar una secuencia .NET, como Java, por ejemplo. Cuando este es el caso, todo lo que se puede hacer es leer todo el archivo en una matriz de bytes.
sjeffrey
44
@sjeffrey: Dije que los datos deberían transmitirse, no pasarse como una secuencia .NET. Los clientes no sabrán la diferencia de ninguna manera.
Brian

Respuestas:

776

Simplemente reemplace todo con:

return File.ReadAllBytes(fileName);

Sin embargo, si usted está preocupado por el consumo de memoria, debe no leer todo el archivo en memoria a la vez a todos. Deberías hacer eso en trozos.

Mehrdad Afshari
fuente
40
este método está limitado a archivos de 2 ^ 32 bytes (4,2 GB)
Mahmoud Farahat
11
File.ReadAllBytes lanza OutOfMemoryException con archivos grandes (probado con un archivo de 630 MB y falló)
sakito
66
@ juanjo.arana Sí, bueno ... por supuesto siempre habrá algo que no cabe en la memoria, en cuyo caso, no hay respuesta a la pregunta. En general, debe transmitir el archivo y no almacenarlo en la memoria por completo. Es posible que desee ver esto para una medida provisional: msdn.microsoft.com/en-us/library/hh285054%28v=vs.110%29.aspx
Mehrdad Afshari
44
Hay un límite para el tamaño de la matriz en .NET, pero en .NET 4.5 puede activar el soporte para matrices grandes (> 2GB) usando la opción de configuración especial, consulte msdn.microsoft.com/en-us/library/hh285054.aspx
ilegal -inmigrante 01 de
3
@harag No, y esa no es la pregunta.
Mehrdad Afshari
72

Podría argumentar que la respuesta aquí generalmente es "no". A menos que necesite absolutamente todos los datos a la vez, considere usar una StreamAPI basada en (o alguna variante de lector / iterador). Es decir especialmente importante cuando tiene múltiples operaciones paralelas (como lo sugiere la pregunta) para minimizar la carga del sistema y maximizar el rendimiento.

Por ejemplo, si está transmitiendo datos a una persona que llama:

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);
    }
}
Marc Gravell
fuente
3
Para agregar a su declaración, incluso sugiero considerar controladores asincrónicos de ASP.NET si tiene una operación vinculada de E / S como transmitir un archivo al cliente. Sin embargo, si tiene que leer el archivo completo byte[]por algún motivo, le sugiero que evite usar transmisiones o cualquier otra cosa y simplemente use la API proporcionada por el sistema.
Mehrdad Afshari
@Mehrdad - de acuerdo; pero el contexto completo no está claro. Del mismo modo, MVC tiene resultados de acción para esto.
Marc Gravell
Sí, necesito todos los datos a la vez. Va a un servicio web de terceros.
Tony_Henrich
¿Cuál es la API proporcionada por el sistema?
Tony_Henrich
1
@Tony: dije en mi respuesta: File.ReadAllBytes.
Mehrdad Afshari
32

Yo pensaría esto:

byte[] file = System.IO.File.ReadAllBytes(fileName);
Powerlord
fuente
3
Tenga en cuenta que esto puede detenerse al obtener archivos realmente grandes.
vapcguy
28

Su código se puede tener en cuenta para esto (en lugar de 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;
} 

Tenga en cuenta el Integer.MaxValue: limitación de tamaño de archivo establecida por el método Read. En otras palabras, solo puedes leer un fragmento de 2GB a la vez.

También tenga en cuenta que el último argumento para FileStream es un tamaño de búfer.

También sugeriría leer sobre FileStream y BufferedStream .

Como siempre, un programa de muestra simple para perfilar que es el más rápido será el más beneficioso.

Además, su hardware subyacente tendrá un gran efecto en el rendimiento. ¿Está utilizando unidades de disco duro basadas en servidor con cachés grandes y una tarjeta RAID con memoria caché integrada? ¿O está utilizando una unidad estándar conectada al puerto IDE?


fuente
¿Por qué el tipo de hardware haría una diferencia? Entonces, si es IDE, usa algún método .NET y si es RAID, ¿usa otro?
Tony_Henrich
@ Tony_Henrich: no tiene nada que ver con las llamadas que realiza desde su lenguaje de programación. Existen diferentes tipos de unidades de disco duro. Por ejemplo, las unidades Seagate se clasifican como "AS" o "NS", siendo NS la unidad de caché grande basada en el servidor donde, como la unidad "AS" es la unidad basada en la computadora del hogar. Las velocidades de búsqueda y las tasas de transferencia interna también afectan la rapidez con la que puede leer algo del disco. Las matrices RAID pueden mejorar enormemente el rendimiento de lectura / escritura a través del almacenamiento en caché. Por lo tanto, es posible que pueda leer el archivo de una vez, pero el hardware subyacente sigue siendo el factor decisivo.
2
Este código contiene un error crítico. Solo se requiere leer para devolver al menos 1 byte.
mafu
Me aseguraría de envolver el largo int int con la construcción marcada de esta manera: check ((int) fs.Length)
tzup
Lo haría var binaryReader = new BinaryReader(fs); fileData = binaryReader.ReadBytes((int)fs.Length);en esa usingdeclaración. Pero eso es efectivamente lo que hizo el OP, solo corté una línea de código al convertirlo fs.Lengthen intlugar de obtener el longvalor de la FileInfolongitud y convertirlo.
vapcguy
9

Dependiendo de la frecuencia de las operaciones, el tamaño de los archivos y la cantidad de archivos que está viendo, hay otros problemas de rendimiento a tener en cuenta. Una cosa para recordar es que cada una de sus matrices de bytes se liberará a merced del recolector de basura. Si no está almacenando en caché ninguno de esos datos, podría terminar creando mucha basura y perdiendo la mayor parte de su rendimiento por % de tiempo en GC. Si los fragmentos son más grandes que 85K, se asignará al Montón de objetos grandes (LOH) que requerirá una colección de todas las generaciones para liberarse (esto es muy costoso, y en un servidor detendrá toda ejecución mientras continúa) ) Además, si tiene una tonelada de objetos en el LOH, puede terminar con la fragmentación de LOH (el LOH nunca se compacta), lo que conduce a un rendimiento deficiente y excepciones de falta de memoria. Puede reciclar el proceso una vez que llegue a cierto punto, pero no sé si es una buena práctica.

El punto es que debe considerar el ciclo de vida completo de su aplicación antes de simplemente leer todos los bytes en la memoria de la manera más rápida posible o podría estar intercambiando el rendimiento a corto plazo por el rendimiento general.

Joel
fuente
código fuente de C # en ello, para gestionar garbage collector, chunks, rendimiento, contadores de eventos , ...
PreguntonCojoneroCabrón
6

Diría que BinaryReaderestá bien, pero se puede refactorizar a esto, en lugar de todas esas líneas de código para obtener la longitud del búfer:

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

Debería ser mejor que usar .ReadAllBytes(), ya que vi en los comentarios en la respuesta superior que incluye .ReadAllBytes()que uno de los comentaristas tuvo problemas con archivos> 600 MB, ya que BinaryReaderestá destinado a este tipo de cosas. Además, poniéndola en un usingcomunicado asegura que el FileStreamy BinaryReaderestán cerrados y eliminados.

vapcguy
fuente
Para C #, debe usar "using (FileStream fs = File.OpenRead (fileName))" en lugar de "using (FileStream fs = new File.OpenRead (fileName))" como se indicó anteriormente. Acabo de eliminar una nueva palabra clave antes de File.OpenRead ()
Syed Mohamed
@Syed El código anterior estaba escrito para C #, pero tienes razón, newno era necesario allí. Remoto.
vapcguy
1

En caso de que 'un archivo grande' signifique más allá del límite de 4 GB, entonces mi siguiente lógica de código escrito es apropiada. La cuestión clave a tener en cuenta es el tipo de datos LARGO utilizado con el método SEEK. Como LONG es capaz de apuntar más allá de 2 ^ 32 límites de datos. En este ejemplo, el código procesa procesando primero el archivo grande en fragmentos de 1 GB, después de que se procesan los fragmentos enteros grandes de 1 GB, se procesan los bytes restantes (<1 GB). Utilizo este código para calcular el CRC de archivos más allá del tamaño de 4 GB. (usando https://crc32c.machinezoo.com/ para el cálculo de crc32c en este ejemplo)

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;
}
Menno de Ruiter
fuente
0

Use la clase BufferedStream en C # para mejorar el rendimiento. Un búfer es un bloque de bytes en la memoria que se usa para almacenar en caché los datos, lo que reduce la cantidad de llamadas al sistema operativo. Los buffers mejoran el rendimiento de lectura y escritura.

Consulte lo siguiente para ver un ejemplo de código y una explicación adicional: http://msdn.microsoft.com/en-us/library/system.io.bufferedstream.aspx

Todd Moses
fuente
¿Cuál es el punto de usar un BufferedStreamcuando estás leyendo todo de una vez?
Mehrdad Afshari
Pidió el mejor rendimiento para no leer el archivo de una vez.
Todd Moses
99
El rendimiento es medible en el contexto de una operación. El almacenamiento en búfer adicional para una secuencia que está leyendo secuencialmente, todo a la vez, en la memoria no es probable que se beneficie de un búfer adicional.
Mehrdad Afshari
0

utilizar este:

 bytesRead = responseStream.ReadAsync(buffer, 0, Length).Result;
Disha Sharma
fuente
2
¡Bienvenido a Stack Overflow! Como las explicaciones son una parte importante de las respuestas en esta plataforma, explique su código y cómo resuelve el problema en la pregunta y por qué podría ser mejor que otras respuestas. Nuestra guía Cómo escribir una buena respuesta puede ser útil para usted. Gracias
David
0

Descripción general: si su imagen se agrega como un recurso acción = incrustado, use GetExecutingAssembly para recuperar el recurso jpg en una secuencia y luego lea los datos binarios de la secuencia en una matriz de bytes

   public byte[] GetAImage()
    {
        byte[] bytes=null;
        var assembly = Assembly.GetExecutingAssembly();
        var resourceName = "MYWebApi.Images.X_my_image.jpg";

        using (Stream stream = assembly.GetManifestResourceStream(resourceName))
        {
            bytes = new byte[stream.Length];
            stream.Read(bytes, 0, (int)stream.Length);
        }
        return bytes;

    }
Leon de Oro
fuente
-4

Recomiendo probar el Response.TransferFile()método a continuación, una Response.Flush()y Response.End()para servir a sus archivos de gran tamaño.

Dave
fuente
-7

Si está tratando con archivos de más de 2 GB, encontrará que los métodos anteriores fallan.

Es mucho más fácil simplemente pasar la transmisión a MD5 y permitir que fragmente el archivo por usted:

private byte[] computeFileHash(string filename)
{
    MD5 md5 = MD5.Create();
    using (FileStream fs = new FileStream(filename, FileMode.Open))
    {
        byte[] hash = md5.ComputeHash(fs);
        return hash;
    }
}
elaverick
fuente
11
No veo cómo el código es relevante para la pregunta (o lo que sugieres en el texto escrito)
Vojtech B