【问题标题】:Performance of Writing to File C#写入文件 C# 的性能
【发布时间】:2012-03-15 06:54:19
【问题描述】:

我的情况概述:

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

这里是一个必须做的例子。 文件行示例:

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. Goto1 直到没有剩余文件

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

额外信息:

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

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

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

【问题讨论】:

  • 如果您跳过“3. 将其解析为新格式”并按原样将行写入新文件会发生什么?如果性能有所提高,您就发现了问题。如果没有,请发布读取和写入数据的代码。
  • 你写入文件的缓冲区大小是多少?
  • 请展示一个在 30 秒内处理文件的代码,分享一个示例文件也是有意义的,这样我们就可以使用相同的数据集进行测试,但我相信如果你愿意分享你正在使用的代码足以看到薄弱的地方

标签: c# performance file io


【解决方案1】:

我不知道您的代码是什么样的,但这是我的机器上的一个示例(当然是使用 SSD 和 i7,但是...)在大约 50 毫秒内处理一个 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);
    }
}

【讨论】:

  • 这正是我所做的。我学习了如何使用 IEnumerable 在读取文件和输出文件之间传输信息。这真的解决了我的生活。处理速度非常快。
【解决方案2】:

这看起来很适合 pipelining

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

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

理想情况下,任务 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 队列是否允许硬盘优化磁头移动,而我/O 串行版本的深度 1。)

【讨论】:

  • 有趣的想法,看看工作示例会很有趣,它是否会产生性能差异
  • 太好了,谢谢! +1。也感谢管道模式 MSDN 参考
  • 嘿,刚刚发现你来自诺维萨德,不知道你参加过苏恰瓦的硬&&软比赛吗? :) 我记得诺维萨德的好团队(Microchip PIC 控制器 + Desctop 软件)
  • @sll 唉,不。自从我还是学生以来已经很久了;)
  • :) 我提到的竞争是在 2003-2005 年,反正:)
【解决方案3】:

这绝对不是 IO 问题 - 检查您的处理,使用分析器知道谁和在哪里拥有所有时间片。

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

【讨论】:

    【解决方案4】:

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

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

    自行运行算法的不同部分并测量时间。首先逐行读取整个文件并进行测量。将相同的行写回一个新文件并测量它。从 xml 中拆分前缀信息并对其进行测量。解析xml.... 这样你就会知道瓶颈是什么,并专注于那部分。

    【讨论】:

    • 我认为您在这里并不需要非常聪明 - 请参阅我的答案以获取一个运行速度非常快且无需过多担心性能的示例 - 只需专注于清晰度即可。
    • 这个答案的第一部分(1-5 个要点)侧重于错误的领域。 Jon Skeet 立即知道,如果编程得当,如此简单的处理不会花费这么长时间。但并不是每个人都有 Jon Skeet 的技能。所以这个答案的第二部分,虽然是一条通用的建议,但对于一个不知道他们在哪里杀死处理速度的程序员来说实际上是有好处的。衡量绩效的“做你的功课”的更详细版本,一步一步做。
    猜你喜欢
    • 1970-01-01
    • 2014-05-10
    • 1970-01-01
    • 1970-01-01
    • 2012-10-29
    • 1970-01-01
    • 1970-01-01
    • 2012-11-30
    相关资源
    最近更新 更多