バイナリファイルをできるだけ速く解析しようとしています。だからこれは私が最初にやろうとしたことです:
using (FileStream filestream = path.OpenRead()) {
using (var d = new GZipStream(filestream, CompressionMode.Decompress)) {
using (MemoryStream m = new MemoryStream()) {
d.CopyTo(m);
m.Position = 0;
using (BinaryReaderBigEndian b = new BinaryReaderBigEndian(m)) {
while (b.BaseStream.Position != b.BaseStream.Length) {
UInt32 value = b.ReadUInt32();
} } } } }
BinaryReaderBigEndian
クラスは、次のように実装されています。
public static class BinaryReaderBigEndian {
public BinaryReaderBigEndian(Stream stream) : base(stream) { }
public override UInt32 ReadUInt32() {
var x = base.ReadBytes(4);
Array.Reverse(x);
return BitConverter.ToUInt32(x, 0);
} }
次に、ReadOnlySpan
ではなくMemoryStream
を使用してパフォーマンスを改善しようとしました。だから、私はやってみました:
using (FileStream filestream = path.OpenRead()) {
using (var d = new GZipStream(filestream, CompressionMode.Decompress)) {
using (MemoryStream m = new MemoryStream()) {
d.CopyTo(m);
int position = 0;
ReadOnlySpan<byte> stream = new ReadOnlySpan<byte>(m.ToArray());
while (position != stream.Length) {
UInt32 value = stream.ReadUInt32(position);
position += 4;
} } } }
BinaryReaderBigEndian
クラスが変更された場所:
public static class BinaryReaderBigEndian {
public override UInt32 ReadUInt32(this ReadOnlySpan<byte> stream, int start) {
var data = stream.Slice(start, 4).ToArray();
Array.Reverse(x);
return BitConverter.ToUInt32(x, 0);
} }
しかし、残念ながら、改善は見られませんでした。それで、私はどこで間違っていますか?
私は自分のコンピューターであなたのコードを測定しました(Intel Q9400、8 GiB RAM、SSD disk、Win10 x64 Home、.NET Framework 4/7/2、 15 MB(解凍時)ファイル)でテストされ、次の結果が得られました。
スパンなしバージョン:520 ms
スパンバージョン:720 ms
したがって、Span
バージョンは実際には遅いです!どうして? new ReadOnlySpan<byte>(m.ToArray())
はファイル全体の追加コピーを実行し、ReadUInt32()
はSpan
の多くのスライスを実行するため(スライスは安価ですが、無料ではありません)。より多くの作業を実行したため、Span
を使用したからといってパフォーマンスが向上することは期待できません。
では、もっと上手くできるのでしょうか?はい。 コードの最も遅い部分は、実際にはガベージコレクションであり、.ToArray()
はReadUInt32()
メソッドで呼び出します。 ReadUInt32()
を自分で実装することで回避できます。これは非常に簡単で、Span
スライスの必要もありません。 new ReadOnlySpan<byte>(m.ToArray())
をnew ReadOnlySpan<byte>(m.GetBuffer()).Slice(0, (int)m.Length);
で置き換えることもできます。これにより、ファイル全体のコピーではなく、安価なスライスが実行されます。したがって、コードは次のようになります。
public static void Read(FileInfo path)
{
using (FileStream filestream = path.OpenRead())
{
using (var d = new GZipStream(filestream, CompressionMode.Decompress))
{
using (MemoryStream m = new MemoryStream())
{
d.CopyTo(m);
int position = 0;
ReadOnlySpan<byte> stream = new ReadOnlySpan<byte>(m.GetBuffer()).Slice(0, (int)m.Length);
while (position != stream.Length)
{
UInt32 value = stream.ReadUInt32(position);
position += 4;
}
}
}
}
}
public static class BinaryReaderBigEndian
{
public static UInt32 ReadUInt32(this ReadOnlySpan<byte> stream, int start)
{
UInt32 res = 0;
for (int i = 0; i < 4; i++)
{
res = (res << 8) | (((UInt32)stream[start + i]) & 0xff);
}
return res;
}
}
これらの変更により、720 msから165 ms(4xもっと早く)。素晴らしいですね。しかし、私たちはもっとうまくやることができます。 MemoryStream
のコピーとインラインを完全に回避し、ReadUInt32()
をさらに最適化できます。
public static void Read(FileInfo path)
{
using (FileStream filestream = path.OpenRead())
{
using (var d = new GZipStream(filestream, CompressionMode.Decompress))
{
var buffer = new byte[64 * 1024];
do
{
int bufferDataLength = FillBuffer(d, buffer);
if (bufferDataLength % 4 != 0)
throw new Exception("Stream length not divisible by 4");
if (bufferDataLength == 0)
break;
for (int i = 0; i < bufferDataLength; i += 4)
{
uint value = unchecked(
(((uint)buffer[i]) << 24)
| (((uint)buffer[i + 1]) << 16)
| (((uint)buffer[i + 2]) << 8)
| (((uint)buffer[i + 3]) << 0));
}
} while (true);
}
}
}
private static int FillBuffer(Stream stream, byte[] buffer)
{
int read = 0;
int totalRead = 0;
do
{
read = stream.Read(buffer, totalRead, buffer.Length - totalRead);
totalRead += read;
} while (read > 0 && totalRead < buffer.Length);
return totalRead;
}
そして今、それは90ミリ秒よりも少なくかかります(オリジナルより8倍速い!)。そしてSpan
なし! Span
は、スライスを実行して配列のコピーを回避できる状況では優れていますが、それを盲目的に使用するだけではパフォーマンスは向上しません。結局のところ、Span
は Array
と同等のパフォーマンス特性 を持つように設計されていますが、それよりも優れているわけではありません(そして、.NET Core 2.1
などの特別なサポートがあるランタイムでのみ) )。