【发布时间】:2011-08-22 20:37:36
【问题描述】:
众所周知,如果您从磁盘读取数据,您会受到 IO 限制,并且您可以比从磁盘读取数据更快地处理/解析读取的数据。
但是我的测试并没有反映这种普遍的智慧(神话?)。当我读取一个文本文件时,每行中用空格分隔的 double 和 int 比我的物理磁盘速度慢得多(因子 6)。 文本文件如下所示
1,1 0
2,1 1
3,1 2
更新 当我在一次读取中使用完整缓冲区执行 ReadFile 以获得“真实”性能时,我已经包含了 PInvoke 性能。
- ReadFile 性能 - ReadFileIntoByteBuffer
- StringReader.ReadLine 性能 - CountLines
- StringReader.Readline 不安全性能 - 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 应该能够做得更好。如果去掉解析字符串的逻辑,我确实得到 258,8 MB/s,这非常好,接近原生速度。但是我看不到使用不安全代码使我的解析更简单的方法。我确实必须处理不完整的行,这使得它非常复杂。
更新 从数字中可以清楚地看出,一个简单的 string.split 的成本已经太高了。但是 StringReader 也确实要花很多钱。高度优化的解决方案如何看起来更接近真实的磁盘速度?我已经尝试了许多使用不安全代码和字符缓冲区的方法,但性能提升可能是 30%,但没有我需要的数量级。我会接受 100MB/s 的解析速度。这应该可以通过托管代码实现,还是我错了?
C# 的解析速度是否比我从硬盘读取的速度更快?它是英特尔 Postville X25M。 CPU 是和旧的英特尔双核。我有 3 GB RAM Windows 7 .NET 3.5 SP1 和 .NET 4。
但我确实在普通硬盘上也看到了相同的结果。当今硬盘的线性读取速度可高达 400MB/s。这是否意味着我应该重构我的应用程序以在实际需要时按需读取数据,而不是因为增加的对象图使 GC 周期更长而以更高的 GC 时间为代价急切地将其读取到内存中。
I have noticed 如果我的托管应用程序使用超过 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<double>();
var ints = new List<int>();
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<double>();
var ints = new List<int>();
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<double>();
var ints = new List<int>();
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);
}
}
【问题讨论】:
-
“常识”确实被广泛接受,甚至更广泛地是错误的。磁盘吞吐量已经变得相当高,即使您没有足够幸运拥有 SATA-3 SSD,您也可以将 RAM 用作磁盘缓存。这也不是 C# 问题,例如,请参阅 stackoverflow.com/questions/4340396 stackoverflow.com/questions/6218667 和 stackoverflow.com/questions/5678932 当然,一些加速处理的技术(将磁盘访问作为限制因素)在托管代码中更加困难。
-
我认为明确提及您的硬盘是 SSD 是个好主意 ;-)
-
底线:可以比现代 I/O 子系统更快地处理数据(也不如基于 RAM 的磁盘缓存快)。但是您需要非常注重性能才能这样做,并且不要使用通用库函数。
-
@Meta-Knight:问题中提到的 400MB/s 必须是 SSD 或磁盘阵列。但结果中的 120MB/s,仅用于读取,如今使用高端消费级旋转磁盘可以实现。
-
我很困惑是什么让你相信第二次测试应该和第一次测试一样快。我假设您认为 I/O 是循环中如此巨大的组成部分,以至于解析几乎可以忽略不计?如果是这样,你假设这个的理由是什么?当然,“传统智慧”对于这种测试来说是非常模糊的。有很多需要考虑。
标签: c# performance file-io