Make MemoryBufferWriter a Stream#1907
Conversation
- Fixed a bug with CopyTo and CopyToAsync
|
Override public virtual Task FlushAsync(CancellationToken cancellationToken)
{
return Task.Factory.StartNew(state => ((Stream)state).Flush(), this,
cancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
} |
| public void WriteMessage(HubMessage message, IBufferWriter<byte> output) | ||
| { | ||
| using (var stream = new LimitArrayPoolWriteStream()) | ||
| var writer = MemoryBufferWriter.Get(); |
There was a problem hiding this comment.
Does this write everything to array pool buffers just to get a length for prefix, then copies everything out of the buffers to the output after writing the prefix?
There was a problem hiding this comment.
Yep. We discussed ways to improve this but haven't enacted any concrete plan yet. The problem is the length is variable sized https://github.com/aspnet/SignalR/blob/dev/specs/HubProtocol.md#varint
| } | ||
| else | ||
| { | ||
| EnsureCapacity(1); |
There was a problem hiding this comment.
No need to recheck conditions
AddSegment()
_currentSegment[0] = value;
_position = 1;|
|
||
| public override void WriteByte(byte value) | ||
| { | ||
| if (_currentSegment != null && _position < _currentSegment.Length) |
There was a problem hiding this comment.
Skip bounds checks
var currentSegment = _currentSegment;
var position = _position;
if (currentSegment != null && (uint)position < (uint)currentSegment.Length)
{
currentSegment[position] = value;
_position = position + 1;
}There was a problem hiding this comment.
This actually seemed to make things slower... Trying again.
There was a problem hiding this comment.
You need both the array and index in a local rather than working off a memory location (this._currentSegment) for it to work; otherwise the Jit won't assume the values are the same when you use them to do the indexing.
There was a problem hiding this comment.
It was better for arrays that were memory references at one stage; but then it was made worse 😢 dotnet/coreclr#15756
| } | ||
|
|
||
| _bytesWritten++; | ||
| _position++; |
There was a problem hiding this comment.
Remove
_position++;As dealt with in two branches of if
| @@ -161,7 +202,7 @@ public byte[] ToArray() | |||
| { | |||
| _fullSegments[i].CopyTo(result, totalWritten); | |||
There was a problem hiding this comment.
Take array to local, save on double List lookup
var segment = _fullSegments[i];
segment.CopyTo(result, totalWritten);
totalWritten += segment.Length;|
|
||
| public override void Write(byte[] buffer, int offset, int count) | ||
| { | ||
| if (_currentSegment != null && _position + count < _currentSegment.Length) |
There was a problem hiding this comment.
Avoid overflow (argument checking aside)
_position < _currentSegment.Length - count| if (_fullSegments != null) | ||
| { | ||
| // Copy full segments | ||
| for (var i = 0; i < _fullSegments.Count; i++) |
There was a problem hiding this comment.
Isn't array, so take property to register
var count = _fullSegments.Count;
for (var i = 0; i < count; i++)Otherwise it will pull it from stack memory each loop
|
|
||
| public override void Write(byte[] buffer, int offset, int count) | ||
| { | ||
| if (_currentSegment != null && _position + count < _currentSegment.Length) |
There was a problem hiding this comment.
Use _position lots so take to local/register so it isn't always reading from stack memory
var position = _position;
if (_currentSegment != null && position < _currentSegment.Length - count)
{
Buffer.BlockCopy(buffer, offset, _currentSegment, position, count);
_position = position + count;| #if NETCOREAPP2_1 | ||
| public override void Write(ReadOnlySpan<byte> span) | ||
| { | ||
| if (_currentSegment != null && span.TryCopyTo(_currentSegment.AsSpan().Slice(_position))) |
There was a problem hiding this comment.
AsSpan takes arguments to do the slice at the same time.
There was a problem hiding this comment.
But then I have to pass the length
There was a problem hiding this comment.
_position is already length?
_currentSegment.AsSpan(0, _position)
There was a problem hiding this comment.
Ah, _position is where you start.
_currentSegment.AsSpan(_position, _currentSegment.Length - _position)? GetSpan, GetMemory do this.
There was a problem hiding this comment.
I didn't want to do that but I can.
| namespace Microsoft.AspNetCore.Internal | ||
| { | ||
| internal sealed class MemoryBufferWriter : IBufferWriter<byte> | ||
| internal sealed class MemoryBufferWriter : Stream, IBufferWriter<byte> |
There was a problem hiding this comment.
The name doesn't fit anymore. How about MemoryWriter?
There was a problem hiding this comment.
I was waiting for you to request a rename.
There was a problem hiding this comment.
Why doesn't the name fit? MemoryStreamWriter? Should it encapsulate everything it's implementing? 😄
There was a problem hiding this comment.
Also renaming a shared source file is a bit of a chore so I'd like to avoid it as part of this PR (plus the name is fine).
There was a problem hiding this comment.
MemoryStreamBufferWriter? I don't care that much 🤷♂️
There was a problem hiding this comment.
Should I added pooled in there too (I'm trolling BTW).
|
I think |
Agreed, I looked at doing that but the performance different is so minimal now, I didn't bother. |
JamesNK
left a comment
There was a problem hiding this comment.
Happy for AsSpan and BufferExtensions.Write optimizations to be done in the future.
|
@JamesNK the future is now. Coming up after this PR. |
It should be marked as AgressiveInline; its already split into |
|
Inlines dotnet/corefx#28928 |
IBufferWriter<byte>)Latest Results
Broadcast json, 1000 connections
Before
After
Previous runs
Before
After
It's slower and allocates a little bit less. Going to profile it. Small note, we don't get the benefit of the thread static caching in this test because
WriteToArrayalready passes in aMemoryPoolBufferWriterto the MessagePackProtocol.Looks like Write and WriteByte are just generally slower:
Old:
New:
Code
After optimizations