如何在C#中快速读取二进制文件? (ReadOnlySpan与MemoryStream)

时间:2018-11-21 16:02:18

标签: c# html .net memorystream

我正在尝试尽可能快地解析二进制文件。所以这是我第一次尝试做的事情:

service

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

但是,不幸的是,我没有发现任何改善。那么,我在哪里做错了?

1 个答案:

答案 0 :(得分:5)

我在我的计算机( Intel Q9400、8 GiB RAM,SSD磁盘,Win10 x64 Home,.NET Framework 4/7/2)上对您的代码进行了一些测量,并测试了15 MB(解压缩后)的文件< / em>),结果如下:

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

因此Span版本实际上要慢一些!为什么?因为new ReadOnlySpan<byte>(m.ToArray())执行整个文件的附加副本,并且ReadUInt32()也执行Span的许多切片(切片很便宜,但不是免费的)。由于您执行了更多的工作,所以不能仅仅因为使用Span而期望性能会更好。

那么我们可以做得更好吗?是。事实证明,代码中最慢的部分实际上是垃圾收集,这是由于重复分配了由Array方法中的.ToArray()调用创建的4字节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毫秒降到了 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倍!)。而且没有SpanSpan在允许执行切片并避免数组复制的情况下非常有用,但仅凭盲目使用它并不能提高性能。毕竟,Span被设计为具有performance characteristics on par with Array,但并不是更好(并且仅在具有特殊支持的运行时,例如.NET Core 2.1上)。