C#写入文件的性能表现

5

我的情况概述:

我的任务是从文件中读取字符串,并将它们重新格式化为更有用的格式。在重新格式化输入后,我必须将其写入输出文件。

以下是需要完成的示例。

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文件则代表对数据库中该文档查询的返回结果。

我的目标:

在这种旧格式下,每个文档都有一个包含所有年份(2006至2010)的查询结果的XML。重新格式化后,每个输入行转换为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倍

性能问题:

每个文件都有400,000行,我有133个文件要处理。

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

  1. 打开一个文件
  2. 读取一行
  3. 将其解析为新格式
  4. 将该行写入输出文件
  5. 重复2-4直到没有剩余行
  6. 重复1-5直到没有剩余文件

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

额外信息:

我的计算机配备Intel i5处理器和8GB RAM。

我避免实例化大量对象以避免内存泄漏,并在打开输入文件时使用“using”子句。

我该怎么做才能使它运行更快?


2
如果您跳过“3.将其解析为新格式”并将该行直接写入新文件,会发生什么?如果性能有所提高,则找到了问题。如果没有,请发布读取和写入数据的代码。 - Andrei
你要写入文件的缓冲区大小是多少? - Matthew
1
请展示一个能够在30秒内处理文件的代码,并且最好分享一个样本文件,这样我们就可以使用相同的数据集进行测试。但我相信如果您分享您正在使用的代码,就足以发现其中的问题了。 - sll
4个回答

12

我不知道你的代码是什么样子的,但这是一个例子,在我的电脑上(虽然有SSD和i7),处理一个大小为400K的文件只需约50ms。

我甚至还没有考虑过优化它 - 我用最干净的方式编写了它。(请注意,它全部是惰性求值;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);
    }
}

1
这正是我所做的。我学会了如何使用IEnumerable在读取文件和输出文件之间流传信息。这真正解决了我的问题。处理速度非常快。 - Marcello Grechi Lins

5

这似乎是一个很好的使用流水线技术的方案。

基本思路是有3个并发的任务(Task),分别代表管道中的每个"阶段",并通过队列(BlockingCollection)相互通信:

  1. 第一个任务逐行读取输入文件,并将读取出来的行放入队列中。
  2. 第二个任务从队列中获取行,对其进行格式化,然后将结果放入另一个队列中。
  3. 第三个任务从第二个队列中获取格式化的结果,并将其写入到最终的输出文件中。

理想情况下,任务1不应该在任务2完成之前等待。

您甚至可以疯狂地将每个单独文件的管道放入单独的并行任务中,但这可能会严重损坏硬盘驱动器的读写头,反而不起作用。但在使用SSD时,这种做法可能是有道理的 - 在做出决策之前请先进行测量。

--- 编辑 ---

John Skeet's的单线程实现基础上,下面是流水线版本的代码(可以工作):

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相比)。

有趣的想法,看到工作示例会很有趣,可以看出它是否会产生性能差异。 - sll
非常好,谢谢!+1。同时感谢管道模式MSDN参考。 - sll
嘿,刚发现你来自诺维萨德,不知道你是否参加过苏恰瓦的Hard&&Soft比赛呢? :) 我还记得来自诺维萨德的一支优秀团队(使用Microchip PIC控制器和桌面软件)。 - sll
@sll 哎呀,不是的。我已经很久没当学生了 ;) - Branko Dimitrijevic
无论如何,我提到的竞赛都是在2003年至2005年间。 :) - sll

2

这绝对不是IO问题 - 检查你的处理过程,使用分析器了解谁在哪里占用所有时间片。

展示你的处理代码,可能你使用了一些低效的字符串操作...


1

有一些基本的事情你可以立即做...

  1. 运行多个线程,以便同时处理多个文件。
  2. 使用StringBuilder或StringBuffer而不是字符串连接
  3. 如果您使用XmlDocument解析XML,请将其替换为XmlTextReader和XmlTextWriter
  4. 如果不需要真正需要它,则不要将字符串转换为数字再转回字符串
  5. 删除每个不必要的字符串操作。例如,不要执行str.Contains只是为了在下一行执行str.IndexOf。相反,调用str.IndexOf存储结果在本地变量中并检查是否> 0。

单独运行算法的不同部分并测量时间。从逐行读取整个文件开始并测量时间。将相同的行写回新文件并测量时间。从xml中拆分前缀信息并测量时间。解析xml.... 这样,您就会知道瓶颈在哪里,并专注于该部分。


我认为你在这里并不需要非常聪明 - 参见我的答案,其中有一个例子可以快速运行,而不必过于担心性能 - 只需专注于清晰度即可。 - Jon Skeet
这个答案的第一部分(1-5个要点)关注的是错误的领域。Jon Skeet 立即知道,如果正确编程,这样简单的处理过程不可能需要那么长时间。但并非每个人都拥有 Jon Skeet 的技能。因此,这个答案的第二部分,作为一条通用建议,实际上对于一个毫无头绪的程序员来说是很好的,它是“做你的功课”的更详细版本,即逐步测量性能。 - Stéphane Gourichon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接