为什么写入文件比附加字符串更快?
Why is writing to a file faster than appending a string?
鉴于 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);
}
为什么写入磁盘比在内存中将字符串附加在一起快得多?我知道添加到一个字符串需要将整个字符串复制到一个新的内存位置,但是添加一个字符串比写入磁盘慢几个数量级,这似乎违反直觉。
首先它为新字符串分配(大量)内存。然后它复制现有的字符串,以及附加的部分,一个字节一个字节。这需要相当多的循环,并且对于每个循环,字符串都会变长,因此整体操作时间是循环次数的指数。
Gen1 的垃圾收集也意味着将最新的字符串复制到 Gen2(因此再次复制)。这将填满一堆这些旧字符串等,所以我们进入 Gen2。这种方法在 GC 上产生了相当多的开销。
对于磁盘来说,它只是写入流,所以它首先在内存中(快)然后是磁盘缓存(快)直到它最终写入磁盘(慢,但是那部分被缓冲所以它看起来非常快)。
而且它只完成一次,因此性能与循环次数几乎呈线性关系。
顺便说一句,您可能想查看 StringBuilder,这可能会更快。
首先确保你的三围没问题。
如果仍然如此,StreamWriter
使用缓冲区写入,你追加一个字符串,每次都会重新创建一个字符串,最终会分配过多的内存,而流写入器仍在缓存。请注意,您没有刷新,这意味着文件在刷新之前不会被写入(这不是您的代码强制执行的),因此可能意味着您只是存储到比字符串附加更有效的内存存储中。即使它被冲洗了,它也会立即完成。使用快速磁盘,您最终会比过于昂贵的字符串连接更快。
如果您对第一个代码使用 StringBuilder
,您会发现执行时间会显着缩短。然后你会看到性能上的真正差异,我相信你会看到 StringBuilder
更快。
如果字符串变得很大(很多 MB),那么复制它们肯定会很耗时。
然而,最大的打击可能是由许多不再需要的旧字符串造成的,它们作为垃圾堆放在堆上,等待被收集。所以垃圾收集器会启动,甚至可能启动很多次,每次都会暂停你的程序。
对于像这样在循环中构造的字符串,请始终考虑使用 StringBuilder
。要匹配您的示例代码:
List<StringBuilder> splitByProtocol = new List<StringBuilder>();
foreach (string s in lineSplit)
{
string protocol = getProtocol();
index = protocolTypes.IndexOf(protocol);
splitByProtocol[index].AppendLine(s);
}
鉴于 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);
}
为什么写入磁盘比在内存中将字符串附加在一起快得多?我知道添加到一个字符串需要将整个字符串复制到一个新的内存位置,但是添加一个字符串比写入磁盘慢几个数量级,这似乎违反直觉。
首先它为新字符串分配(大量)内存。然后它复制现有的字符串,以及附加的部分,一个字节一个字节。这需要相当多的循环,并且对于每个循环,字符串都会变长,因此整体操作时间是循环次数的指数。
Gen1 的垃圾收集也意味着将最新的字符串复制到 Gen2(因此再次复制)。这将填满一堆这些旧字符串等,所以我们进入 Gen2。这种方法在 GC 上产生了相当多的开销。
对于磁盘来说,它只是写入流,所以它首先在内存中(快)然后是磁盘缓存(快)直到它最终写入磁盘(慢,但是那部分被缓冲所以它看起来非常快)。 而且它只完成一次,因此性能与循环次数几乎呈线性关系。
顺便说一句,您可能想查看 StringBuilder,这可能会更快。
首先确保你的三围没问题。
如果仍然如此,StreamWriter
使用缓冲区写入,你追加一个字符串,每次都会重新创建一个字符串,最终会分配过多的内存,而流写入器仍在缓存。请注意,您没有刷新,这意味着文件在刷新之前不会被写入(这不是您的代码强制执行的),因此可能意味着您只是存储到比字符串附加更有效的内存存储中。即使它被冲洗了,它也会立即完成。使用快速磁盘,您最终会比过于昂贵的字符串连接更快。
如果您对第一个代码使用 StringBuilder
,您会发现执行时间会显着缩短。然后你会看到性能上的真正差异,我相信你会看到 StringBuilder
更快。
如果字符串变得很大(很多 MB),那么复制它们肯定会很耗时。
然而,最大的打击可能是由许多不再需要的旧字符串造成的,它们作为垃圾堆放在堆上,等待被收集。所以垃圾收集器会启动,甚至可能启动很多次,每次都会暂停你的程序。
对于像这样在循环中构造的字符串,请始终考虑使用 StringBuilder
。要匹配您的示例代码:
List<StringBuilder> splitByProtocol = new List<StringBuilder>();
foreach (string s in lineSplit)
{
string protocol = getProtocol();
index = protocolTypes.IndexOf(protocol);
splitByProtocol[index].AppendLine(s);
}