it-swarm-eu.dev

Come scrivere il codice di streaming di file super-veloce in C #?

Devo dividere un file enorme in molti file più piccoli. Ciascuno dei file di destinazione è definito da un offset e dalla lunghezza come numero di byte. Sto usando il seguente codice:

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

Considerando che devo chiamare questa funzione circa 100.000 volte, è straordinariamente lento.

  1. C'è un modo per rendere il writer collegato direttamente al Reader? (Cioè, senza effettivamente caricare il contenuto nel buffer in memoria.)
39
ala

Non credo che all'interno di .NET ci sia qualcosa che permetta di copiare una sezione di un file senza averla memorizzata in memoria. Tuttavia, mi sembra che sia comunque inefficiente, poiché è necessario aprire il file di input e cercare molte volte. Se stai solo suddividendo il file, perché non aprire il file di input una volta e poi scrivi qualcosa come:

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

Ciò ha una minore inefficienza nella creazione di un buffer per ogni chiamata: potresti voler creare il buffer una volta e inoltrarlo nel metodo:

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

Nota che questo chiude anche il flusso di output (a causa dell'istruzione using) che il tuo codice originale non ha.

Il punto importante è che questo utilizzerà il buffering dei file del sistema operativo in modo più efficiente, perché riutilizzi lo stesso flusso di input, invece di riaprire il file all'inizio e quindi cercare.

I think sarà molto più veloce, ma ovviamente dovrai provarlo per vedere ...

Questo presuppone blocchi contigui, ovviamente. Se è necessario saltare i bit del file, è possibile farlo dall'esterno del metodo. Inoltre, se stai scrivendo file molto piccoli, potresti voler ottimizzare anche per quella situazione - il modo più semplice per farlo sarebbe probabilmente quello di introdurre un BufferedStream che avvolge il flusso di input.

46
Jon Skeet

Il modo più veloce per eseguire I/O su file da C # consiste nell'utilizzare le funzioni Windows ReadFile e WriteFile. Ho scritto una classe C # che racchiude questa capacità e un programma di benchmarking che esamina i metodi I/O differnet, inclusi BinaryReader e BinaryWriter. Vedi il mio post sul blog all'indirizzo:

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

25
Bob Bryan

Quanto è grande length? Si può fare di meglio riutilizzare un buffer di dimensioni fisse (moderatamente grande, ma non osceno) e dimenticare BinaryReader... basta usare Stream.Read e Stream.Write.

(modifica) qualcosa come:

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

Non dovresti riaprire il file sorgente ogni volta che fai una copia, meglio aprirlo una volta e passare il BinaryReader risultante alla funzione copia. Inoltre, potrebbe essere d'aiuto se ordini i tuoi annunci, così non farai grandi salti all'interno del file.

Se le lunghezze non sono troppo grandi, puoi anche provare a raggruppare più chiamate di copia raggruppando gli offset vicini l'uno all'altro e leggendo l'intero blocco necessario per loro, ad esempio:

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

può essere raggruppato in una sola lettura:

offset = 1234, length = 1074

Quindi devi solo "cercare" nel tuo buffer e puoi scrivere i tre nuovi file da lì senza doverli nuovamente leggere.

3
schnaader

Hai preso in considerazione l'utilizzo del CCR poiché stai scrivendo in file separati, puoi fare tutto in parallelo (leggi e scrivi) e il CCR rende molto facile farlo.

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

Questo codice invia offset a una porta CCR che provoca la creazione di un thread per eseguire il codice nel metodo Split. Ciò causa l'apertura del file più volte, ma elimina la necessità di sincronizzazione. Puoi renderlo più efficiente in termini di memoria, ma dovrai sacrificare la velocità.

3
SpaceghostAli

La prima cosa che consiglierei è di prendere le misure. Dove stai perdendo il tuo tempo? È nella lettura o nella scrittura?

Oltre 100.000 accessi (sommano i tempi): Quanto tempo è dedicato all'allocazione dell'array di buffer? Quanto tempo è dedicato all'apertura del file per la lettura (si tratta sempre dello stesso file?) Quanto costa? il tempo è trascorso in operazioni di lettura e scrittura?

Se non stai facendo alcun tipo di trasformazione sul file, hai bisogno di un BinaryWriter o puoi usare un filestream per le scritture? (provalo, ottieni un output identico? fa risparmiare tempo?)

1
JMarsch

Usando FileStream + StreamWriter so che è possibile creare file di grandi dimensioni in poco tempo (meno di 1 minuto e 30 secondi). Genero tre file per un totale di oltre 700 megabyte da un file usando quella tecnica.

Il tuo problema principale con il codice che stai utilizzando è che stai aprendo un file ogni volta. Questo sta creando un overhead di I/O su file.

Se conoscessi i nomi dei file che avresti generato in anticipo, potresti estrarre File.OpenWrite in un metodo separato; aumenterà la velocità. Senza vedere il codice che determina come si dividono i file, non penso che si possa ottenere molto più velocemente.

1
mcauthorn

Nessuno suggerisce la filettatura? Scrivere i file più piccoli sembra un esempio di libro di testo in cui i thread sono utili. Impostare un gruppo di thread per creare i file più piccoli. in questo modo, è possibile crearli tutti in parallelo e non è necessario attendere che ciascuno finisca. La mia ipotesi è che la creazione dei file (operazione su disco) richiederà molto più tempo della divisione dei dati. e, naturalmente, è necessario verificare prima che un approccio sequenziale non sia adeguato.

0
TheSean