Skip to content

Unexpected and undocumented (performance) pitfall in BufferedStream.WriteByte #104559

Description

@ANahr

Description

When calling WriteByte on a BufferedStream an implicit Flush will be called on the wrapped stream if the internal buffer is full. This is different from all other Write methods on BufferedStream and is not even mentioned in the documentation.
This can be problematic if the wrapped stream or streams have properties that are suspencible to Flushes (see repo steps for an example).
The practical situation is even worse because for lots of writer abstractions (e.g. BinaryWriter) it is mostly an implementation specific if Write or WriteByte is used. So in effect it is mostly "random" if Flushes happen when writing to an underlying BufferedStream.

Reproduction Steps

Please consider the following code:

var latencyStream = new SomeStreamWhereCallingWithSmallBuffersOrFlushesIsExtremelyExpensive();
var specialLatencyBufferStream  = new BufferedStream(latencyStream, 1024 * 1024 * 100);
var compressionStream = new DeflateStream(specialLatencyBufferStream, CompressionMode.Compress, true);
var bufferedStream = new BufferedStream(compressionStream, 1024 * 8);

If you are now serializing with a BinaryWriter on bufferedStream it will effectively bypass the specialLatencyBufferStream "randomly" (depending on whether WriteByte or any of the other Write methods are used when surpassing the 8kb bufferedStream)

Expected behavior

Write and WriteByte behave the same and don't Flush

Actual behavior

Write methods don't Flush, WriteByte does

Regression?

no

Known Workarounds

Not calling WriteByte

Risks of change

Applications might depend on the behavior. However given this is not documented and mostly "random" in effect this is very unlikely.

Other information

The reason for the behavior is in BufferedStream.cs:

        private void WriteByteSlow(byte value)
        {
            EnsureNotClosed();

            if (_writePos == 0)
            {
                EnsureCanWrite();
                ClearReadBufferBeforeWrite();
                EnsureBufferAllocated();
            }

            // We should not be flushing here, but only writing to the underlying stream, but previous version flushed, so we keep this.
            if (_writePos >= _bufferSize - 1)
                FlushWrite();

            _buffer![_writePos++] = value;

            Debug.Assert(_writePos < _bufferSize);
        }

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-System.IOin-prThere is an active PR which will close this issue when it is mergedtenet-performancePerformance related issue

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions