web-dev-qa-db-ja.com

C#でバイナリファイルをすばやく読み取る方法は? (ReadOnlySpanとMemoryStream)

バイナリファイルをできるだけ速く解析しようとしています。だからこれは私が最初にやろうとしたことです:

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

しかし、残念ながら、改善は見られませんでした。それで、私はどこで間違っていますか?

13
heliosophist

私は自分のコンピューターであなたのコードを測定しました(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は、スライスを実行して配列のコピーを回避できる状況では優れていますが、それを盲目的に使用するだけではパフォーマンスは向上しません。結局のところ、SpanArrayと同等のパフォーマンス特性 を持つように設計されていますが、それよりも優れているわけではありません(そして、.NET Core 2.1などの特別なサポートがあるランタイムでのみ) )。

8
Ňuf