public override void Write(byte[] array, int offset, int count)
{
if (array == null)
throw new ArgumentNullException(nameof(array), SR.ArgumentNull_Buffer);
if (offset < 0)
throw new ArgumentOutOfRangeException(nameof(offset), SR.ArgumentOutOfRange_NeedNonNegNum);
if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_NeedNonNegNum);
if (array.Length - offset < count)
throw new ArgumentException(SR.Argument_InvalidOffLen);
EnsureNotClosed();
EnsureCanWrite();
if (_writePos == 0)
ClearReadBufferBeforeWrite();
#region Write algorithm comment
// We need to use the buffer, while avoiding unnecessary buffer usage / memory copies.
// We ASSUME that memory copies are much cheaper than writes to the underlying stream, so if an extra copy is
// guaranteed to reduce the number of writes, we prefer it.
// We pick a simple strategy that makes degenerate cases rare if our assumptions are right.
//
// For ever write, we use a simple heuristic (below) to decide whether to use the buffer.
// The heuristic has the desirable property (*) that if the specified user data can fit into the currently available
// buffer space without filling it up completely, the heuristic will always tell us to use the buffer. It will also
// tell us to use the buffer in cases where the current write would fill the buffer, but the remaining data is small
// enough such that subsequent operations can use the buffer again.
//
// Algorithm:
// Determine whether or not to buffer according to the heuristic (below).
// If we decided to use the buffer:
// Copy as much user data as we can into the buffer.
// If we consumed all data: We are finished.
// Otherwise, write the buffer out.
// Copy the rest of user data into the now cleared buffer (no need to write out the buffer again as the heuristic
// will prevent it from being filled twice).
// If we decided not to use the buffer:
// Can the data already in the buffer and current user data be combines to a single write
// by allocating a "shadow" buffer of up to twice the size of _bufferSize (up to a limit to avoid LOH)?
// Yes, it can:
// Allocate a larger "shadow" buffer and ensure the buffered data is moved there.
// Copy user data to the shadow buffer.
// Write shadow buffer to the underlying stream in a single operation.
// No, it cannot (amount of data is still too large):
// Write out any data possibly in the buffer.
// Write out user data directly.
//
// Heuristic:
// If the subsequent write operation that follows the current write operation will result in a write to the
// underlying stream in case that we use the buffer in the current write, while it would not have if we avoided
// using the buffer in the current write (by writing current user data to the underlying stream directly), then we
// prefer to avoid using the buffer since the corresponding memory copy is wasted (it will not reduce the number
// of writes to the underlying stream, which is what we are optimising for).
// ASSUME that the next write will be for the same amount of bytes as the current write (most common case) and
// determine if it will cause a write to the underlying stream. If the next write is actually larger, our heuristic
// still yields the right behaviour, if the next write is actually smaller, we may making an unnecessary write to
// the underlying stream. However, this can only occur if the current write is larger than half the buffer size and
// we will recover after one iteration.
// We have:
// useBuffer = (_writePos + count + count < _bufferSize + _bufferSize)
//
// Example with _bufferSize = 20, _writePos = 6, count = 10:
//
// +---------------------------------------+---------------------------------------+
// | current buffer | next iteration's "future" buffer |
// +---------------------------------------+---------------------------------------+
// |0| | | | | | | | | |1| | | | | | | | | |2| | | | | | | | | |3| | | | | | | | | |
// |0|1|2|3|4|5|6|7|8|9|0|1|2|3|4|5|6|7|8|9|0|1|2|3|4|5|6|7|8|9|0|1|2|3|4|5|6|7|8|9|
// +-----------+-------------------+-------------------+---------------------------+
// | _writePos | current count | assumed next count|avail buff after next write|
// +-----------+-------------------+-------------------+---------------------------+
//
// A nice property (*) of this heuristic is that it will always succeed if the user data completely fits into the
// available buffer, i.e. if count < (_bufferSize - _writePos).
#endregion Write algorithm comment
Debug.Assert(_writePos < _bufferSize);
int totalUserbytes;
bool useBuffer;
checked
{ // We do not expect buffer sizes big enough for an overflow, but if it happens, lets fail early:
totalUserbytes = _writePos + count;
useBuffer = (totalUserbytes + count < (_bufferSize + _bufferSize));
}
if (useBuffer)
{
WriteToBuffer(array, ref offset, ref count);
if (_writePos < _bufferSize)
{
Debug.Assert(count == 0);
return;
}
Debug.Assert(count >= 0);
Debug.Assert(_writePos == _bufferSize);
Debug.Assert(_buffer != null);
_stream.Write(_buffer, 0, _writePos);
_writePos = 0;
WriteToBuffer(array, ref offset, ref count);
Debug.Assert(count == 0);
Debug.Assert(_writePos < _bufferSize);
}
else
{ // if (!useBuffer)
// Write out the buffer if necessary.
if (_writePos > 0)
{
Debug.Assert(_buffer != null);
Debug.Assert(totalUserbytes >= _bufferSize);
// Try avoiding extra write to underlying stream by combining previously buffered data with current user data:
if (totalUserbytes <= (_bufferSize + _bufferSize) && totalUserbytes <= MaxShadowBufferSize)
{
EnsureShadowBufferAllocated();
Buffer.BlockCopy(array, offset, _buffer, _writePos, count);
_stream.Write(_buffer, 0, totalUserbytes);
_writePos = 0;
return;
}
_stream.Write(_buffer, 0, _writePos);
_writePos = 0;
}
// Write out user data.
_stream.Write(array, offset, count);
}
}