我有一个很好的任务,就是如何处理加载到应用程序脚本编辑器中的大文件(对于我们用于快速宏的内部产品,它就像VBA)。大多数文件大约300-400 KB,这是很好的加载。但是当它们超过100 MB时,这个过程很难(正如你所期待的那样)。
该文件被读取并被推入RichTextBox然后导航 - 不要过于担心这部分。
编写初始代码的开发人员只是使用StreamReader并执行
[Reader].ReadToEnd()
可能需要很长时间才能完成。
我的任务是打破这段代码,将其以块的形式读入缓冲区并显示一个带有取消选项的进度条。
一些假设:
现在提出问题:
这些(在您的专业意见中)是好主意吗?我在过去读过Streams的内容时遇到过一些问题,因为它总会错过最后几个字节或者其他东西,但如果是这样的话,我会问另一个问题。
答案 0 :(得分:161)
您可以使用BufferedStream提高读取速度,如下所示:
using (FileStream fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (BufferedStream bs = new BufferedStream(fs))
using (StreamReader sr = new StreamReader(bs))
{
string line;
while ((line = sr.ReadLine()) != null)
{
}
}
2013年3月更新
我最近编写了用于读取和处理(搜索文本)1 GB-ish文本文件的代码(比这里涉及的文件大得多),并通过使用生产者/消费者模式获得了显着的性能提升。生产者任务使用BufferedStream
以文本行读取,并将其交给执行搜索的单独的消费者任务。
我以此为契机学习TPL数据流,非常适合快速编码这种模式。
为什么BufferedStream更快
缓冲区是内存中用于缓存数据的字节块,从而减少了对操作系统的调用次数。缓冲区可提高读写性能。缓冲区可用于读取或写入,但不能同时使用。 BufferedStream的Read和Write方法自动维护缓冲区。
2014年12月更新:您的里程可能会发生变化
根据评论,FileStream应该在内部使用BufferedStream。在首次提供此答案时,我通过添加BufferedStream测量了显着的性能提升。当时我在32位平台上瞄准.NET 3.x.今天,在64位平台上面向.NET 4.5,我没有看到任何改进。
相关强>
我遇到过这样一种情况:从ASP.Net MVC操作将大量生成的CSV文件传输到Response流非常慢。在这种情况下,添加BufferedStream可将性能提高100倍。有关详情,请参阅Unbuffered Output Very Slow
答案 1 :(得分:14)
您说在加载大文件时,系统会要求您显示进度条。这是因为用户真的想要查看文件加载的确切百分比,还是仅仅因为他们想要视觉反馈才能发生某些事情?
如果后者为真,则解决方案变得更加简单。只需在后台线程上执行reader.ReadToEnd()
,并显示一个选取框类型的进度条而不是正确的进度条。
我提出这一点,因为根据我的经验,这种情况经常发生。当您编写数据处理程序时,用户肯定会对%完整数字感兴趣,但对于简单但缓慢的UI更新,他们更可能只想知道计算机没有崩溃。 : - )
答案 2 :(得分:14)
如果您阅读performance and benchmark stats on this website,您会发现读取的最快方式(因为阅读,写入和处理都不同),文本文件如下代码片段:
using (StreamReader sr = File.OpenText(fileName))
{
string s = String.Empty;
while ((s = sr.ReadLine()) != null)
{
//do your stuff here
}
}
所有大约9种不同的方法都是基准标记,但是大多数时候这种方法似乎都提前了,甚至像其他读者所提到的那样执行缓冲读取器。
答案 3 :(得分:8)
对于二进制文件,我发现读取它们的最快方法就是这个。
MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(file);
MemoryMappedViewStream mms = mmf.CreateViewStream();
using (BinaryReader b = new BinaryReader(mms))
{
}
在我的测试中它的速度提高了数百倍。
答案 4 :(得分:6)
使用后台工作程序并只读取有限数量的行。仅在用户滚动时阅读更多内容。
并尝试永远不要使用ReadToEnd()。这是你认为“他们为什么要成功?”的功能之一;它是一个script kiddies'助手,可以很好地处理小事情,但正如你所看到的那样,对于大文件来说很糟糕......
那些告诉你使用StringBuilder的人需要更频繁地阅读MSDN:
表现考虑因素
Concat和AppendFormat方法都将新数据连接到现有的String或StringBuilder对象。 String对象并置操作始终从现有字符串和新数据创建新对象。 StringBuilder对象维护一个缓冲区以容纳新数据的串联。如果房间可用,新数据将附加到缓冲区的末尾;否则,分配一个新的较大缓冲区,将原始缓冲区中的数据复制到新缓冲区,然后将新数据附加到新缓冲区。
String或StringBuilder对象的串联操作的性能取决于内存分配发生的频率。
字符串连接操作始终分配内存,而StringBuilder连接操作仅在StringBuilder对象缓冲区太小而无法容纳新数据时分配内存。因此,如果连接固定数量的String对象,则String类更适合并置操作。在这种情况下,编译器甚至可以将单个连接操作组合成单个操作。如果连接任意数量的字符串,则StringBuilder对象最好用于连接操作;例如,如果循环连接随机数量的用户输入字符串。
这意味着巨大的内存分配,大量使用交换文件系统,模拟硬盘驱动器的部分就像RAM内存,但硬盘驱动器非常慢。
对于将系统用作单用户的人来说,StringBuilder选项看起来很好,但是当你有两个或更多用户同时阅读大文件时,你会遇到问题。
答案 5 :(得分:5)
这应该足以让你入门。
class Program
{
static void Main(String[] args)
{
const int bufferSize = 1024;
var sb = new StringBuilder();
var buffer = new Char[bufferSize];
var length = 0L;
var totalRead = 0L;
var count = bufferSize;
using (var sr = new StreamReader(@"C:\Temp\file.txt"))
{
length = sr.BaseStream.Length;
while (count > 0)
{
count = sr.Read(buffer, 0, bufferSize);
sb.Append(buffer, 0, count);
totalRead += count;
}
}
Console.ReadKey();
}
}
答案 6 :(得分:4)
查看以下代码段。你提到过Most files will be 30-40 MB
。这声称在英特尔四核处理器上1.4秒内读取180 MB:
private int _bufferSize = 16384;
private void ReadFile(string filename)
{
StringBuilder stringBuilder = new StringBuilder();
FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read);
using (StreamReader streamReader = new StreamReader(fileStream))
{
char[] fileContents = new char[_bufferSize];
int charsRead = streamReader.Read(fileContents, 0, _bufferSize);
// Can't do much with 0 bytes
if (charsRead == 0)
throw new Exception("File is 0 bytes");
while (charsRead > 0)
{
stringBuilder.Append(fileContents);
charsRead = streamReader.Read(fileContents, 0, _bufferSize);
}
}
}
答案 7 :(得分:3)
你可能最好使用处理here的内存映射文件。内存映射文件支持将在.NET 4中使用(我想...我听说通过其他人谈论它)因此这个包装器使用p / invokes来完成同样的工作..
编辑:在MSDN上查看其工作原理,这里是blog条目,说明在即将发布的.NET 4中它是如何完成的发布。我之前给出的链接是pinvoke周围的包装来实现这一点。您可以将整个文件映射到内存中,并在滚动文件时将其视为滑动窗口。
答案 8 :(得分:2)
虽然最受欢迎的答案是正确的,但它缺乏多核处理的使用。就我而言,我使用 PLink 有 12 个内核:
Parallel.ForEach(
File.ReadLines(filename), //returns IEumberable<string>: lazy-loading
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
(line, state, index) =>
{
//process line value
}
);
值得一提的是,我在一个面试问题中得到了这一点,要求返回出现次数最多的前 10 名:
var result = new ConcurrentDictionary<string, int>(StringComparer.InvariantCultureIgnoreCase);
Parallel.ForEach(
File.ReadLines(filename),
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
(line, state, index) =>
{
result.AddOrUpdate(line, 1, (key, val) => val + 1);
}
);
return result
.OrderByDescending(x => x.Value)
.Take(10)
.Select(x => x.Value);
Benchmarking:
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i7-8700K CPU 3.70GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
[Host] : .NET Framework 4.8 (4.8.4250.0), X64 RyuJIT
DefaultJob : .NET Framework 4.8 (4.8.4250.0), X64 RyuJIT
方法 | 平均 | 错误 | StdDev | Gen 0 | Gen 1 | 第 2 代 | 已分配 |
---|---|---|---|---|---|---|---|
GetTopWordsSync | 33.03 秒 | 0.175 秒 | 0.155 秒 | 1194000 | 314000 | 7000 | 7.06 GB |
GetTopWordsParallel | 10.89 秒 | 0.121 秒 | 0.113 秒 | 1225000 | 354000 | 8000 | 7.18 GB |
正如您所见,性能提高了 75%。
答案 9 :(得分:1)
迭代器可能适用于此类工作:
public static IEnumerable<int> LoadFileWithProgress(string filename, StringBuilder stringData)
{
const int charBufferSize = 4096;
using (FileStream fs = File.OpenRead(filename))
{
using (BinaryReader br = new BinaryReader(fs))
{
long length = fs.Length;
int numberOfChunks = Convert.ToInt32((length / charBufferSize)) + 1;
double iter = 100 / Convert.ToDouble(numberOfChunks);
double currentIter = 0;
yield return Convert.ToInt32(currentIter);
while (true)
{
char[] buffer = br.ReadChars(charBufferSize);
if (buffer.Length == 0) break;
stringData.Append(buffer);
currentIter += iter;
yield return Convert.ToInt32(currentIter);
}
}
}
}
您可以使用以下方法调用它:
string filename = "C:\\myfile.txt";
StringBuilder sb = new StringBuilder();
foreach (int progress in LoadFileWithProgress(filename, sb))
{
// Update your progress counter here!
}
string fileData = sb.ToString();
在加载文件时,迭代器会将进度编号从0返回到100,您可以使用它来更新进度条。循环完成后,StringBuilder将包含文本文件的内容。
另外,因为您需要文本,我们可以使用BinaryReader读取字符,这将确保您的缓冲区在读取任何多字节字符(UTF-8,UTF-16等时正确排列)。
这一切都是在不使用后台任务,线程或复杂的自定义状态机的情况下完成的。
答案 10 :(得分:1)
答案 11 :(得分:0)
所有优秀答案!然而,对于寻找答案的人来说,这些似乎有些不完整。
作为标准字符串,只能是大小X,2Gb到4Gb,具体取决于您的配置,这些答案并不能真正满足OP的问题。一种方法是使用字符串列表:
List<string> Words = new List<string>();
using (StreamReader sr = new StreamReader(@"C:\Temp\file.txt"))
{
string line = string.Empty;
while ((line = sr.ReadLine()) != null)
{
Words.Add(line);
}
}
有些人可能想要在处理时进行Tokenise和拆分。字符串列表现在可以包含非常大量的文本。