Reduce reader/writer contention in Pipelines#130884
Conversation
|
Azure Pipelines: Successfully started running 3 pipeline(s). 12 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
=== typical counts before the PR: |
|
=== typical counts after the PR: |
There was a problem hiding this comment.
Pull request overview
This PR updates System.IO.Pipelines internals to reduce reader/writer lock contention by changing how BufferSegment instances and backing memory are acquired/returned, and adjusts related scheduling and tests.
Changes:
- Switch
Pipe’s segment pooling from a stack to an SPSC queue and return consumed segments outside the main lock. - Speculatively rent backing memory outside the lock and add a lock-free fast path for
Advancewhen writing is active. - Update the segment-pool reuse test expectation (FIFO) and set ThreadPool scheduling to prefer local queues.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.IO.Pipelines/tests/BufferSegmentPoolTest.cs | Updates pooling test to expect FIFO reuse order. |
| src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/ThreadPoolScheduler.netcoreapp.cs | Uses preferLocal: true for ThreadPool scheduling. |
| src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/PipeOptions.cs | Updates segment pool sizing defaults/comments (but currently misses required property assignment). |
| src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/Pipe.cs | Moves to SPSC pooling + reduces time under the shared lock (memory rent + segment return). |
| src/libraries/System.IO.Pipelines/src/System.IO.Pipelines.csproj | Links in SPSC queue + padding shared sources (and introduces a BOM). |
|
Benchmark used (--scenario json --profile aspnet-gold-lin): The changes to notice:
- Max Lock Contention (#/s) | 483
+ Max Lock Contention (#/s) | 66
- Requests/sec | 2,094,374
+ Requests/sec | 2,148,465
- Read throughput (MB/s) | 291.61
+ Read throughput (MB/s) | 299.14 |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| // The check for the current pooled count may race with dequeing, | ||
| // but occasional overestimating is ok here. | ||
| if (_bufferSegmentPool.Count < _options.MaxSegmentPoolSize) | ||
| { | ||
| _bufferSegmentPool.Push(segment); | ||
| _bufferSegmentPool.Enqueue(segment); | ||
| } |
There was a problem hiding this comment.
Safe enough for us. Overestimating due to concurrent dequeuing should be rare and just means that an edge case when we cannot recycle a segment due to queue being too full may happen, while if we had up to date count it would not. (or it would still happen if enqueuer was a bit faster).
The Count is O(n) only in transitional state when queue grows to larger capacity. Otherwise there is only one underlying segment.
Because of double-up growth policy, even in edge cases, the queue can grow only a few times before reaching limits of our use.
| lock (SyncObj) | ||
| { |
There was a problem hiding this comment.
this is not a real "leak". It is ok to GC in exceptional situations.
|
Azure Pipelines: Successfully started running 3 pipeline(s). 12 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
| CompletionData completionData = default; | ||
|
|
||
| lock (SyncObj) | ||
| try |
There was a problem hiding this comment.
The diff looks big, but it is the same code as before just wrapped in a try and the segment-releasing loop (while (returnStart != null && returnStart != returnEnd)) is taken out of lock and into the finally that follows.
|
Build failures on NET481 seems unrelated to the change: D:\a_work\1\s\src\libraries\Microsoft.Extensions.Caching.Abstractions\tests\TrimmingTests\HybridCacheEntryOptionsAccessorTest.cs(17,6): error CS0246: The type or namespace name 'UnsafeAccessorAttribute' could not be found (are you missing a using directive or an assembly reference?) [D:\a_work\1\s\src\libraries\Microsoft.Extensions.Caching.Abstractions\tests\Microsoft.Extensions.Caching.Abstractions.Tests.csproj::TargetFramework=net481] |
The change deals with excessive contentions coming from Pipelines in some benchmarks when reader and writer contend for the lock used to protect shared state of the Pipe.
Changes:
For example, writer thread, if it looks for more work after writing, would be preferred to take care of asynchronous reading and processing of written data, thus further reducing contention and improving locality of access.
=== effect on JSON asp.net benchmark running on a 56-core machine: