.NET中更快(不安全)的BinaryReader
我遇到了一个情况,我有一个非常大的文件,我需要从中读取二进制数据。
因此,我意识到.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秒,现在使用我的FastBinaryReader
22秒)。
对于为什么读这么一个相对较小的文件仍然需要这么长时间,我仍然有点困惑。 如果我将文件从一个磁盘复制到另一个磁盘,则只需几秒钟,因此磁盘吞吐量不是问题。
我进一步内联了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存在缺陷。 请参阅下面接受的答案和我自己的答案(使用解决方案)。
执行文件复制时,会读取大块数据并将其写入磁盘。
您正在一次读取整个文件四个字节。 这肯定会慢一些。 即使流实现足够智能缓冲,您仍然至少有500 MB / 4 = 131072000 API调用。
是不是更明智地只读取大量数据,然后按顺序执行它,并重复直到文件被处理?
我遇到了与BinaryReader / FileStream类似的性能问题,在进行性能分析后,我发现问题不在于FileStream
缓冲,而在于使用此行:
while (br.BaseStream.Position < br.BaseStream.Length) {
具体来说, FileStream
上的属性br.BaseStream.Length
进行(相对)慢的系统调用以获取每个循环上的文件大小。 将代码更改为此后:
long length = br.BaseStream.Length; while (br.BaseStream.Position < length) {
并且为FileStream
使用适当的缓冲区大小,我实现了与MemoryStream
示例类似的性能。
有趣的是,将整个文件读入缓冲区并在内存中进行处理会产生巨大的差异。 这是以记忆为代价的,但我们有很多。
这让我觉得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秒(取决于我猜的磁盘缓存)。现在已经足够好了。
一个警告; 你可能想要仔细检查你的CPU的字节顺序 …假设little-endian 不太安全(想想:itanium等)。
您可能还想看看BufferedStream
有任何区别(我不确定)。