【问题标题】:Why is writing to a file faster than appending a string?为什么写入文件比附加字符串快?
【发布时间】:2017-04-14 23:53:15
【问题描述】:

鉴于 RAM 比硬盘快得多,我对下面的代码感到惊讶。

我试图根据一列的值拆分 CSV 文件,并将该单元格中具有不同值的每一行写入不同的文件。

我在尝试:

List<string> protocolTypes = new List<string>();
List<string> splitByProtocol = new List<string>();
foreach (string s in lineSplit)
{
    string protocol = getProtocol();
    index = protocolTypes.IndexOf(protocol);
    splitByProtocol[index] = splitByProtocol[index] + s + "\n";
}

这需要很长时间,但将其更改为流写入器要快得多:

List<string> protocolTypes = new List<string>();
List<StreamWriter> splitByProtocol = new List<StreamWriter>();
foreach (string s in lineSplit)
{
    string protocol = getProtocol();
    index = protocolTypes.IndexOf(protocol);
    splitByProtocol[index].WriteLine(s);
}

为什么写入磁盘比在内存中附加字符串快得多?我知道添加到一个字符串需要将整个字符串复制到一个新的内存位置,但是添加一个字符串比写入磁盘要慢几个数量级,这似乎违反直觉。

【问题讨论】:

  • 这似乎不是真正的代码,因为您没有将项目添加到列表中。
  • 每个+ 操作都会创建一个新字符串(这需要分配内存)。请改用StringBuilder
  • @MikeS159:除了可能的IndexOutOfBoundException,这个简单的字符串连接不会导致任何明显的性能问题。你是怎么测量的?我猜大部分工作都是在getProtocol
  • @TimSchmelter 如果您认为简单的字符串连接不会导致性能问题,那么您在字符串操作方面似乎还没有做太多工作。这是一个时间和空间上的 O(n^2) 操作。

标签: c# string streamwriter


【解决方案1】:

首先它为新字符串分配(大量)内存。然后它逐字节复制现有字符串和附加的部分。这需要相当多的循环,并且对于每个循环,字符串都会变长,因此总体操作时间与循环数成指数关系。

Gen1 的垃圾回收也意味着将最新的字符串复制到 Gen2(因此再次复制)。这将填满一堆这些旧字符串等,所以我们进入第 2 代。这种方法在 GC 上产生了相当多的开销。

对于磁盘,它只写入流,所以它首先在内存中(快)然后是磁盘缓存(快),直到它最终被写入磁盘(慢,但那部分是缓冲的,所以它看起来非常快)。 此外,它只执行一次,因此性能与循环数几乎呈线性关系。

顺便说一句,您可能想查看 StringBuilder,这可能会更快。

【讨论】:

    【解决方案2】:

    如果字符串变得很大(很多 MB),那么复制它们肯定会变得很耗时。

    然而,最大的打击可能是由许多不再需要的旧字符串造成的,它们像垃圾一样坐在堆上,等待收集。所以垃圾收集器会启动,甚至可能多次,每次都暂停你的程序。

    对于在这样的循环中构造的字符串,请始终考虑改用StringBuilder。要匹配您的示例代码:

    List<StringBuilder> splitByProtocol = new List<StringBuilder>();
    foreach (string s in lineSplit)
    {
        string protocol = getProtocol();
        index = protocolTypes.IndexOf(protocol);
        splitByProtocol[index].AppendLine(s);
    }
    

    【讨论】:

      【解决方案3】:

      首先确保您的测量结果正常。

      如果仍然如此,StreamWriter 使用缓冲区写入,您附加一个字符串,该字符串将每次再次创建一个字符串,最终将有过多的内存分配,而流写入器仍在缓存中。请注意,您没有刷新,这意味着文件在刷新之前不会写入(这不是由您的代码强制执行的),因此可能意味着您只是存储到比字符串附加更有效的内存存储中。即使它被冲洗,它也会立即完成。使用快速磁盘,您最终会比过于昂贵的字符串连接更快。

      如果您将StringBuilder 用于您的第一个代码,您会发现您的执行时间将显着减少。然后你会看到真正的性能差异,我相信你会看到StringBuilder更快。

      【讨论】:

      • 缓冲区填满时会自动刷新,因此他不会从不写入文件。
      • 同意,澄清@Servy
      • 我忘记了StringBuilder,我以后会用它来试试。
      猜你喜欢
      • 1970-01-01
      • 2014-12-11
      • 2011-04-03
      • 1970-01-01
      • 2011-06-21
      • 2012-02-09
      • 1970-01-01
      • 2017-12-10
      • 2016-12-26
      相关资源
      最近更新 更多