La meilleure façon de lire un gros fichier dans un tableau d'octets en C #?

392

J'ai un serveur Web qui lira de gros fichiers binaires (plusieurs mégaoctets) dans des tableaux d'octets. Le serveur pourrait lire plusieurs fichiers en même temps (différentes demandes de page), donc je cherche la façon la plus optimisée de le faire sans trop taxer le CPU. Le code ci-dessous est-il assez bon?

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
la source
60
Votre exemple peut être abrégé en byte[] buff = File.ReadAllBytes(fileName).
Jesse C. Slicer
3
Pourquoi le fait d'être un service Web tiers implique-t-il que le fichier doit être entièrement en RAM avant d'être envoyé au service Web, plutôt que d'être diffusé? Le webservice ne connaîtra pas la différence.
Brian
@Brian, Certains clients ne savent pas comment gérer un flux .NET, comme Java par exemple. Lorsque c'est le cas, tout ce qui peut être fait est de lire le fichier entier dans un tableau d'octets.
sjeffrey
4
@sjeffrey: J'ai dit que les données devraient être diffusées, pas transmises en tant que flux .NET. Les clients ne connaîtront pas la différence de toute façon.
Brian

Réponses:

776

Remplacez simplement le tout par:

return File.ReadAllBytes(fileName);

Cependant, si vous êtes préoccupé par la consommation de mémoire, vous ne devez pas lire le fichier en entier en une seule fois. Vous devriez le faire en morceaux.

Mehrdad Afshari
la source
40
cette méthode est limitée à des fichiers de 2 ^ 32 octets (4,2 Go)
Mahmoud Farahat
11
File.ReadAllBytes lève OutOfMemoryException avec de gros fichiers (testé avec un fichier de 630 Mo et il a échoué)
sakito
6
@ juanjo.arana Ouais, eh bien ... bien sûr, il y aura toujours quelque chose qui ne rentrera pas dans la mémoire, auquel cas, il n'y aura pas de réponse à la question. En règle générale, vous devez diffuser le fichier et ne pas le stocker entièrement en mémoire. Vous voudrez peut-être regarder ceci pour une mesure provisoire
Mehrdad Afshari
4
Il y a une limite pour la taille du tableau dans .NET, mais dans .NET 4.5, vous pouvez activer la prise en charge des grands tableaux (> 2 Go) à l'aide de l'option de configuration spéciale, voir msdn.microsoft.com/en-us/library/hh285054.aspx
illégal -immigrant
3
@harag Non, et ce n'est pas ce que demande la question.
Mehrdad Afshari
72

Je pourrais faire valoir que la réponse ici est généralement «ne pas». À moins que vous n'ayez absolument besoin de toutes les données à la fois, envisagez d'utiliser une StreamAPI basée sur (ou une variante de lecteur / itérateur). Cela est particulièrement important lorsque vous avez plusieurs opérations parallèles (comme suggéré par la question) pour minimiser la charge système et maximiser le débit.

Par exemple, si vous diffusez des données à un appelant:

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
la source
3
Pour ajouter à votre déclaration, je suggère même d'envisager des gestionnaires asynchrones ASP.NET si vous avez une opération liée aux E / S comme le streaming d'un fichier vers le client. Cependant, si vous devez lire l'intégralité du fichier sur un byte[]pour une raison quelconque, je suggère d'éviter d'utiliser des flux ou autre chose et d'utiliser simplement l'API fournie par le système.
Mehrdad Afshari
@Mehrdad - d'accord; mais le contexte complet n'est pas clair. De même, MVC a des résultats d'action pour cela.
Marc Gravell
Oui, j'ai besoin de toutes les données en même temps. Il va à un service Web tiers.
Tony_Henrich
Quelle est l'API fournie par le système?
Tony_Henrich
1
@Tony: j'ai dit dans ma réponse: File.ReadAllBytes.
Mehrdad Afshari
33

Je pense que ceci:

byte[] file = System.IO.File.ReadAllBytes(fileName);
Powerlord
la source
3
Notez que cela peut se bloquer lors de l'obtention de fichiers très volumineux.
vapcguy
28

Votre code peut y être pris en compte (au lieu 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;
} 

Notez la limitation de taille de fichier Integer.MaxValue placée par la méthode Read. En d'autres termes, vous ne pouvez lire qu'un morceau de 2 Go à la fois.

Notez également que le dernier argument du FileStream est une taille de tampon.

Je suggère également de lire sur FileStream et BufferedStream .

Comme toujours, un exemple de programme simple à profiler qui est le plus rapide sera le plus avantageux.

Votre matériel sous-jacent aura également un effet important sur les performances. Utilisez-vous des disques durs sur serveur avec de grands caches et une carte RAID avec cache de mémoire intégré? Ou utilisez-vous un lecteur standard connecté au port IDE?


la source
Pourquoi le type de matériel ferait-il une différence? Donc, si c'est IDE, vous utilisez une méthode .NET et si c'est RAID, vous en utilisez une autre?
Tony_Henrich
@Tony_Henrich - Cela n'a rien à voir avec les appels que vous effectuez à partir de votre langage de programmation. Il existe différents types de disques durs. Par exemple, les disques Seagate sont classés comme "AS" ou "NS", NS étant le lecteur basé sur le serveur et à grande mémoire cache, alors que le lecteur "AS" est le lecteur basé sur l'ordinateur domestique. Les vitesses de recherche et les taux de transfert internes affectent également la vitesse à laquelle vous pouvez lire quelque chose à partir du disque. Les matrices RAID peuvent considérablement améliorer les performances de lecture / écriture grâce à la mise en cache. Ainsi, vous pourrez peut-être lire le fichier en une seule fois, mais le matériel sous-jacent reste le facteur décisif.
2
Ce code contient un bogue critique. La lecture n'est requise que pour renvoyer au moins 1 octet.
mafu
Je m'assurerais d'envelopper le long en cast int avec la construction vérifiée comme ceci: vérifié ((int) fs.Length)
tzup
Je ferais juste var binaryReader = new BinaryReader(fs); fileData = binaryReader.ReadBytes((int)fs.Length);dans cette usingdéclaration. Mais c'est effectivement ce que l'OP a fait, j'ai juste coupé une ligne de code en le convertissant en fs.Lengthau intlieu d'obtenir la longvaleur de la FileInfolongueur et de la convertir.
vapcguy
9

Selon la fréquence des opérations, la taille des fichiers et le nombre de fichiers que vous consultez, il existe d'autres problèmes de performances à prendre en considération. Une chose à retenir est que chacun de vos tableaux d'octets sera libéré à la merci du ramasse-miettes. Si vous ne mettez en cache aucune de ces données, vous pourriez finir par créer beaucoup de déchets et perdre la plupart de vos performances à % Time dans GC. Si les morceaux sont supérieurs à 85 Ko, vous les allouerez au grand tas d'objets (LOH), ce qui nécessitera une collection de toutes les générations à libérer (cela est très coûteux, et sur un serveur arrêtera toute exécution pendant qu'il se déroule) ). De plus, si vous avez une tonne d'objets sur la LOH, vous pouvez vous retrouver avec une fragmentation de la LOH (la LOH n'est jamais compactée), ce qui entraîne de mauvaises performances et des exceptions de mémoire insuffisante. Vous pouvez recycler le processus une fois que vous avez atteint un certain point, mais je ne sais pas si c'est une meilleure pratique.

Le fait est que vous devez considérer le cycle de vie complet de votre application avant de lire simplement tous les octets en mémoire de la manière la plus rapide possible, ou vous pourriez échanger des performances à court terme pour des performances globales.

Joel
la source
code source C # à ce sujet, pour gérer garbage collector, chunks, les performances, les compteurs d'événements , ...
PreguntonCojoneroCabrón
6

Je dirais que BinaryReaderc'est bien, mais peut être refactorisé à cela, au lieu de toutes ces lignes de code pour obtenir la longueur du tampon:

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

Devrait être mieux que d'utiliser .ReadAllBytes(), car j'ai vu dans les commentaires sur la réponse supérieure qui inclut .ReadAllBytes()que l'un des commentateurs avait des problèmes avec des fichiers> 600 Mo, car un BinaryReaderest destiné à ce genre de chose. En outre, le mettre dans une usingdéclaration garantit que le FileStreamet BinaryReadersont fermés et éliminés.

vapcguy
la source
Pour C #, vous devez utiliser «en utilisant (FileStream fs = File.OpenRead (fileName))» au lieu de «en utilisant (FileStream fs = new File.OpenRead (fileName))» comme indiqué ci-dessus. Nouveau mot-clé supprimé juste avant File.OpenRead ()
Syed Mohamed
@Syed Le code ci-dessus a été écrit pour C #, mais vous avez raison, il newn'était pas nécessaire là-bas. Supprimé.
vapcguy
1

Dans le cas où «un gros fichier» signifie au-delà de la limite de 4 Go, ma logique de code écrite suivante est appropriée. Le problème clé à noter est le type de données LONG utilisé avec la méthode SEEK. Comme un LONG est capable de pointer au-delà de 2 ^ 32 limites de données. Dans cet exemple, le code traite d'abord le traitement du gros fichier en morceaux de 1 Go, après le traitement des gros morceaux entiers de 1 Go, les octets restants (<1 Go) sont traités. J'utilise ce code pour calculer le CRC des fichiers au-delà de la taille de 4 Go. (en utilisant https://crc32c.machinezoo.com/ pour le calcul de crc32c dans cet exemple)

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
la source
0

Utilisez la classe BufferedStream en C # pour améliorer les performances. Un tampon est un bloc d'octets en mémoire utilisé pour mettre en cache des données, réduisant ainsi le nombre d'appels au système d'exploitation. Les tampons améliorent les performances de lecture et d'écriture.

Consultez l'exemple suivant pour un exemple de code et des explications supplémentaires: http://msdn.microsoft.com/en-us/library/system.io.bufferedstream.aspx

Todd Moses
la source
Quel est l'intérêt d'utiliser un BufferedStreamlorsque vous lisez le tout à la fois?
Mehrdad Afshari
Il a demandé la meilleure performance pour ne pas lire le fichier à la fois.
Todd Moses
9
La performance est mesurable dans le contexte d'une opération. Une mise en mémoire tampon supplémentaire pour un flux que vous lisez séquentiellement, en une seule fois, dans la mémoire ne bénéficiera probablement pas d'une mémoire tampon supplémentaire.
Mehrdad Afshari
0

utilisez ceci:

 bytesRead = responseStream.ReadAsync(buffer, 0, Length).Result;
Disha Sharma
la source
2
Bienvenue dans Stack Overflow! Comme les explications sont une partie importante des réponses sur cette plate-forme, veuillez expliquer votre code et comment il résout le problème dans la question et pourquoi il pourrait être meilleur que d'autres réponses. Notre guide Comment rédiger une bonne réponse pourrait vous être utile. Merci
David
0

Présentation: si votre image est ajoutée en tant que ressource action = incorporée, utilisez GetExecutingAssembly pour récupérer la ressource jpg dans un flux, puis lisez les données binaires du flux dans un tableau d'octets

   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;

    }
Lion d'or
la source
-4

Je recommanderais d'essayer la Response.TransferFile()méthode puis un Response.Flush()et Response.End()pour servir vos gros fichiers.

Dave
la source
-7

Si vous traitez des fichiers de plus de 2 Go, vous constaterez que les méthodes ci-dessus échouent.

Il est beaucoup plus facile de transférer le flux vers MD5 et de permettre à celui-ci de segmenter votre fichier pour vous:

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
la source
11
Je ne vois pas en quoi le code est pertinent pour la question (ou ce que vous suggérez dans le texte écrit)
Vojtech B