写入文件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个文件要处理。

目前,以下是我的应用程序的流程:

  1. 打开文件
  2. 读取一行
  3. 将其解析为新格式
  4. 将行写入输出文件
  5. 转到2,直到没有左行为止
  6. 转到1,直到没有剩余文件为止

每个输入文件大约有700MB,读取文件并将转换后的文件写入另一个文件需要很长时间。一个400KB的文件大约需要30秒才能完成该过程。

额外信息:

我的机器运行在英特尔i5处理器上,内存为8GB。

我并不是为了避免内存而实例化大量的对象。泄漏,并且我在打开输入文件时使用using子句。

我能做些什么让它跑得更快?

写入文件C#的性能

我不知道你的代码是什么样子的,但这里有一个例子,在我的盒子上(诚然有SSD和i7,但…)在大约50ms内处理一个400K文件。

我甚至没有想过优化它——我已经用我能用的最干净的方式写了它。(请注意,这一切都是延迟评估的;File.ReadLinesFile.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. 第二个任务从队列中获取行,对其进行格式化,并将结果放入另一个队列中
  3. 第三个任务从第二个队列中获取格式化的结果,并将它们写入结果文件

理想情况下,任务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来了解所有时间片的持有者和持有者。

显示您的处理代码,可能是您使用了一些低效的字符串操作。。。

你可以马上做一些基本的事情。。。

  1. 运行多个线程,以便同时处理多个文件
  2. 使用StringBuilder或StringBuffer而不是字符串concat
  3. 如果使用XmlDocument解析XML,请将其替换为XmlTextReader和XmlTextWriter
  4. 如果不需要,不要将字符串转换为数字,然后再转换为字符串
  5. 删除所有不必要的字符串操作。例如,不要做str.Contains只是为了在下一行做str.IndexOf。相反,调用str.IndexOf将结果存储在本地var中,并检查是否大于0

自行运行算法的不同部分并测量时间。从逐行读取整个文件开始,然后进行测量。将相同的行写回一个新文件并进行测量。从xml中分离前缀信息并进行度量。分析xml。。。。这样你就会知道瓶颈是什么,并专注于这一部分。