Melhor maneira de ler um arquivo grande em uma matriz de bytes em c #?

391

Eu tenho um servidor web que lê grandes arquivos binários (vários megabytes) em matrizes de bytes. O servidor pode estar lendo vários arquivos ao mesmo tempo (solicitações de página diferentes), por isso estou procurando a maneira mais otimizada de fazer isso sem sobrecarregar demais a CPU. O código abaixo é bom o suficiente?

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
fonte
60
Seu exemplo pode ser abreviado para byte[] buff = File.ReadAllBytes(fileName).
Jesse C. Slicer
3
Por que ser um serviço da web de terceiros implica que o arquivo precisa estar totalmente na RAM antes de ser enviado ao serviço da web, em vez de ser transmitido? O serviço da web não saberá a diferença.
Brian
@ Brian, alguns clientes não sabem como lidar com um fluxo .NET, como Java, por exemplo. Nesse caso, tudo o que pode ser feito é ler o arquivo inteiro na matriz de bytes.
sjeffrey
4
@sjeffrey: Eu disse que os dados devem ser transmitidos, não passados ​​como um fluxo .NET. Os clientes não saberão a diferença de qualquer maneira.
21712 Brian

Respostas:

776

Simplesmente substitua a coisa toda por:

return File.ReadAllBytes(fileName);

No entanto, se você está preocupado com o consumo de memória, você deve não ler o arquivo inteiro na memória de uma só vez a todos. Você deve fazer isso em pedaços.

Mehrdad Afshari
fonte
40
esse método é limitado a arquivos de 2 ^ 32 bytes (4,2 GB)
Mahmoud Farahat 4/12
11
File.ReadAllBytes joga OutOfMemoryException com arquivos grandes (testado com 630 MB de arquivos e falhou)
Sakito
6
@ juanjo.arana Sim, bem ... é claro que sempre haverá algo que não cabe na memória; nesse caso, não há resposta para a pergunta. Geralmente, você deve transmitir o arquivo e não armazená-lo na memória. Você pode querer olhar para isso de uma medida paliativa: msdn.microsoft.com/en-us/library/hh285054%28v=vs.110%29.aspx
Mehrdad Afshari
4
Há um limite para o tamanho da matriz no .NET, mas no .NET 4.5 você pode ativar o suporte para matrizes grandes (> 2 GB) usando a opção de configuração especial, consulte msdn.microsoft.com/en-us/library/hh285054.aspx
ilegal -imigrante
3
@harag Não, e não é isso que a pergunta faz.
Mehrdad Afshari
72

Eu poderia argumentar que a resposta aqui geralmente é "não". A menos que você precise absolutamente de todos os dados de uma vez, considere usar uma StreamAPI baseada em (ou alguma variante do leitor / iterador). Isso é especialmente importante quando você tem várias operações paralelas (conforme sugerido pela pergunta) para minimizar a carga do sistema e maximizar a taxa de transferência.

Por exemplo, se você estiver transmitindo dados para um chamador:

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
fonte
3
Para adicionar à sua declaração, até sugiro considerar manipuladores assíncronos do ASP.NET se você tiver uma operação vinculada a E / S, como transmitir um arquivo ao cliente. No entanto, se você precisar ler o arquivo inteiro byte[]por algum motivo, sugiro evitar o uso de fluxos ou qualquer outra coisa e apenas usar a API fornecida pelo sistema.
Mehrdad Afshari
@Mehrdad - concordou; mas o contexto completo não está claro. Da mesma forma, o MVC tem resultados de ação para isso.
Marc Gravell
Sim, preciso de todos os dados de uma só vez. Vai para um serviço da web de terceiros.
Tony_Henrich
Qual é a API fornecida pelo sistema?
Tony_Henrich
11
@Tony: afirmei na minha resposta: File.ReadAllBytes.
Mehrdad Afshari
32

Eu pensaria o seguinte:

byte[] file = System.IO.File.ReadAllBytes(fileName);
Powerlord
fonte
3
Observe que isso pode parar ao obter arquivos realmente grandes.
vapcguy
28

Seu código pode ser fatorado para isso (em vez 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;
} 

Observe a limitação de tamanho de arquivo Integer.MaxValue colocada pelo método Read. Em outras palavras, você pode ler apenas um pedaço de 2 GB de uma vez.

Observe também que o último argumento para o FileStream é um tamanho de buffer.

Eu também sugeriria ler sobre o FileStream e BufferedStream .

Como sempre, um programa de amostra simples para definir o perfil mais rápido será mais benéfico.

Além disso, seu hardware subjacente terá um grande efeito no desempenho. Você está usando unidades de disco rígido baseadas em servidor com caches grandes e uma placa RAID com cache de memória integrado? Ou você está usando uma unidade padrão conectada à porta IDE?


fonte
Por que o tipo de hardware faria diferença? Então, se é IDE, você usa algum método .NET e, se é RAID, usa outro?
Tony_Henrich
@ Tony_Henrich - Não tem nada a ver com o que você faz da sua linguagem de programação. Existem diferentes tipos de unidades de disco rígido. Por exemplo, as unidades Seagate são classificadas como "AS" ou "NS", com NS sendo a unidade de cache grande baseada em servidor, onde a unidade "AS" é o consumidor - unidade baseada em computador doméstico. As velocidades de busca e as taxas de transferência interna também afetam a rapidez com que você pode ler algo do disco. Matrizes RAID podem melhorar bastante o desempenho de leitura / gravação por meio de cache. Portanto, você poderá ler o arquivo de uma só vez, mas o hardware subjacente ainda é o fator decisivo.
2
Este código contém um erro crítico. A leitura é necessária apenas para retornar pelo menos 1 byte.
Mafu
Gostaria de ter certeza para embrulhar o tempo para int elenco com a construção marcada assim: verificada ((int) fs.Length)
TZUP
Eu apenas faria var binaryReader = new BinaryReader(fs); fileData = binaryReader.ReadBytes((int)fs.Length);nessa usingafirmação. Mas isso é efetivamente o que o OP fez, apenas recortei uma linha de código convertendo fs.Lengthpara, em intvez de obter o longvalor do FileInfocomprimento e convertê-lo.
vapcguy
9

Dependendo da frequência das operações, do tamanho dos arquivos e do número de arquivos que você está visualizando, há outros problemas de desempenho a serem considerados. Uma coisa a lembrar é que cada uma de suas matrizes de bytes será liberada à mercê do coletor de lixo. Se você não estiver armazenando em cache nenhum desses dados, poderá criar muito lixo e perder a maior parte do seu desempenho para % Time in GC. Se os pedaços forem maiores que 85K, você estará alocando para o Large Object Heap (LOH), que exigirá a liberação de uma coleção de todas as gerações (isso é muito caro e o servidor interromperá toda a execução enquanto estiver acontecendo) ) Além disso, se você tiver vários objetos no LOH, poderá acabar com a fragmentação do LOH (o LOH nunca é compactado), o que leva a um desempenho ruim e a exceções de falta de memória. Você pode reciclar o processo depois de atingir um determinado ponto, mas não sei se essa é uma prática recomendada.

O ponto é que você deve considerar o ciclo de vida completo do seu aplicativo antes de necessariamente ler todos os bytes na memória da maneira mais rápida possível ou pode estar trocando desempenho de curto prazo pelo desempenho geral.

Joel
fonte
código fonte C # sobre isso, para gerenciar garbage collector, chunks, desempenho, contadores de eventos , ...
PreguntonCojoneroCabrón
6

Eu diria que BinaryReaderestá bem, mas pode ser refatorado para isso, em vez de todas essas linhas de código para obter o comprimento do buffer:

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

Deveria ser melhor do que usar .ReadAllBytes(), já que vi nos comentários a resposta principal que inclui .ReadAllBytes()que um dos comentaristas teve problemas com arquivos> 600 MB, já que a BinaryReaderdestina-se a esse tipo de coisa. Além disso, colocando-o em uma usingdeclaração assegura a FileStreame BinaryReaderestá fechado e descartado.

vapcguy
fonte
Para C #, é necessário usar "using (FileStream fs = File.OpenRead (fileName))" em vez de "using (FileStream fs = new File.OpenRead (fileName))" como fornecido acima. Acabei de remover a nova palavra-chave antes de File.OpenRead ()
Syed Mohamed
@ Syed O código acima foi escrito para C #, mas você está certo de que newnão era necessário lá. Removido.
21418 vapcguy
1

No caso de "um arquivo grande", além do limite de 4 GB, minha lógica de código escrita a seguir é apropriada. O principal problema a ser observado é o tipo de dados LONG usado com o método SEEK. Como um LONG, é capaz de apontar além de 2 ^ 32 limites de dados. Neste exemplo, o código está processando primeiro o processamento do arquivo grande em pedaços de 1 GB, depois que os pedaços grandes de 1 GB são processados, os bytes restantes (<1 GB) são processados. Eu uso esse código para calcular o CRC de arquivos além do tamanho de 4 GB. (usando https://crc32c.machinezoo.com/ para o cálculo crc32c neste exemplo)

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
fonte
0

Use a classe BufferedStream em C # para melhorar o desempenho. Um buffer é um bloco de bytes na memória usado para armazenar dados em cache, reduzindo assim o número de chamadas para o sistema operacional. Os buffers melhoram o desempenho de leitura e gravação.

Consulte o seguinte para obter um exemplo de código e explicação adicional: http://msdn.microsoft.com/en-us/library/system.io.bufferedstream.aspx

Todd Moses
fonte
Qual o sentido de usar um BufferedStreamquando você está lendo a coisa toda de uma só vez?
Mehrdad Afshari
Ele pediu o melhor desempenho para não ler o arquivo de uma só vez.
Todd Moses
9
O desempenho é mensurável no contexto de uma operação. Buffer adicional para um fluxo que você está lendo sequencialmente, de uma só vez, na memória provavelmente não se beneficiará de um buffer extra.
Mehrdad Afshari
0

usa isto:

 bytesRead = responseStream.ReadAsync(buffer, 0, Length).Result;
Disha Sharma
fonte
2
Bem-vindo ao Stack Overflow! Como as explicações são uma parte importante das respostas nesta plataforma, explique seu código e como ele resolve o problema na pergunta e por que ele pode ser melhor do que outras respostas. Nosso guia Como escrever uma boa resposta pode ser útil para você. Obrigado
David
0

Visão geral: se sua imagem for adicionada como um recurso action = incorporado, use o GetExecutingAssembly para recuperar o recurso jpg em um fluxo e leia os dados binários no fluxo em uma 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;

    }
Leão dourado
fonte
-4

Eu recomendaria tentar o Response.TransferFile()método, em seguida, um Response.Flush()e Response.End()para servir seus arquivos grandes.

Dave
fonte
-7

Se você estiver lidando com arquivos acima de 2 GB, verá que os métodos acima falham.

É muito mais fácil enviar o fluxo para o MD5 e permitir que você divida seu arquivo para você:

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
fonte
11
Eu não vejo como o código é relevante para a questão (ou o que você sugere no texto escrito)
Vojtech B