写入文件C#的性能

时间:2012-02-24 20:03:41

标签: c# performance file io

我的情况概述:

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

以下是必须完成的示例。 文件行示例:

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倍。

效果问题:

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

目前,这是我的应用流程:

  1. 打开文件
  2. 阅读一行
  3. 将其解析为新格式
  4. 将该行写入输出文件
  5. 转到2直到没有左行
  6. 转到1,直到没有左文件
  7. 每个输入文件大约为700MB,读取文件并将其转换后的版本写入另一个文件需要永远。一个400KB的文件需要大约30秒才能完成这个过程。

    额外信息:

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

    我没有实例化大量的对象来避免mem。泄漏,我在输入文件打开时使用using子句。

    我该怎么做才能让它跑得更快?

4 个答案:

答案 0 :(得分:11)

我不知道你的代码是什么样的,但这里有一个例子,在我的盒子上(当然是带有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);
    }
}

答案 1 :(得分:5)

这似乎是pipelining的优秀候选人。

基本思想是拥有3个并发Task,一个用于管道中的每个“阶段”,通过队列(BlockingCollection)相互通信:

  1. 第一个任务逐行读取输入文件并将读取行放入队列。
  2. 第二个任务从队列中获取行,格式化它们并将结果放入另一个队列。
  3. 第三个任务从第二个队列获取格式化结果,并将它们写入生成的文件。
  4. 理想情况下,任务1 等待任务2完成后再转到下一个文件。

    你甚至可以疯狂地将每个单独文件的管道放入一个单独的并行任务中,但是这会让你的硬盘头很糟糕,它可能会让你受伤更多。另一方面,对于SSD来说,实际上这可能是合理的 - 无论如何在做出决定之前进行测量。

    ---编辑---

    使用John Skeet's single-threaded implementation作为基础,以下是流水线版本的样子(工作示例):

    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的串行版本。)

答案 2 :(得分:2)

绝对不是IO问题 - 检查您的处理,使用分析器知道所有时间片的持有者和位置。

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

答案 3 :(得分:1)

你可以马上做几件基本事情......

  1. 运行多个线程,以便同时处理多个文件。
  2. 使用StringBuilder或StringBuffer代替string concat
  3. 如果使用XmlDocument解析XML,请将其替换为XmlTextReader和XmlTextWriter
  4. 如果您不需要,请不要将字符串转换为数字并返回字符串
  5. 删除所有不必要的字符串操作。例如,不要在下一行做str.Contains只做str.IndexOf。而是调用str.IndexOf将结果存储在本地var中并检查是否&gt; 0
  6. 自行运行算法的不同部分并测量时间。首先逐行读取整个文件并测量。将相同的行写回新文件并进行测量。从xml中拆分前缀信息并测量它。解析xml .... 通过这种方式,您将了解瓶颈是什么,并专注于该部分。