it-swarm.com.de

Wie schreibe ich superschnellen Datei-Streaming-Code in C #?

Ich muss eine große Datei in viele kleinere Dateien aufteilen. Jede der Zieldateien wird durch einen Versatz und eine Länge als Anzahl von Bytes definiert. Ich verwende den folgenden Code:

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

Wenn man bedenkt, dass ich diese Funktion etwa 100.000 Mal aufrufen muss, ist sie bemerkenswert langsam.

  1. Gibt es eine Möglichkeit, den Writer direkt an den Reader anzuschließen? (Das heißt, ohne den Inhalt tatsächlich in den Puffer im Speicher zu laden.)
39
ala

Ich glaube nicht, dass es irgendetwas in .NET gibt, das es erlaubt, einen Teil einer Datei zu kopieren, ohne sie im Speicher abzulegen. Es fällt mir jedoch auf, dass dies ohnehin ineffizient ist, da es die Eingabedatei öffnen und viele Male suchen muss. Wenn Sie nur die Datei aufteilen, warum nicht die Eingabedatei einmal öffnen und dann einfach etwas schreiben wie:

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

Dies hat eine geringfügige Ineffizienz beim Erstellen eines Puffers bei jedem Aufruf. Möglicherweise möchten Sie den Puffer einmal erstellen und auch an die Methode übergeben:

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

Beachten Sie, dass dadurch auch der Ausgabestream geschlossen wird (aufgrund der using-Anweisung), der in Ihrem ursprünglichen Code nicht vorhanden war.

Der wichtige Punkt ist, dass dies die Dateipufferung des Betriebssystems effizienter nutzt, da Sie denselben Eingabestream wiederverwenden, anstatt die Datei am Anfang erneut zu öffnen und dann zu suchen.

Ich denke , dass es bedeutend schneller sein wird, aber offensichtlich müssen Sie es versuchen, um zu sehen ...

Dies setzt natürlich zusammenhängende Stücke voraus. Wenn Sie Teile der Datei überspringen müssen, können Sie dies außerhalb der Methode tun. Wenn Sie sehr kleine Dateien schreiben, möchten Sie möglicherweise auch diese Situation optimieren. Am einfachsten ist dies wahrscheinlich, wenn Sie den Eingabestream mit einem BufferedStream umschließen.

46
Jon Skeet

Die schnellste Möglichkeit, Datei-E/A über C # auszuführen, ist die Verwendung der Windows-Funktionen ReadFile und WriteFile. Ich habe eine C # -Klasse geschrieben, die diese Fähigkeit enthält, sowie ein Benchmarking-Programm, das verschiedene E/A-Methoden einschließlich BinaryReader und BinaryWriter betrachtet. Siehe meinen Blogeintrag unter:

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

25
Bob Bryan

Wie groß ist length? Sie sollten einen Puffer mit fester Größe (mäßig groß, aber nicht obszön) wiederverwenden. Vergessen Sie BinaryReader..., verwenden Sie einfach Stream.Read und Stream.Write.

(edit) etwas wie:

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

Sie sollten die Quelldatei nicht jedes Mal erneut öffnen, wenn Sie eine Kopie erstellen. Öffnen Sie sie besser einmal und übergeben Sie den resultierenden BinaryReader an die Kopierfunktion. Es kann auch hilfreich sein, wenn Sie Ihre Suchanfragen ordnen, so dass Sie keine großen Sprünge in der Datei machen.

Wenn die Längen nicht zu groß sind, können Sie auch versuchen, mehrere Kopieraufrufe zu gruppieren, indem Sie nahe beieinander liegende Versätze gruppieren und den gesamten Block lesen, den Sie dafür benötigen. Beispiel:

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

kann zu einer Lesung zusammengefasst werden:

offset = 1234, length = 1074

Dann müssen Sie nur noch in Ihrem Puffer "suchen" und können die drei neuen Dateien von dort aus schreiben, ohne erneut lesen zu müssen.

3
schnaader

Haben Sie die Verwendung des CCR in Betracht gezogen, seit Sie in separate Dateien schreiben, können Sie alles parallel ausführen (lesen und schreiben), und mit dem CCR ist dies sehr einfach.

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

Dieser Code schreibt Offsets an einen CCR-Port, wodurch ein Thread erstellt wird, um den Code in der Split-Methode auszuführen. Dies führt dazu, dass Sie die Datei mehrmals öffnen, aber die Synchronisierung entfällt. Sie können den Speicher effizienter gestalten, müssen aber auf Geschwindigkeit verzichten.

3
SpaceghostAli

Als Erstes würde ich empfehlen, Messungen vorzunehmen. Wo verlierst du deine Zeit? Ist es beim Lesen oder beim Schreiben?

Über 100.000 Zugriffe (Summe der Zeiten): Wie viel Zeit wird für die Zuweisung des Pufferarrays verwendet? Wie viel Zeit wird für das Öffnen der Datei zum Lesen verwendet (ist dies jedes Mal dieselbe Datei?) Wie viel Zeit wird mit Lese- und Schreibvorgängen verbracht?

Benötigen Sie einen BinaryWriter, wenn Sie keine Art von Transformation für die Datei durchführen, oder können Sie einen Dateistream für das Schreiben verwenden? (Versuchen Sie es, erhalten Sie eine identische Ausgabe? Spart es Zeit?)

1
JMarsch

Mit FileStream + StreamWriter ist mir bekannt, dass Sie in kürzester Zeit (weniger als 1 Minute und 30 Sekunden) massive Dateien erstellen können. Mit dieser Technik generiere ich drei Dateien mit einer Gesamtgröße von 700 Megabyte.

Ihr Hauptproblem mit dem von Ihnen verwendeten Code ist, dass Sie jedes Mal eine Datei öffnen. Das erzeugt einen Datei-E/A-Overhead.

Wenn Sie die Namen der Dateien kennen, die Sie vorab generieren würden, könnten Sie File.OpenWrite in eine separate Methode extrahieren. es wird die Geschwindigkeit erhöhen. Ohne den Code zu sehen, der bestimmt, wie Sie die Dateien teilen, glaube ich nicht, dass Sie viel schneller werden können.

1
mcauthorn

Niemand schlägt das Einfädeln vor? Das Schreiben der kleineren Dateien sieht wie ein Lehrbuch aus, in dem Threads nützlich sind. Richten Sie mehrere Threads ein, um die kleineren Dateien zu erstellen. Auf diese Weise können Sie sie alle parallel erstellen, und Sie müssen nicht warten, bis alle beendet sind. Ich gehe davon aus, dass das Erstellen der Dateien (Plattenoperation) viel länger dauert als das Aufteilen der Daten. Natürlich sollten Sie zunächst überprüfen, ob ein sequentieller Ansatz nicht angemessen ist.

0
TheSean