写入文件C#的性能
本文关键字:性能 文件 | 更新日期: 2023-09-27 18:25:48
我的情况概述:
我的任务是从文件中读取字符串,并将它们重新格式化为更有用的格式。重新格式化输入后,我必须将其写入输出文件。
下面是必须做的一个例子。文件行示例:
ANO=2010;CPF=17834368168;YEARS=2010;2009;2008;2007;2006 <?xml version='1.0' encoding='ISO-8859-1'?><QUERY><RESTITUICAO><CPF>17834368168</CPF><ANO>2010</ANO><SITUACAODECLARACAO>Sua declaração não consta na base de dados da Receita Federal</SITUACAODECLARACAO><DATACONSULTA>05/01/2012</DATACONSULTA></RESTITUICAO><RESTITUICAO><CPF>17834368168</CPF><ANO>2009</ANO><SITUACAODECLARACAO>Sua declaração não consta na base de dados da Receita Federal</SITUACAODECLARACAO><DATACONSULTA>05/01/2012</DATACONSULTA></RESTITUICAO><RESTITUICAO><CPF>17834368168</CPF><ANO>2008</ANO><SITUACAODECLARACAO>Sua declaração não consta na base de dados da Receita Federal</SITUACAODECLARACAO><DATACONSULTA>05/01/2012</DATACONSULTA></RESTITUICAO><RESTITUICAO><CPF>17834368168</CPF><ANO>2007</ANO><SITUACAODECLARACAO>Sua declaração consta como Pedido de Regularização(PR), na base de dados da Secretaria da Receita Federal do Brasil</SITUACAODECLARACAO><DATACONSULTA>05/01/2012</DATACONSULTA></RESTITUICAO><RESTITUICAO><CPF>17834368168</CPF><ANO>2006</ANO><SITUACAODECLARACAO>Sua declaração não consta na base de dados da Receita Federal</SITUACAODECLARACAO><DATACONSULTA>05/01/2012</DATACONSULTA></RESTITUICAO><STATUS><RESULT>TRUE</RESULT><MESSAGE></MESSAGE></STATUS></QUERY>
该输入文件每行都有两个重要信息:CPF
,它是我将使用的文档编号,以及XML
文件(它表示对数据库中文档的查询的返回)。
我必须实现的目标:
本old format
中的每个文档都有一个XML
,其中包含所有年份(2006年至2010年)的查询返回。重新格式化后,每条输入行被转换为5条输出行:
CPF=17834368168;YEARS=2010; <?xml version='1.0' encoding='ISO-8859-1'?><QUERY><RESTITUICAO><CPF>17834368168</CPF><ANO>2010</ANO><SITUACAODECLARACAO>Sua declaração não consta na base de dados da Receita Federal</SITUACAODECLARACAO><DATACONSULTA>05/01/2012</DATACONSULTA></RESTITUICAO><STATUS><RESULT>TRUE</RESULT><MESSAGE></MESSAGE></STATUS></QUERY>
CPF=17834368168;YEARS=2009; <?xml version='1.0' encoding='ISO-8859-1'?><QUERY><RESTITUICAO><CPF>17834368168</CPF><ANO>2009</ANO><SITUACAODECLARACAO>Sua declaração não consta na base de dados da Receita Federal</SITUACAODECLARACAO><DATACONSULTA>05/01/2012</DATACONSULTA></RESTITUICAO><STATUS><RESULT>TRUE</RESULT><MESSAGE></MESSAGE></STATUS></QUERY>
CPF=17834368168;YEARS=2008; <?xml version='1.0' encoding='ISO-8859-1'?><QUERY><RESTITUICAO><CPF>17834368168</CPF><ANO>2008</ANO><SITUACAODECLARACAO>Sua declaração não consta na base de dados da Receita Federal</SITUACAODECLARACAO><DATACONSULTA>05/01/2012</DATACONSULTA></RESTITUICAO><STATUS><RESULT>TRUE</RESULT><MESSAGE></MESSAGE></STATUS></QUERY>
CPF=17834368168;YEARS=2007; <?xml version='1.0' encoding='ISO-8859-1'?><QUERY><RESTITUICAO><CPF>17834368168</CPF><ANO>2007</ANO><SITUACAODECLARACAO>Sua declaração consta como Pedido de Regularização(PR), na base de dados da Secretaria da Receita Federal do Brasil</SITUACAODECLARACAO><DATACONSULTA>05/01/2012</DATACONSULTA></RESTITUICAO><STATUS><RESULT>TRUE</RESULT><MESSAGE></MESSAGE></STATUS></QUERY>
CPF=17834368168;YEARS=2006; <?xml version='1.0' encoding='ISO-8859-1'?><QUERY><RESTITUICAO><CPF>17834368168</CPF><ANO>2006</ANO><SITUACAODECLARACAO>Sua declaração não consta na base de dados da Receita Federal</SITUACAODECLARACAO><DATACONSULTA>05/01/2012</DATACONSULTA></RESTITUICAO><STATUS><RESULT>TRUE</RESULT><MESSAGE></MESSAGE></STATUS></QUERY>
一行,包含每年有关该文档的信息。因此,基本上,输出文件的长度是输入文件的5倍。
性能问题:
每个文件有400000行,我有133个文件要处理。
目前,以下是我的应用程序的流程:
- 打开文件
- 读取一行
- 将其解析为新格式
- 将行写入输出文件
- 转到2,直到没有左行为止
- 转到1,直到没有剩余文件为止
每个输入文件大约有700MB,读取文件并将转换后的文件写入另一个文件需要很长时间。一个400KB的文件大约需要30秒才能完成该过程。
额外信息:
我的机器运行在英特尔i5处理器上,内存为8GB。
我并不是为了避免内存而实例化大量的对象。泄漏,并且我在打开输入文件时使用using
子句。
我能做些什么让它跑得更快?
我不知道你的代码是什么样子的,但这里有一个例子,在我的盒子上(诚然有SSD和i7,但…)在大约50ms内处理一个400K文件。
我甚至没有想过优化它——我已经用我能用的最干净的方式写了它。(请注意,这一切都是延迟评估的;File.ReadLines
和File.WriteAllLines
负责打开和关闭文件。)
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
class Test
{
public static void Main()
{
Stopwatch stopwatch = Stopwatch.StartNew();
var lines = from line in File.ReadLines("input.txt")
let cpf = ParseCpf(line)
let xml = ParseXml(line)
from year in ParseYears(line)
select cpf + year + xml;
File.WriteAllLines("output.txt", lines);
stopwatch.Stop();
Console.WriteLine("Completed in {0}ms", stopwatch.ElapsedMilliseconds);
}
// Returns the CPF, in the form "CPF=xxxxxx;"
static string ParseCpf(string line)
{
int start = line.IndexOf("CPF=");
int end = line.IndexOf(";", start);
// TODO: Validation
return line.Substring(start, end + 1 - start);
}
// Returns a sequence of year values, in the form "YEAR=2010;"
static IEnumerable<string> ParseYears(string line)
{
// First year.
int start = line.IndexOf("YEARS=") + 6;
int end = line.IndexOf(" ", start);
// TODO: Validation
string years = line.Substring(start, end - start);
foreach (string year in years.Split(';'))
{
yield return "YEARS=" + year + ";";
}
}
// Returns all the XML from the leading space onwards
static string ParseXml(string line)
{
int start = line.IndexOf(" <?xml");
// TODO: Validation
return line.Substring(start);
}
}
这看起来是一个很好的流水线候选。
基本思想是有3个并发任务,管道中的每个"阶段"一个,通过队列(BlockingCollection)相互通信:
- 第一个任务逐行读取输入文件,并将读取的行放入队列中
- 第二个任务从队列中获取行,对其进行格式化,并将结果放入另一个队列中
- 第三个任务从第二个队列中获取格式化的结果,并将它们写入结果文件
理想情况下,任务1应该而不是等待任务2完成后再转到下一个文件。
你甚至可以疯狂地将每个文件的管道放入一个单独的并行任务中,但这会严重破坏硬盘驱动器的头部,可能会弊大于利。另一方面,对于SSD来说,这实际上可能是合理的——在任何情况下,都是在做出决定之前采取的措施。
---编辑---
使用John Skeet的单线程实现作为基础,以下是管道版本的样子(工作示例):
class Test {
struct Queue2Element {
public string CPF;
public List<string> Years;
public string XML;
}
public static void Main() {
Stopwatch stopwatch = Stopwatch.StartNew();
var queue1 = new BlockingCollection<string>();
var task1 = new Task(
() => {
foreach (var line in File.ReadLines("input.txt"))
queue1.Add(line);
queue1.CompleteAdding();
}
);
var queue2 = new BlockingCollection<Queue2Element>();
var task2 = new Task(
() => {
foreach (var line in queue1.GetConsumingEnumerable())
queue2.Add(
new Queue2Element {
CPF = ParseCpf(line),
XML = ParseXml(line),
Years = ParseYears(line).ToList()
}
);
queue2.CompleteAdding();
}
);
var task3 = new Task(
() => {
var lines =
from element in queue2.GetConsumingEnumerable()
from year in element.Years
select element.CPF + year + element.XML;
File.WriteAllLines("output.txt", lines);
}
);
task1.Start();
task2.Start();
task3.Start();
Task.WaitAll(task1, task2, task3);
stopwatch.Stop();
Console.WriteLine("Completed in {0}ms", stopwatch.ElapsedMilliseconds);
}
// Returns the CPF, in the form "CPF=xxxxxx;"
static string ParseCpf(string line) {
int start = line.IndexOf("CPF=");
int end = line.IndexOf(";", start);
// TODO: Validation
return line.Substring(start, end + 1 - start);
}
// Returns a sequence of year values, in the form "YEAR=2010;"
static IEnumerable<string> ParseYears(string line) {
// First year.
int start = line.IndexOf("YEARS=") + 6;
int end = line.IndexOf(" ", start);
// TODO: Validation
string years = line.Substring(start, end - start);
foreach (string year in years.Split(';')) {
yield return "YEARS=" + year + ";";
}
}
// Returns all the XML from the leading space onwards
static string ParseXml(string line) {
int start = line.IndexOf(" <?xml");
// TODO: Validation
return line.Substring(start);
}
}
事实证明,上面的并行版本只比串行版本稍微快一点。显然,这个任务比其他任何任务都更受I/O约束,所以流水线并没有多大帮助。如果你增加处理量(例如添加一个强大的验证),这可能会改变有利于并行性的情况,但目前你最好只专注于串行改进(正如John Skeet自己所指出的,代码并没有那么快)。
(此外,我用缓存文件进行了测试-我想知道是否有办法清除Windows文件缓存,看看深度为2的硬件I/O队列是否能让硬盘与串行版本的I/O深度1相比优化磁头移动。)
这绝对不是IO问题-检查您的处理,使用profiler来了解所有时间片的持有者和持有者。
显示您的处理代码,可能是您使用了一些低效的字符串操作。。。
你可以马上做一些基本的事情。。。
- 运行多个线程,以便同时处理多个文件
- 使用StringBuilder或StringBuffer而不是字符串concat
- 如果使用XmlDocument解析XML,请将其替换为XmlTextReader和XmlTextWriter
- 如果不需要,不要将字符串转换为数字,然后再转换为字符串
- 删除所有不必要的字符串操作。例如,不要做str.Contains只是为了在下一行做str.IndexOf。相反,调用str.IndexOf将结果存储在本地var中,并检查是否大于0
自行运行算法的不同部分并测量时间。从逐行读取整个文件开始,然后进行测量。将相同的行写回一个新文件并进行测量。从xml中分离前缀信息并进行度量。分析xml。。。。这样你就会知道瓶颈是什么,并专注于这一部分。