如何在C#中解析文本文件并被绑定?

众所周知,如果您从光盘读取数据,那么您就是IO绑定的,并且您可以比从光盘读取数据更快地处理/解析读取数据。

但这种常识(神话?)并没有反映在我的测试中。 当我读取一个带有double和int的文本文件,每行以空格分隔时,我比物理光盘速度慢得多(因子6)。 文本文件如下所示

1,1 0 2,1 1 3,1 2 

更新我在一次读取时使用完整缓冲区执行ReadFile时包含了PInvoke性能,以获得“真实”性能。

  • ReadFile性能 – ReadFileIntoByteBuffer
  • StringReader.ReadLine性能 – CountLines
  • StringReader.Readline unsafe perf – ParseLinesUnsafe
  • StringReader.Read unsafe char buf – ParseLinesUnsafeCharBuf
  • StringReader.ReadLine +解析性能 – ParseLines

结果是

 Did native read 179,0MB in 0,4s, 484,2MB/s Did read 10.000.000 lines in 1,6s, 112,7MB/s Did parse and read unsafe 179,0MB in 2,3s, 76,5MB/s Did parse and read unsafe char buf 179,0MB in 2,8s, 63,5MB/s Did read and parse 179,0MB in 9,3s, 19,3MB/s 

虽然我确实尝试跳过ParseLinesUnsafeCharBuf中的字符串构造开销,但它仍然比每次分配新字符串的版本慢得多。 它仍然比原来的20 MB更好用最简单的解决方案,但我认为.NET应该能够做得更好。 如果remoe是解析字符串的逻辑,我确实得到258,8 MB / s,这非常好,接近本机速度。 但我没有看到使用不安全代码的方法使我的解析更简单。 我必须处理不完整的线条,这使得它非常复杂。

更新从数字中可以清楚地看出,一个简单的string.split已经花费太多了。 但是StringReader也花了不少钱。 高度优化的解决方案如何看起来更接近实际的光盘速度? 我已经尝试了很多不安全的代码和字符缓冲区的方法,但性能提升可能是30%,但我不需要大小的数量级。 我的解析速度可以达到100MB / s。 这应该可以通过托管代码实现,还是我错了?

用C#解析的速度是否比我从硬盘上读取的速度快? 它是Intel Postville X25M。 CPU是旧的英特尔双核。 我有3 GB RAM Windows 7 .NET 3.5 SP1和.NET 4。

但我确实在普通硬盘上看到了相同的结果。 使用当今的硬盘,线性读取速度可高达400MB / s。 这是否意味着我应该重新构建我的应用程序,以便在实际需要时按需读取数据,而不是以更高GC时间的成本急切地将其读入内存,因为增加的对象图使GC周期更长。

我注意到 ,如果我的托管应用程序使用超过500MB的内存,它的响应速度就会降低。 一个主要因素似乎是对象图的复杂性。 因此,在需要时读取数据可能会更好。 至少这是我对当前数据的结论。

这是代码

 using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Diagnostics; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; using System.ComponentModel; namespace IOBound { class Program { static void Main(string[] args) { string data = @"C:\Source\IOBound\NumericData.txt"; if (!File.Exists(data)) { CreateTestData(data); } int MB = (int) (new FileInfo(data).Length/(1024*1024)); var sw = Stopwatch.StartNew(); uint bytes = ReadFileIntoByteBuffer(data); sw.Stop(); Console.WriteLine("Did native read {0:F1}MB in {1:F1}s, {2:F1}MB/s", bytes/(1024*1024), sw.Elapsed.TotalSeconds, MB / sw.Elapsed.TotalSeconds); sw = Stopwatch.StartNew(); int n = CountLines(data); sw.Stop(); Console.WriteLine("Did read {0:N0} lines in {1:F1}s, {2:F1}MB/s", n, sw.Elapsed.TotalSeconds, MB / sw.Elapsed.TotalSeconds); sw = Stopwatch.StartNew(); ParseLinesUnsafe(data); sw.Stop(); Console.WriteLine("Did parse and read unsafe {0:F1}MB in {1:F1}s, {2:F1}MB/s", MB, sw.Elapsed.TotalSeconds, MB / sw.Elapsed.TotalSeconds); sw = Stopwatch.StartNew(); ParseLinesUnsafeCharBuf(data); sw.Stop(); Console.WriteLine("Did parse and read unsafe char buf {0:F1}MB in {1:F1}s, {2:F1}MB/s", MB, sw.Elapsed.TotalSeconds, MB / sw.Elapsed.TotalSeconds); sw = Stopwatch.StartNew(); ParseLines(data); sw.Stop(); Console.WriteLine("Did read and parse {0:F1}MB in {1:F1}s, {2:F1}MB/s", MB, sw.Elapsed.TotalSeconds, MB / sw.Elapsed.TotalSeconds); } private unsafe static uint ReadFileIntoByteBuffer(string data) { using(var stream = new FileStream(data, FileMode.Open)) { byte[] buf = new byte[200 * 1024 * 1024]; fixed(byte* pBuf = &buf[0]) { uint dwRead = 0; if (ReadFile(stream.SafeFileHandle, pBuf, 200 * 1000 * 1000, out dwRead, IntPtr.Zero) == 0) { throw new Win32Exception(); } return dwRead; } } } private static int CountLines(string data) { using (var reader = new StreamReader(data)) { string line; int count = 0; while ((line = reader.ReadLine()) != null) { count++; } return count; } } unsafe private static void ParseLinesUnsafeCharBuf(string data) { var dobules = new List(); var ints = new List(); using (var reader = new StreamReader(data)) { double d = 0; long a = 0, b = 0; int i = 0; char[] buffer = new char[10*1000*1000]; int readChars = 0; int startIdx = 0; fixed(char *ln = buffer) { while ((readChars = reader.Read(buffer, startIdx, buffer.Length - startIdx)) != 0) { char* pEnd = ln + readChars + startIdx; char* pCur = ln; char* pLineStart = null; while (pCur != pEnd) { a = 0; b = 0; while (pCur != pEnd && *pCur == '\r' || *pCur == '\n') { pCur++; } pLineStart = pCur; while(pCur != pEnd && char.IsNumber(*pCur)) { a = a * 10 + (*pCur++ - '0'); } if (pCur == pEnd || *pCur == '\r') { goto incompleteLine; } if (*pCur++ == ',') { long div = 1; while (pCur != pEnd && char.IsNumber(*pCur)) { b += b * 10 + (*pCur++ - '0'); div *= 10; } if (pCur == pEnd || *pCur == '\r') { goto incompleteLine; } d = a + ((double)b) / div; } else { goto skipRest; } while (pCur != pEnd && char.IsWhiteSpace(*pCur)) { pCur++; } if (pCur == pEnd || *pCur == '\r') { goto incompleteLine; } i = 0; while (pCur != pEnd && char.IsNumber(*pCur)) { i = i * 10 + (*pCur++ - '0'); } if (pCur == pEnd) { goto incompleteLine; } dobules.Add(d); ints.Add(i); continue; incompleteLine: startIdx = (int)(pEnd - pLineStart); Buffer.BlockCopy(buffer, (int)(pLineStart - ln) * 2, buffer, 0, 2 * startIdx); break; skipRest: while (pCur != pEnd && *pCur != '\r') { pCur++; } continue; } } } } } unsafe private static void ParseLinesUnsafe(string data) { var dobules = new List(); var ints = new List(); using (var reader = new StreamReader(data)) { string line; double d=0; long a = 0, b = 0; int ix = 0; while ((line = reader.ReadLine()) != null) { int len = line.Length; fixed (char* ln = line) { while (ix < len && char.IsNumber(ln[ix])) { a = a * 10 + (ln[ix++] - '0'); } if (ln[ix] == ',') { ix++; long div = 1; while (ix < len && char.IsNumber(ln[ix])) { b += b * 10 + (ln[ix++] - '0'); div *= 10; } d = a + ((double)b) / div; } while (ix < len && char.IsWhiteSpace(ln[ix])) { ix++; } int i = 0; while (ix < len && char.IsNumber(ln[ix])) { i = i * 10 + (ln[ix++] - '0'); } dobules.Add(d); ints.Add(ix); } } } } private static void ParseLines(string data) { var dobules = new List(); var ints = new List(); using (var reader = new StreamReader(data)) { string line; char[] sep = new char[] { ' ' }; while ((line = reader.ReadLine()) != null) { var parts = line.Split(sep); if (parts.Length == 2) { dobules.Add( double.Parse(parts[0])); ints.Add( int.Parse(parts[1])); } } } } static void CreateTestData(string fileName) { FileStream fstream = new FileStream(fileName, FileMode.Create); using (StreamWriter writer = new StreamWriter(fstream, Encoding.UTF8)) { for (int i = 0; i < 10 * 1000 * 1000; i++) { writer.WriteLine("{0} {1}", 1.1d + i, i); } } } [DllImport("kernel32.dll", SetLastError = true)] unsafe static extern uint ReadFile(SafeFileHandle hFile, [Out] byte* lpBuffer, uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, IntPtr lpOverlapped); } } 

所以这里有几个问题。 其他人已经评论过Windows的IO缓存以及实际的硬件缓存,所以我将单独留下它。

另一个问题是你测量read()+ parse()的组合操作,并将其与just read()的速度进行比较。 基本上你需要意识到A + B总是超过A(假设非负)。

因此,要了解您是否是IO绑定,您需要了解读取文件需要多长时间。 你做到了。 在我的机器上,您的测试运行大约220毫秒来读取文件。

现在,您需要测量解析许多不同字符串所需的时间。 孤立起来有点棘手。 所以,让我们假设我们将它们放在一起并减去从解析时间读取所需的时间。 此外,我们不是要测量你对数据做了什么,而只是测量解析,所以抛出List和List,让我们只是解析。 在我的机器上运行它大约1000毫秒,少于220毫秒的读取,你的解析代码每100万行大约需要780毫秒。

那为什么它这么慢(比读取慢3-4倍)? 再次让我们消除一些东西。 注释掉int.Parse和double.Parse并再次运行。 这比读取时间220减少了460毫秒,我们现在处理240毫秒。 当然’解析’只调用string.Split()。 Hrmmm看起来像string.Split会花费你和磁盘IO一样多,考虑到.NET如何处理字符串也就不足为奇了。

那么C#可以比从磁盘读取更快或更快地解析吗? 是的,它可以,但你必须变得讨厌。 你看到int.Parse和double.Parse受到它们具有文化意识的事实的影响。 由于这一点以及这些解析程序处理许多格式的事实,它们在你的例子中有些昂贵。 我的意思是说我们正在每微秒(百万分之一秒)解析一个double和int,这通常都不错。

因此,为了匹配磁盘读取的速度(因此是IO绑定),我们需要重写处理文本行的方式。 这是一个令人讨厌的例子,但它适用于你的例子……

 int len = line.Length; fixed (char* ln = line) { double d; long a = 0, b = 0; int ix = 0; while (ix < len && char.IsNumber(ln[ix])) a = a * 10 + (ln[ix++] - '0'); if (ln[ix] == '.') { ix++; long div = 1; while (ix < len && char.IsNumber(ln[ix])) { b += b * 10 + (ln[ix++] - '0'); div *= 10; } d = a + ((double)b)/div; } while (ix < len && char.IsWhiteSpace(ln[ix])) ix++; int i = 0; while (ix < len && char.IsNumber(ln[ix])) i = i * 10 + (ln[ix++] - '0'); } 

运行这个糟糕的代码会产生大约450毫秒的运行时间,或大约2n的读取时间。 所以,假装你认为上面的代码片段是可以接受的(我希望你不要上帝),你可以让一个线程读取字符串和另一个解析,你就会接近IO绑定。 将两个线程放在解析上,您将受到IO绑定。 如果你这样做是另一个问题。

那么让我们回到你原来的问题:

众所周知,如果您从光盘读取数据,那么您就是IO绑定的,并且您可以比从光盘读取数据更快地处理/解析读取数据。

但这个共同的智慧(神话?)

好吧,不,我不会称之为神话。 事实上,我会辩论你的原始代码仍然是IO Bound。 您碰巧独立运行测试,因此影响很小,是从设备读取时间的1/6。 但是考虑一下如果磁盘繁忙会发生什么? 如果你的防病毒扫描程序正在翻阅每个文件怎么办? 简单地说,你的程序会随着硬盘活动的增加而变慢,并且可能会成为IO界限。

恕我直言,这种“共同智慧”的原因是:

在写入上比在读取上更容易获得IO绑定。

写入设备需要更长的时间,并且通常比生成数据更昂贵。 如果要查看IO Bound的操作,请查看“CreateTestData”方法。 您的CreateTestData方法将数据写入磁盘需要2倍的时间,而不仅仅是调用String.Format(...)。 这是完全缓存。 关闭缓存( FileOptions.WriteThrough )并再次尝试...现在CreateTestData慢3x-4x。 使用以下方法自行尝试:

 static int CreateTestData(string fileName) { FileStream fstream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough); using (StreamWriter writer = new StreamWriter(fstream, Encoding.UTF8)) { for (int i = 0; i < linecount; i++) { writer.WriteLine("{0} {1}", 1.1d + i, i); } } return linecount; } static int PrintTestData(string fileName) { for (int i = 0; i < linecount; i++) { String.Format("{0} {1}", 1.1d + i, i); } return linecount; } 

这仅适用于初学者,如果您真的想要获得IO绑定,则可以开始使用直接IO。 请参阅使用FILE_FLAG_NO_BUFFERING的CreateFile文档。 当您开始绕过硬件缓存并等待IO完成时,写入速度会慢得多。 这是传统数据库写入速度非常慢的一个主要原因。 他们必须强制硬件完成写入并等待它。 只有这样,他们才能将事务称为“已提交”,数据位于物理设备上的文件中。

更新

好的阿洛伊斯,看起来你只是在寻找你能走得多快。 为了更快,您需要停止处理字符串和字符并删除分配以加快速度。 下面的代码改进了上面的行/字符解析器大约一个数量级(在计数行上增加了大约30ms),同时在堆上只分配了一个缓冲区。

警告你需要意识到我正在certificate它可以快速完成。 我建议你走这条路。 此代码有一些严重的限制和/或错误。 就像当你以“1.2589E + 19”的forms击中双冠时会发生什么? 坦率地说,我认为你应该坚持使用原始代码而不必担心尝试优化它。 或者将文件格式更改为二进制而不是文本(请参阅BinaryWriter )。 如果您使用的是二进制文件,则可以使用以下代码的变体与BitConvert.ToDouble / ToInt32 ,它会更快。

 private static unsafe int ParseFast(string data) { int count = 0, valid = 0, pos, stop, temp; byte[] buffer = new byte[ushort.MaxValue]; const byte Zero = (byte) '0'; const byte Nine = (byte) '9'; const byte Dot = (byte)'.'; const byte Space = (byte)' '; const byte Tab = (byte) '\t'; const byte Line = (byte) '\n'; fixed (byte *ptr = buffer) using (Stream reader = File.OpenRead(data)) { while (0 != (temp = reader.Read(buffer, valid, buffer.Length - valid))) { valid += temp; pos = 0; stop = Math.Min(buffer.Length - 1024, valid); while (pos < stop) { double d; long a = 0, b = 0; while (pos < valid && ptr[pos] >= Zero && ptr[pos] <= Nine) a = a*10 + (ptr[pos++] - Zero); if (ptr[pos] == Dot) { pos++; long div = 1; while (pos < valid && ptr[pos] >= Zero && ptr[pos] <= Nine) { b += b*10 + (ptr[pos++] - Zero); div *= 10; } d = a + ((double) b)/div; } else d = a; while (pos < valid && (ptr[pos] == Space || ptr[pos] == Tab)) pos++; int i = 0; while (pos < valid && ptr[pos] >= Zero && ptr[pos] <= Nine) i = i*10 + (ptr[pos++] - Zero); DoSomething(d, i); while (pos < stop && ptr[pos] != Line) pos++; while (pos < stop && !(ptr[pos] >= Zero && ptr[pos] <= Nine)) pos++; } if (pos < valid) Buffer.BlockCopy(buffer, pos, buffer, 0, valid - pos); valid -= pos; } } return count; } 

正如其他答案/评论所提到的那样,您可能无论如何都要从缓存中读取文件,因此磁盘/ SDRAM速度不是限制因素。

解析文件时,您正在从堆中进行更多分配(当您拆分字符串时,以及将自动框解析值添加到列表中时),而不是只计算行数。 这些堆分配的成本可能会导致两次传递之间的性能差异。

可以比从磁盘读取的速度更快地进行解析,但针对速度进行优化的解析器会因内存使用情况而变化。

只是几个建议(如果你愿意接受更复杂的实施)……

  • 您可以使用LinkedList而不是List来避免在Add -ing时重新分配/复制。
  • 用搜索分隔符的手写代码替换Split ,然后使用Substring提取“字段”。 您只需要搜索单个分隔符,并且事先知道字段数,因此Split对您来说太笼统了。
  • 使用Read而不是ReadLine这样您就可以重复使用相同的缓冲区,并避免为每一行分配新的字符串。
  • 如果你真的是表现良好的,那么将解析分成并发Task 。 当你在它的时候,把文件读取也放在它自己的任务中。

关于绩效只有一个“共同的智慧” – 决定你想要过程的速度和衡量一切,根据收集的数据采取行动并重复。

截至目前,您的代码可能会为每个读取的字节分配10倍内存(有许多层读取器,字符串读取器等)。 如果你想要绝对速度,你可能需要消除大部分的重新分配。 我会首先重写代码以便一直使用单个读取器并测量性能是否足够好。

如果你使用大文件,你可以得到的最快的是AFAIK MemoryMappedFile类(.NET 4中的新增function)。

通过这个你基本上直接使用OS FileSystem缓存管理器内存页…如果这不够快,那么你需要编写自己的文件系统…

对于GC,可以通过app.config自定义行为 – 例如:

    

有关GC自定义选项,请参阅http://msdn.microsoft.com/en-us/library/6bs4szyc.aspx – esp。 /

向它投掷一个分析器,看起来大部分时间确实用于解析文件。

我尝试使用已读取的byte[]而不是ParseLines方法的路径在MemoryStream传递样本,并且从文件路径解析和从内存字节解析之间的差异可以忽略不计。

换句话说,它是完成的处理,而不是占用大量时间的阅读。

我把这个分析器扔到了它: http : //code.google.com/p/slimtune/

从那里我可以看到ParseLines方法在内存流上调用时具有以下时序:

System.Io.StreamReader.ReadLine() 25.34%
System.Double.Parse 23.86%
System.Number.ParseInt32 21.72%
System.String.Split 20.91%
– 其他一些不太重要的方法 –

所以这告诉我的是,即使从内存流中读取行也有点慢,就像大多数字符串操作一样。

我怀疑缓存会影响您正在进行的测量。 硬盘具有用于缓存最近访问的数据的RAM,操作系统也是如此。

为了更好的测试,您需要在测试之间重新启动。 我建议完全断电以确保磁盘的RAM也被清除。