Skip to content
This repository was archived by the owner on Nov 20, 2018. It is now read-only.

Adds PipeWriterAdapter #1065

Merged
merged 26 commits into from
Nov 17, 2018
Merged

Adds PipeWriterAdapter #1065

merged 26 commits into from
Nov 17, 2018

Conversation

jkotalik
Copy link
Contributor

Half of dotnet/aspnetcore#3966.

Few questions:

  • What should we do with OnReaderCompleted? If we implement it, it means that we would probably need to create a PipeReaderAdapter (or somehow make a shared object).
  • I think I need to synchronize FlushAsync and CancelPendingFlush

Tests

Performance

Current performance numbers:

Method Mean Error StdDev Median Op/s Gen 0/1k Op Gen 1/1k Op Gen 2/1k Op Allocated Memory/Op
WriteHelloWorld 6.280 us 0.1119 us 1.033 us 5.965 us 159,246.75 - - - 4.72 KB
WriteHelloWorldLargeNumberOfWrites 2,515.595 us 23.2819 us 222.184 us 2,512.710 us 397.52 - - - 4256.74 KB
WriteHelloWorldLargeWrite 22.644 us 0.3660 us 3.410 us 22.165 us 44,162.09 - - - 117.45 KB
WriteHelloWorldLargeNumberOfLargeWrites 16,120.442 us 70.6716 us 652.178 us 15,988.595 us 62.03 1000.0000 1000.0000 1000.0000 28904.27 KB

Add benchmarks

Remove benchmark artifacts

Cleanup

remove extra stuff

nit
@jkotalik jkotalik requested review from davidfowl and pakrym November 13, 2018 18:44
@jkotalik jkotalik changed the title Implemented PipeWriterAdapter Adds PipeWriterAdapter Nov 13, 2018
}

// Get a new buffer using the minimum segment size, unless the size hint is larger than a single segment.
_currentSegment = ArrayPool<byte>.Shared.Rent(Math.Max(_minimumSegmentSize, sizeHint));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take IMemoryPool as ctor argument.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm not sure we want that in the default path.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArrayPool<byte>.Shared devirtualizes now dotnet/coreclr#20637 so if you were using IMemoryPool you'd want something like

var size = Math.Max(_minimumSegmentSize, sizeHint);
_currentSegment = _pool?.AllocByte(size) ?? ArrayPool<byte>.Shared.Rent(size);

Though, isn't IMemoryPool in the wrong namespace? (i.e. downstream)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can start with it and look at benchmark results.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMemoryPool allocates like a mofo, we can avoid it in this case with little downside.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we do the same in pipe then?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or think about the way to fix that allocation

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some concrete benefits you get from the IMemoryPool

  1. Somebody can plug in an implementation (it's an abstraction)
  2. We have an implementation (in ASP.NET Core) that uses pre-pinned memory (a variant of the first one)

The default implementation (ArrayMemoryPool) allocates a managed object per segment. It's not that big a deal when doing large allocations but can be avoided completely when using the array pool. We get no benefit in ASP.NET Core if these Streams use pre-pinned buffers since the Streams we'll be writing to won't be pinning buffer for async reads. That happens in the transport layer in Kestrel.

Before reverting to use the IMemoryPool, we should get the baseline so we can measure the differences.

Copy link
Contributor

@pakrym pakrym left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see more implementation specific tests that assert expectations about how data should be written to the stream, memory pool block management, etc.

/// <summary>
/// Implements PipeWriter using a base stream implementation.
/// </summary>
public class PipeWriterAdapter : PipeWriter, IDisposable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

override WriteAsync and provide an optimized path that writes directly to the Stream if there's no buffered data.

Copy link
Contributor Author

@jkotalik jkotalik Nov 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you say "No buffered data", you mean no data in _completedSegments or _currentSegment?

@jkotalik
Copy link
Contributor Author

Better perf numbers.

BenchmarkDotNet=v0.10.13, OS=Windows 10.0.17134
Intel Xeon CPU E5-1650 v4 3.60GHz, 1 CPU, 12 logical cores and 6 physical cores
.NET Core SDK=2.2.100-preview2-009404
  [Host]     : .NET Core 2.2.0-preview2-26905-02 (CoreCLR 4.6.26905.03, CoreFX 4.6.26905.02), 64bit RyuJIT
  Job-MVJLBU : .NET Core 2.2.0-preview2-26905-02 (CoreCLR 4.6.26905.03, CoreFX 4.6.26905.02), 64bit RyuJIT

Runtime=Core  Server=True  Toolchain=.NET Core 2.2  
RunStrategy=Throughput  
Method Mean Error StdDev Op/s Allocated
WriteHelloWorld 149.7 ns 2.952 ns 4.327 ns 6,679,947.5 32 B
WriteHelloWorldLargeWrite 60,649.7 ns 812.198 ns 759.731 ns 16,488.1 100002 B

#else
#error Target frameworks need to be updated.
#endif
segment.Return();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to test more cancellation scenarios. Current logic would do crazy things if you continue writing after CancelPendingFlush (you always reset _bytesWritten but clear segments only after writing them all, this might cause double writes/double returns/etc).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still adding a few more tests.

@davidfowl
Copy link
Member

#1065 (comment)

These numbers don't sit well with me. I don't see how the allocated bytes can be that similar.

}

// Note this currently doesn't do anything.
// Implementing completions/callbacks would require creating a PipeReaderAdapter.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throw a NotSupportedException

@pakrym
Copy link
Contributor

pakrym commented Nov 16, 2018

What are the latest perf results?

@jkotalik
Copy link
Contributor Author

Removed the MemoryStream and made an underlying noop stream instead.

Method Mean Error StdDev Op/s Allocated
WriteHelloWorld 179.2 ns 3.603 ns 6.404 ns 5,580,116.0 0 B
WriteHelloWorldLargeWrite 2,029.8 ns 40.592 ns 94.883 ns 492,654.3 0 B

#endif
_bytesWritten -= segment.Length;
segment.Return();
_completedSegments.RemoveAt(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For next PR: this is slow and we don't have a benchmark that exercises this code path at all.

Copy link
Contributor

@pakrym pakrym left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems fine for the first iteration.


public void Return()
{
_memoryOwner.Dispose();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the next time: null _memoryOwner and add assert to avoid double returns and catch bugs.

@jkotalik jkotalik merged commit 962ec07 into master Nov 17, 2018
@natemcmaster natemcmaster deleted the jkotalik/writeAdapter branch November 20, 2018 06:29
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants