web-dev-qa-db-ja.com

C#で超高速ファイルストリーミングコードを記述する方法

巨大なファイルを多くの小さなファイルに分割する必要があります。宛先ファイルのそれぞれは、オフセットと長さによってバイト数として定義されます。私は次のコードを使用しています:

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

この関数を約100,000回呼び出さなければならないことを考えると、非常に遅いです。

  1. ライターをリーダーに直接接続する方法はありますか? (つまり、実際にコンテンツをメモリのバッファにロードすることはありません。)
39
ala

.NETには、メモリにバッファリングせずにファイルのセクションをコピーできるものはないと思います。ただし、入力ファイルを開いて何度もシークする必要があるため、とにかくこれは非効率的だと思います。ファイルを分割するjustの場合は、入力ファイルを一度開いてから、次のように記述してください。

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

これは、各呼び出しでバッファを作成するのにわずかな非効率性があります-バッファを一度作成して、それをメソッドに渡すこともできます:

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

これにより、元のコードではできなかった(usingステートメントにより)出力ストリームも閉じられることに注意してください。

重要な点は、最初にファイルを開いてからシークするのではなく、同じ入力ストリームを再利用するため、オペレーティングシステムのファイルバッファリングをより効率的に使用することです。

Ithinkそれは大幅に高速になりますが、明らかに試してみる必要があります.

もちろん、これは連続したチャンクを想定しています。ファイルの一部をスキップする必要がある場合は、メソッドの外部から実行できます。また、非常に小さなファイルを作成している場合は、その状況にも最適化することをお勧めします。おそらく最も簡単な方法は、入力ストリームをラップする BufferedStream を導入することです。 。

46
Jon Skeet

C#からファイルI/Oを実行する最も速い方法は、WindowsのReadFile関数とWriteFile関数を使用することです。この機能をカプセル化するC#クラスと、BinaryReaderやBinaryWriterなどの異なるI/Oメソッドを調べるベンチマークプログラムを作成しました。私のブログ投稿をご覧ください:

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

26
Bob Bryan

lengthはどれくらいの大きさですか?固定サイズ(適度に大きいが、わいせつではない)バッファーを再利用し、BinaryReader...を忘れて、Stream.ReadおよびStream.Write

(編集)次のようなもの:

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

コピーを行うたびにソースファイルを再度開くのではなく、一度開いてから、結果のBinaryReaderをコピー関数に渡すことをお勧めします。また、シークを注文すると役立つ場合があります。そのため、ファイル内で大きなジャンプをすることはありません。

長さが長すぎない場合は、互いに近いオフセットをグループ化し、それらに必要なブロック全体を読み取ることで、複数のコピー呼び出しをグループ化することもできます。次に例を示します。

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

1つの読み取りにグループ化できます。

offset = 1234, length = 1074

その後、バッファを「シーク」するだけで、そこから3つの新しいファイルを書き込むことができます。再度読み取る必要はありません。

3
schnaader

CCRを使用することを検討したことがあります。これは、別々のファイルに書き込むため、すべてを並行して実行(読み取りおよび書き込み)でき、CCRを使用すると非常に簡単に実行できるためです。

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

このコードは、CCRポートにオフセットをポストします。これにより、Splitメソッドのコードを実行するスレッドが作成されます。これにより、ファイルを複数回開くことになりますが、同期の必要はなくなります。メモリの効率を高めることができますが、速度を犠牲にする必要があります。

3
SpaceghostAli

私がお勧めする最初のことは、測定を行うことです。どこで時間を失っていますか?読み取り中ですか、書き込み中ですか?

100,000回を超えるアクセス(合計):バッファー配列の割り当てに費やされる時間はどれくらいですか?読み取りのためにファイルを開くのにどのくらいの時間がかかりますか(毎回同じファイルですか?)読み取りおよび書き込み操作にどれくらいの時間がかかりますか?

ファイルに対して何らかのタイプの変換を行っていない場合、BinaryWriterが必要ですか、または書き込みにファイルストリームを使用できますか? (試して、同じ出力が得られますか?時間を節約できますか?)

1
JMarsch

FileStream + StreamWriterを使用すると、短時間(1分30秒未満)で大量のファイルを作成できることがわかっています。その手法を使用して、1つのファイルから合計700 + MBを超える3つのファイルを生成します。

使用しているコードの主な問題は、毎回ファイルを開くことです。それはファイルI/Oオーバーヘッドを生み出しています。

事前に生成するファイルの名前がわかっている場合は、File.OpenWriteを別のメソッドに抽出できます。速度が上がります。ファイルを分割する方法を決定するコードを見ることなく、私はあなたがもっと速くなるとは思わない。

1
mcauthorn

誰もスレッドを提案しませんか?小さなファイルを書くことは、スレッドが役立つ場所のテキスト本の例のように見えます。多数のスレッドを設定して、より小さいファイルを作成します。これにより、すべてを並行して作成でき、それぞれが完了するのを待つ必要はありません。私の想定では、ファイルの作成(ディスク操作)はデータの分割よりもはるかに時間がかかります。そしてもちろん、最初にシーケンシャルアプローチが適切でないことを確認する必要があります。

0
TheSean