簡體   English   中英

如何在c#中快速讀取二進制文件? (ReadOnlySpan vs MemoryStream)

[英]How to read a binary file quickly in c#? (ReadOnlySpan vs 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);
}  }

但不幸的是,我沒有注意到任何進步。 那么,我在哪里做錯了?

我在我的計算機上測量了你的代碼( Intel Q9400,8 GiB RAM,SSD磁盤,Win10 x64 Home,.NET Framework 4/7/2,測試了15 MB(解壓縮后)文件 ),結果如下:

無跨度版本: 520毫秒
跨度版本: 720毫秒

因此Span版本實際上更慢! 為什么? 因為new ReadOnlySpan<byte>(m.ToArray())執行整個文件的附加副本,並且ReadUInt32()執行Span許多切片(切片很便宜,但不是免費的)。 由於您執行了更多工作,因此您不能僅僅因為使用了Span而預期性能會更好。

那我們能做得更好嗎? 是。 事實證明, 代碼中最慢的部分實際上是垃圾收集,這是由於在ReadUInt32()方法中重復分配由.ToArray()調用創建的4字節Array引起的。 您可以通過自己實現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毫秒下降到165毫秒 (快4倍)。 聽起來很棒,不是嗎? 但我們可以做得更好。 我們可以完全避免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 SpanSpan情況下很有用,它允許執行切片並避免數組復制,但只是盲目地使用它不會提高性能。 畢竟, Span 性能特征與Array相當 ,但並不是更好(並且只在具有特殊支持的運行時,例如.NET Core 2.1 )。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM