【问题标题】:Performance of FileStream's Write vs WriteByte on an IEnumerable<byte>FileStream 的 Write 与 WriteByte 在 IEnumerable<byte> 上的性能
【发布时间】:2012-09-14 18:17:06
【问题描述】:

我需要将 IEnumerable&lt;byte&gt; 的字节写入文件。
我可以将其转换为数组并使用Write(byte[]) 方法:

using (var stream = File.Create(path))
    stream.Write(bytes.ToArray());

但由于IEnumerable 不提供集合的项目数,因此不建议使用ToArray unless it's absolutely necessary

所以我可以迭代 IEnumerable 并在每次迭代中使用 WriteByte(byte)

using (var stream = File.Create(path))
    foreach (var b in bytes)
        stream.WriteByte(b);

我想知道在写入大量数据时哪个会更快。

我猜想使用Write(byte[]) 会根据数组大小设置缓冲区,这样处理数组时会更快。

我的问题是,当我只有一个包含 MB 数据的 IEnumerable&lt;byte&gt; 时,哪种方法更好?将其转换为数组并调用Write(byte[]) 或迭代它并为每个数组调用WriteByte(byte)

【问题讨论】:

  • 为什么不自己测量呢?你有什么样的IEnumerable&lt;T&gt;?它还实现其他集合接口(如IList&lt;T&gt;)的可能性有多大?

标签: .net performance stream bytearray filestream


【解决方案1】:

枚举大量字节流是一个过程,会为通常很便宜的事情增加大量开销:将字节从一个缓冲区复制到下一个缓冲区。

通常,LINQ 样式的开销并不重要,但在普通硬盘驱动器上每秒处理 1 亿字节时,您注意到严重的开销。这不是过早的优化。我们可以预见这将是一个性能热点,因此我们应该积极优化。

因此,当在您周围复制字节时,可能根本不应该依赖 IEnumerableIList 这样的抽象。传递还包含OffsetCount 的数组或ArraySegement&lt;byte&gt;。这使您不必过于频繁地对数组进行切片。

对于高吞吐量 IO 来说,还有一个致命的问题是每个字节调用一个方法。就像按字节读取和按字节写入一样。这杀死性能,因为这些方法必须每秒调用数亿次。我亲身经历过。

始终一次处理至少 4096 字节的整个缓冲区。根据您使用的 IO 媒体,您可以使用更大的缓冲区(64k、256k 甚至兆字节)。

【讨论】:

  • FileStream 默认有一个 4096 字节的内部缓冲区。 WriteByte 写入该缓冲区并仅在已满时刷新它。当您在读取和写入之间切换、调用Flush 或关闭流时,也会发生刷新。另一方面,遍历枚举器无论如何都需要发生。
  • @Wormbo 您甚至不希望每个字节调用一个虚拟函数(并且内部写入可能不仅仅是设置该字节)。当以全顺序速度写入磁盘时,即使这样也非常昂贵。您要做的就是块复制,速度非常快。
  • 我想知道“大量数据”的真正含义。我们是在谈论数万甚至数百万字节吗?我做了一些测试,大约 19 到 2000 万字节,ToArray() 开销开始超过我机器上的WriteByte() 开销。不过,固定大小的单独缓冲区几乎总是优于这两种方法。
  • @Wormbo yes 在调用 write 之前调用 ToArray 是没有用的,因为 ToArray 枚举了序列。开销只是移到了不同​​的地方,但数量大致相同。一旦你有一个序列,一切都将丢失。必须一直使用字节数组。尝试测量写new byte[1024 * 1024 * 64] 需要多长时间。测量 CPU 时间。
【解决方案2】:

您应该分析哪个版本更快。 FileStream 类有一个内部缓冲区,可以将 Read()Write() 方法与实际的文件系统访问解耦。

如果您没有在 FileStream 构造函数中指定缓冲区大小,它默认使用类似 4096 字节的缓冲区。该缓冲区会将您的许多WriteByte() 调用组合成对基础文件的一次写入。唯一的问题是WriteByte() 调用的开销是否会超过Enumerable.ToArray() 调用的开销。后者肯定会使用更多内存,但您总是必须处理这种权衡。

仅供参考:Enumerable.ToArray() 的当前 .NET 4 实现涉及通过在必要时复制其大小来增长数组。每次增长时,都会复制所有值。此外,当所有项目都存储在数组中时,其内容将再次复制到最终大小的数组中。对于实际实现ICollection&lt;T&gt;IEnumerable&lt;T&gt; 实例,代码利用这一事实从正确的数组大小开始,并让集合改为复制。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-06-17
    • 2014-06-12
    • 2017-12-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-03
    相关资源
    最近更新 更多