何时使用.NET BufferedStream类?

62

MSDN网站中指出:

缓冲区是内存中的一块字节块,用于缓存数据,从而减少对操作系统的调用次数。缓冲区可以提高读写性能。缓冲区可用于读取或写入,但不能同时进行。BufferedStream类的Read和Write方法会自动维护缓冲区。

我应该在任何可能的情况下都使用这个类吗?

6个回答

73

根据 Brad Abrams 的说法,几乎不需要: 链接

不需要在 FileStream 外面包装 BufferedStream,没有任何好处。事实上,我们大约4年前将 BufferedStream 的缓存逻辑复制到了 FileStream 中,以鼓励更好的默认性能... 实际上,我认为.NET Framework 中没有任何 Stream 需要它,但如果自定义的 Stream 实现不会默认进行缓存,则可能需要它。


20
FileStream 只是 .net 流之一,例如将单个字节写入 NetworkStream,由于它未覆盖 Stream 的通用 WriteByte() 方法,因此效率非常低。 BufferedStream 可以极大地提高此类用例的性能(请参见@idn的答案)。 - Evgeniy Berezovsky
请注意,即使使用FileStream,如果在FileStream之后存在操作(例如FileStream-> 解压缩->BufferedStream),下游缓冲仍然会带来好处,其中解压缩步骤不会在内部缓冲。 - playsted
具有讽刺意味的是,如果“BufferedStream”的“缓冲逻辑”“被复制到.NET Framework中的[大多数]流…”是真的,那么这样做实际上通过将其硬编码到每个流中固定了该逻辑,并且BufferedStream今天剩余的用途实际上是替换而不是包装那些具体的Stream实现,以便调整、微调或修复该“逻辑”中的任何缺陷。尤其是,具体流的内部**Flush**行为无法更改,在certain scenarios中可能不理想。 - Glenn Slayden
我认为最新的信息是,在 .net core 3.1 - 6 中,添加 BufferedSteam 在某些情况下确实对性能产生了积极影响。这似乎是一个被引入的 bug,但是当使用 GZipStream 时,它看起来是有效的。来源:https://github.com/dotnet/runtime/issues/39233#issuecomment-745572680 - leon.io

22

我知道的最佳情况是使用BinaryFormatter直接从NetworkStream进行序列化/反序列化。在其中使用BufferedStream可以将性能提升十倍。


你说得对,我使用它来缓冲TcpClient的内置流,以便直接将二进制序列化缓冲到流中,这导致发送了大量的4字节数据包。 - ricebus

21
以下是我正在学习的在线课程中的一些内容:

BufferedStream类是一个具体的类,它扩展了Stream类,并用于为另一种类型的流提供附加的内存缓冲区,可以同步和异步使用。创建类的实例时,必须将BufferedStream类配置为读取或写入其中之一,但不能将其配置为同时执行两个任务。

Microsoft通过包含内置缓冲区来改善了.NET Framework中所有流的性能。通过将BufferedStream应用于现有流,例如FileStream或MemoryStream,性能显着提高。将BufferedStream应用于现有的.NET Framework流会导致双重缓冲区。

BufferedStream类最常见的应用程序是在不包括内置缓冲区的自定义流类中。


58
我不理解为什么要给MemoryStream应用缓冲区,因为它本质上已经是一个大的byte[]内存缓冲区。句子“The performance noticeably improved by applying a BufferedStream to existing streams, such as a FileStream or MemoryStream”似乎不完整,也许缺少否定词“不是”。我想知道这是否是一个误导性的回答... - Drew Noakes
1
如果您只是阅读此答案而没有看到其他答案,您可能会像我一样感到困惑。我认为黄色突出显示的文本是矛盾的,可能已经过时了。我认为应该忽略它,其他答案解释了FileStream和MemoryStream不需要BufferedStream。 - Cameron
一个在MemoryStream上的缓冲流是否可能会将部分数据缓存到更快的缓存类别(如L3缓存)中? - findusl
很遗憾,我不得不在SO上挖掘这些信息,而不是在MSDN页面上查找StreamReader的相关信息。 - arrowd
@findusl 不是一回事。Stream 概念的通用性质正是导致它无法控制其内容布局和访问方式的原因,而这正是需要(详细到极致)了解的唯一信息,才能开始产生任何可衡量的内存总线性能差异。更现实的问题是最坏情况下读/写 100% 故障/脏虚拟内存页面的“内存访问”,但即使在这种情况下,Stream 也无法超越操作系统的页面故障机制。 - Glenn Slayden
如上所述,.net团队成员提供了一些更新的信息,显示对于GZipStream应该考虑使用BufferedWriter。 来源:https://github.com/dotnet/runtime/issues/39233#issuecomment-745572680 - leon.io

6
晚回答,但我还是要回答。如果你理解它的工作原理,你就能理解为什么在某些情况下使用它没有意义,为什么在其他情况下不使用它有意义。
为了证明这一点,我使用StreamWrapper类。只使用这个类来查看你打了多少次断点!在这个类上放置断点。我们的目标是看到我们调用Write、Read和其他方法的次数。
// This class is only used for demo purposes. Place a breakpoint on all parts
class StreamWrapper : Stream
{
    Stream stream;

    public StreamWrapper(Stream s)
    {
        stream = s;
    }

    public override bool CanRead => stream.CanRead;

    public override bool CanSeek => stream.CanSeek;

    public override bool CanWrite => stream.CanWrite;

    public override long Length => stream.Length;

    public override long Position { get => stream.Position; set => stream.Position = value; }

    public override void Flush()
    {
        stream.Flush();
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        return stream.Read(buffer, offset, count);
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        return stream.Seek(offset,origin);
    }

    public override void SetLength(long value)
    {
        stream.SetLength(value);
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        stream.Write(buffer, offset, count);
    }


}

现在您有了一个基本上是现有流的包装器类,您可以进行以下测试:

示例写法:

// in real life you want this to be larger. 
int bufferSize = 8;


// Use BufferedStream to buffer writes to a MemoryStream.
using (var memory_test = new StreamWrapper(new MemoryStream()))
using (BufferedStream stream = new BufferedStream(memory_test, bufferSize))
{


    // all this will only send one write to memory_test!
    stream.Write(new byte[] { 1, 2 });
    stream.Write(new byte[] { 1, 2 });
    stream.Write(new byte[] { 1, 2 });
    stream.Write(new byte[] { 1, 2 });
    // BREAKPOINT ONLY HITS ONE TIME 


    // All this will also send only one write to memory_test
    for (int i = 0; i < 8; i++)                            
        stream.WriteByte(5);
    // BREAKPOINT ONLY HITS ONE TIME AGAIN INSTAD OF 8


    // this will send one write to memory_test. Writes of more than 8 bytes can happen!
    stream.Write(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 });
    // ALL THIS WILL SEND ONE WRITE AGAIN
}

示例阅读:

// example reading
{
    // create stream with some data in it that we will be reading
    var ms = new MemoryStream();
    {
        // Write 256 bytes
        for (int i = 0; i <= byte.MaxValue; i++)
        {
            ms.WriteByte((byte)i);
        }
        ms.Position = 0;
    }

    // Use BufferedStream to buffer writes to a MemoryStream.
    using (var memory_test = new StreamWrapper(ms))
    {                                                        
        using (BufferedStream stream = new BufferedStream(memory_test, 8))
        {
            // note I do not care about the output of each read for demo breakpoint. On real life you will store that output
            // for now we only care how many times we hit the breakpoint because on real life that could be a slow/expensive 
            // operation such as opening a file for writing and you want to do those as few as possible.

            // hit breakpoint only one time with all this reads
            stream.ReadByte();
            stream.ReadByte();
            stream.ReadByte();
            stream.ReadByte();
            stream.ReadByte();
            stream.ReadByte();
            stream.ReadByte();
            stream.ReadByte();


            // hit breakpoint only one time with all this reads
            stream.Read(new byte[2] );
            stream.Read(new byte[2] );
            stream.Read(new byte[2] );
            stream.Read(new byte[2] );


            // hit breakpoint only one time even though it is larger than our buffer size 8
            // our goal is to hit the breakpoint as fewest time as possible because in real life
            // this could be slow/expensive operations
            stream.Read(new byte[1024] );

        }
    }
}

如果这是对文件进行写操作的话,通过确保每次至少写入8个字节而不是1个字节,可以提高性能。在实际应用中,您希望这个数字约为4 KB。

5

普通的文件I/O流已经使用StreamReader/StreamWriter进行了缓冲。

由于流上的读写操作通常使用接受字节数组的Read/Write方法,您自然会提供一些缓冲。

如果您使用非常小的数组或使用WriteByte,则通过在中间使用BufferedStream可能会获得更好的性能。


3

在任何可能的场合都必须使用常识。当对内存流进行读写时,使用这个类是没有用的,但是在进行网络或磁盘IO时可能会非常有用(如果这些子系统的流没有自己进行缓冲)。


你是否有一份明确的清单,显示哪些流是带缓冲的,哪些不是?我遇到了这个问题,特别是对NetworkStream感兴趣。我想删除一些缓冲代码,因为我相当确定它们没有必要。我会继续浏览! - Drew Noakes
事实证明,在NetworkStream的情况下,您只能使用byte[]进行读写,因此缓冲由调用者控制。 NetworkStream也不可寻址,因此不需要内部缓冲区。 - Drew Noakes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接