web-dev-qa-db-fra.com

Comment écrire du code de streaming de fichiers ultra-rapide en C #?

Je dois diviser un fichier énorme en plusieurs fichiers plus petits. Chacun des fichiers de destination est défini par un décalage et une longueur correspondant au nombre d'octets. J'utilise le code suivant:

private void copy(string srcFile, string dstFile, int offset, int length)
{
    BinaryReader reader = new BinaryReader(File.OpenRead(srcFile));
    reader.BaseStream.Seek(offset, SeekOrigin.Begin);
    byte[] buffer = reader.ReadBytes(length);

    BinaryWriter writer = new BinaryWriter(File.OpenWrite(dstFile));
    writer.Write(buffer);
}

Étant donné que je dois appeler cette fonction environ 100 000 fois, elle est remarquablement lente.

  1. Existe-t-il un moyen de connecter le graveur directement au Reader? (C'est-à-dire sans charger le contenu dans la mémoire tampon en mémoire.)
39
ala

Je ne crois pas que rien dans .NET permette de copier une section de fichier sans la mettre en mémoire tampon. Cependant, il me semble que cela est de toute façon inefficace, car il faut ouvrir le fichier d'entrée et chercher plusieurs fois. Si vous séparez simplement du fichier, pourquoi ne pas l'ouvrir une fois, puis écrire quelque chose comme:

public static void CopySection(Stream input, string targetFile, int length)
{
    byte[] buffer = new byte[8192];

    using (Stream output = File.OpenWrite(targetFile))
    {
        int bytesRead = 1;
        // This will finish silently if we couldn't read "length" bytes.
        // An alternative would be to throw an exception
        while (length > 0 && bytesRead > 0)
        {
            bytesRead = input.Read(buffer, 0, Math.Min(length, buffer.Length));
            output.Write(buffer, 0, bytesRead);
            length -= bytesRead;
        }
    }
}

Cela crée une inefficacité mineure dans la création d'un tampon à chaque appel. Vous pouvez créer le tampon une fois et le transférer également dans la méthode:

public static void CopySection(Stream input, string targetFile,
                               int length, byte[] buffer)
{
    using (Stream output = File.OpenWrite(targetFile))
    {
        int bytesRead = 1;
        // This will finish silently if we couldn't read "length" bytes.
        // An alternative would be to throw an exception
        while (length > 0 && bytesRead > 0)
        {
            bytesRead = input.Read(buffer, 0, Math.Min(length, buffer.Length));
            output.Write(buffer, 0, bytesRead);
            length -= bytesRead;
        }
    }
}

Notez que cela ferme également le flux de sortie (à cause de l'instruction using), ce que votre code d'origine n'a pas fait.

L'important est que cela utilise plus efficacement la mise en mémoire tampon du fichier du système d'exploitation, car vous réutilisez le même flux d'entrée au lieu de rouvrir le fichier au début, puis d'effectuer une recherche.

Je pense que ce sera beaucoup plus rapide, mais vous devrez évidemment l'essayer pour voir ...

Cela suppose des morceaux contigus, bien sûr. Si vous devez ignorer des morceaux du fichier, vous pouvez le faire en dehors de la méthode. De même, si vous écrivez de très petits fichiers, vous voudrez peut-être également optimiser la situation. La meilleure façon de le faire serait probablement d’introduire un BufferedStream wrapper le flux d’entrée.

46
Jon Skeet

Le moyen le plus rapide d'effectuer des E/S sur fichier à partir de C # consiste à utiliser les fonctions Windows ReadFile et WriteFile. J'ai écrit une classe C # qui encapsule cette capacité ainsi qu'un programme d'analyse comparative qui examine différentes méthodes d'E/S, notamment BinaryReader et BinaryWriter. Voir mon blog sur:

http://designingefficientsoftware.wordpress.com/2011/03/03/efficient-file-io-from-csharp/

25
Bob Bryan

Quelle est la taille de length? Vous ferez peut-être mieux de réutiliser un tampon de taille fixe (moyennement grand, mais pas obscène) et d'oublier BinaryReader... utilisez simplement Stream.Read et Stream.Write.

(modifier) ​​quelque chose comme:

private static void copy(string srcFile, string dstFile, int offset,
     int length, byte[] buffer)
{
    using(Stream inStream = File.OpenRead(srcFile))
    using (Stream outStream = File.OpenWrite(dstFile))
    {
        inStream.Seek(offset, SeekOrigin.Begin);
        int bufferLength = buffer.Length, bytesRead;
        while (length > bufferLength &&
            (bytesRead = inStream.Read(buffer, 0, bufferLength)) > 0)
        {
            outStream.Write(buffer, 0, bytesRead);
            length -= bytesRead;
        }
        while (length > 0 &&
            (bytesRead = inStream.Read(buffer, 0, length)) > 0)
        {
            outStream.Write(buffer, 0, bytesRead);
            length -= bytesRead;
        }
    }        
}
6
Marc Gravell

Vous ne devez pas rouvrir le fichier source chaque fois que vous faites une copie, mais ouvrez-le une fois et transmettez le résultat résultant à la fonction de copie. De plus, cela peut aider si vous commandez vos recherches, afin de ne pas faire de grands sauts dans le fichier.

Si les longueurs ne sont pas trop grandes, vous pouvez également essayer de regrouper plusieurs appels de copie en regroupant les décalages proches les uns des autres et en lisant tout le bloc dont vous avez besoin, par exemple:

offset = 1234, length = 34
offset = 1300, length = 40
offset = 1350, length = 1000

peuvent être regroupés à une lecture:

offset = 1234, length = 1074

Ensuite, il vous suffit de "chercher" dans votre tampon et d’écrire les trois nouveaux fichiers à partir de là sans avoir à relire.

3
schnaader

Avez-vous envisagé d'utiliser le CCR puisque vous écrivez dans des fichiers séparés, vous pouvez tout faire en parallèle (lecture et écriture) et le CCR facilite grandement cette opération.

static void Main(string[] args)
    {
        Dispatcher dp = new Dispatcher();
        DispatcherQueue dq = new DispatcherQueue("DQ", dp);

        Port<long> offsetPort = new Port<long>();

        Arbiter.Activate(dq, Arbiter.Receive<long>(true, offsetPort,
            new Handler<long>(Split)));

        FileStream fs = File.Open(file_path, FileMode.Open);
        long size = fs.Length;
        fs.Dispose();

        for (long i = 0; i < size; i += split_size)
        {
            offsetPort.Post(i);
        }
    }

    private static void Split(long offset)
    {
        FileStream reader = new FileStream(file_path, FileMode.Open, 
            FileAccess.Read);
        reader.Seek(offset, SeekOrigin.Begin);
        long toRead = 0;
        if (offset + split_size <= reader.Length)
            toRead = split_size;
        else
            toRead = reader.Length - offset;

        byte[] buff = new byte[toRead];
        reader.Read(buff, 0, (int)toRead);
        reader.Dispose();
        File.WriteAllBytes("c:\\out" + offset + ".txt", buff);
    }

Ce code enregistre des décalages sur un port CCR, ce qui entraîne la création d'un thread pour exécuter le code dans la méthode Split. Cela vous oblige à ouvrir le fichier plusieurs fois, mais vous évite le besoin de synchronisation. Vous pouvez le rendre plus efficace en mémoire, mais vous devrez sacrifier la vitesse.

3
SpaceghostAli

La première chose que je recommande est de prendre des mesures. Où perds-tu ton temps? Est-ce dans la lecture ou l'écriture?

Plus de 100 000 accès (somme des temps): Combien de temps est consacré à l’allocation du tableau tampon?. le temps est-il passé en lecture et en écriture?

Si vous n'effectuez aucun type de transformation sur le fichier, avez-vous besoin d'un BinaryWriter ou pouvez-vous utiliser un flux de fichiers pour les écritures? (essayez-le, obtenez-vous une sortie identique? cela vous fait-il gagner du temps?)

1
JMarsch

Utilisation de FileSeam + StreamWriter Je sais qu’il est possible de créer d’énormes fichiers en très peu de temps (moins de 1 minute 30 secondes). Je génère trois fichiers totalisant plus de 700 mégaoctets à partir d'un fichier utilisant cette technique.

Votre principal problème avec le code que vous utilisez est que vous ouvrez un fichier à chaque fois. Cela crée une surcharge d'E/S de fichier.

Si vous connaissiez à l'avance les noms des fichiers que vous généreriez, vous pouvez extraire le fichier File.OpenWrite dans une méthode distincte. cela augmentera la vitesse. Sans voir le code qui détermine comment vous divisez les fichiers, je ne pense pas que vous puissiez aller beaucoup plus vite.

1
mcauthorn

Personne ne suggère de fileter? L'écriture des fichiers plus petits ressemble à un exemple de livre de texte où les discussions sont utiles. Configurez un groupe de threads pour créer les fichiers plus petits. De cette façon, vous pouvez tous les créer en parallèle et vous n'avez pas besoin d'attendre que chacun d'eux soit terminé. Mon hypothèse est que la création des fichiers (opération du disque) prendra beaucoup plus de temps que la division des données. et bien sûr, vous devez d'abord vérifier qu'une approche séquentielle n'est pas adéquate.

0
TheSean