26

我遇到了一种情况,我有一个非常大的文件,我需要从中读取二进制数据。

因此,我意识到 .NET 中的默认 BinaryReader 实现非常慢。在使用.NET Reflector查看它时,我遇到了这个问题:

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

4 回答 4

20

我在使用 BinaryReader/FileStream 时遇到了类似的性能问题,在分析之后,我发现问题不在于FileStream缓冲,而在于这一行:

while (br.BaseStream.Position < br.BaseStream.Length) {

具体来说,br.BaseStream.Lengtha 上的属性FileStream进行(相对)缓慢的系统调用以获取每个循环的文件大小。将代码更改为此后:

long length = br.BaseStream.Length;
while (br.BaseStream.Position < length) {

并为 使用适当的缓冲区大小FileStream,我获得了与示例类似的性能MemoryStream

于 2012-04-26T23:39:19.857 回答
11

有趣的是,将整个文件读入缓冲区并在内存中遍历它会产生巨大的差异。这是以内存为代价的,但我们有很多。

这让我觉得 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 秒(取决于我猜的磁盘缓存)。这对现在来说已经足够了。

于 2009-08-06T12:21:53.140 回答
10

当您进行文件复制时,会读取大量数据并将其写入磁盘。

您一次读取整个文件四个字节。这势必会慢一些。即使流实现足够智能以进行缓冲,您仍然至少有 500 MB/4 = 131072000 次 API 调用。

仅仅读取一大块数据,然后按顺序遍历它,然后重复直到文件被处理,这不是更明智吗?

于 2009-08-06T11:59:08.307 回答
5

一个警告;您可能需要仔细检查您的CPU 的字节序……假设 little-endian不太安全(想想:安腾等)。

您可能还想看看是否BufferedStream有什么不同(我不确定它会不会)。

于 2009-08-06T11:50:37.433 回答