.NET の高速 (安全ではない) BinaryReader
-
11-09-2019 - |
質問
バイナリ データを読み取る必要があるかなり大きなファイルがあるという状況に遭遇しました。
その結果、.NET のデフォルトの BinaryReader 実装はかなり遅いことがわかりました。それを見てみると、 .NETリフレクター 私はこれを見つけました:
public virtual int ReadInt32()
{
if (this.m_isMemoryStream)
{
MemoryStream stream = this.m_stream as MemoryStream;
return stream.InternalReadInt32();
}
this.FillBuffer(4);
return (((this.m_buffer[0] | (this.m_buffer[1] << 8)) | (this.m_buffer[2] << 0x10)) | (this.m_buffer[3] << 0x18));
}
32 ビット CPU が発明されて以来、コンピュータが 32 ビット値で動作するようにどのように設計されてきたかを考えると、これは非常に非効率であるように思えます。
そこで、代わりに次のようなコードを使用して独自の (安全でない) FastBinaryReader クラスを作成しました。
public unsafe class FastBinaryReader :IDisposable
{
private static byte[] buffer = new byte[50];
//private Stream baseStream;
public Stream BaseStream { get; private set; }
public FastBinaryReader(Stream input)
{
BaseStream = input;
}
public int ReadInt32()
{
BaseStream.Read(buffer, 0, 4);
fixed (byte* numRef = &(buffer[0]))
{
return *(((int*)numRef));
}
}
...
}
どちらのほうがはるかに速いです。500 MB のファイルを読み取るのにかかる時間を 5 ~ 7 秒短縮することができましたが、それでも全体としてはかなり遅いです(最初は 29 秒、現在は約 22 秒かかります) FastBinaryReader
).
このような比較的小さなファイルを読み取るのになぜこれほど時間がかかるのか、いまだに不思議に思っています。あるディスクから別のディスクにファイルをコピーする場合、所要時間はわずか数秒であるため、ディスクのスループットは問題になりません。
ReadInt32 などをさらにインライン化しました。を呼び出し、最終的にこのコードになりました。
using (var br = new FastBinaryReader(new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, 0x10000, FileOptions.SequentialScan)))
while (br.BaseStream.Position < br.BaseStream.Length)
{
var doc = DocumentData.Deserialize(br);
docData[doc.InternalId] = doc;
}
}
public static DocumentData Deserialize(FastBinaryReader reader)
{
byte[] buffer = new byte[4 + 4 + 8 + 4 + 4 + 1 + 4];
reader.BaseStream.Read(buffer, 0, buffer.Length);
DocumentData data = new DocumentData();
fixed (byte* numRef = &(buffer[0]))
{
data.InternalId = *((int*)&(numRef[0]));
data.b = *((int*)&(numRef[4]));
data.c = *((long*)&(numRef[8]));
data.d = *((float*)&(numRef[16]));
data.e = *((float*)&(numRef[20]));
data.f = numRef[24];
data.g = *((int*)&(numRef[25]));
}
return data;
}
これをさらに高速化する方法についてさらにアイデアはありますか?データは線形、固定サイズ、シーケンシャルであるため、マーシャリングを使用してファイル全体をカスタム構造上のメモリに直接マップできるかもしれないと考えていました。
解決済み: FileStream のバッファリング/BufferedStream に欠陥があるという結論に達しました。以下の受け入れられた回答と私自身の回答(解決策付き)を参照してください。
解決
ファイルコピーを実行すると、大量のデータがディスクに読み取られ、ディスクに書き込まれます。
ファイル全体を一度に 4 バイトずつ読み取ることになります。これは遅くなるはずです。ストリームの実装がバッファリングできるほど賢くても、少なくとも 500 MB/4 = 131072000 の API 呼び出しが発生します。
大きなデータの塊を読み取って、それを順番に処理し、ファイルが処理されるまで繰り返す方が賢明ではないでしょうか?
他のヒント
BinaryReader/FileStream でも同様のパフォーマンスの問題が発生しましたが、プロファイリングの結果、問題は BinaryReader/FileStream ではないことがわかりました。 FileStream
バッファリングですが、代わりに次の行を使用します。
while (br.BaseStream.Position < br.BaseStream.Length) {
具体的には、その物件 br.BaseStream.Length
に FileStream
各ループでファイル サイズを取得するために (比較的) 遅いシステム コールを実行します。コードを次のように変更した後:
long length = br.BaseStream.Length;
while (br.BaseStream.Position < length) {
適切なバッファ サイズを使用して、 FileStream
, と同様のパフォーマンスを達成しました。 MemoryStream
例。
興味深いことに、ファイル全体をバッファに読み取ってメモリ内で処理すると、大きな違いが生じました。これにはメモリが犠牲になりますが、メモリは十分にあります。
このことを考えると、FileStream (さらに言えば BufferedStream) のバッファ実装に欠陥があるのではないかと思われます。どのサイズのバッファを試してもパフォーマンスは依然として悪かったからです。
using (var br = new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, 0x10000, FileOptions.SequentialScan))
{
byte[] buffer = new byte[br.Length];
br.Read(buffer, 0, buffer.Length);
using (var memoryStream = new MemoryStream(buffer))
{
while (memoryStream.Position < memoryStream.Length)
{
var doc = DocumentData.Deserialize(memoryStream);
docData[doc.InternalId] = doc;
}
}
}
22 秒から 2 ~ 5 秒に短縮されました (おそらくディスク キャッシュに依存すると思います)。今のところはそれで十分です。
注意点が 1 つあります。再確認してみてはいかがでしょうか CPUのエンディアンネス...リトルエンディアンがそうでないと仮定すると とても 安全です(次のように考えてください:イタニウムなど)。
どうかも確認したいかもしれません BufferedStream
違いはあります(違いがあるかどうかはわかりません)。