FileStream rewrite: Caching the ValueTaskSource in AsyncWindowsFileStreamStrategy - #51363

Merged
Anipik merged 15 commits into
dotnet:mainfrom
carlossanlop:Caching
Apr 16, 2021
Merged

FileStream rewrite: Caching the ValueTaskSource in AsyncWindowsFileStreamStrategy#51363
Anipik merged 15 commits into
dotnet:mainfrom
carlossanlop:Caching

Conversation

@carlossanlop

@carlossanlopcarlossanlop commented Apr 16, 2021

Copy link
Copy Markdown
Contributor

Fixes#50972
Fixes#25074

When AsyncWindowsFileStreamStrategy is wrapped by a BufferedFileStreamStrategy, we need to make sure the ValueTaskSource instance is cached to reduce the number of allocations when calling ReadAsync or WriteAsync multiple times in a row.

This PR is a continuation of #50802, in which we switched from using TaskCompletionSource to IValueTaskSource.

Changes:

  • Moved the PreAllocatedOverlapped instance inside ValueTaskSource, so the latter becomes its owner. This was done because we are only supposed to have an instance of a PreAllocatedOverlapped if the ValueTaskSource was created from OnBufferedAllocated, which is a method called only by BufferedFileStreamStrategy right before writing or reading.
  • Removed MemoryValueTaskSource and moved the cases handled by it to ValueTaskSource.
  • Created a method that refreshes the value of the NativeOverlapped*. This is done every time we call ReadAsync/WriteAsync, to make sure we are pinning the memory passed by the user.

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @carlossanlop
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #50972

DAFT: Pending benchmarks.

When AsyncWindowsFileStreamStrategy is wrapped by a BufferedFileStreamStrategy, we need to make sure the ValueTaskSource instance is cached to reduce the number of allocations when calling ReadAsync or WriteAsync multiple times in a row.

This PR is a continuation of #50802, in which we switched from using TaskCompletionSource to IValueTaskSource.

Changes:

  • Moved the PreAllocatedOverlapped instance inside ValueTaskSource, so the latter becomes its owner. This was done because we are only supposed to have an instance of a PreAllocatedOverlapped if the ValueTaskSource was created from OnBufferedAllocated, which is a method called only by BufferedFileStreamStrategy right before writing or reading.
  • Removed MemoryValueTaskSource and moved the cases handled by it to ValueTaskSource.
  • Created a method that refreshes the value of the NativeOverlapped*. This is done every time we call ReadAsync/WriteAsync, to make sure we are pinning the memory passed by the user.
Author:carlossanlop
Assignees:carlossanlop
Labels:

area-System.IO

Milestone:6.0.0

@adamsitnik

Copy link
Copy Markdown
Member

Initial benchmark results (base is #50802):

Details
MethodToolchainfileSizeuserBufferSizeoptionsMeanRatioGen 0Allocated
ReadAsync\cache\corerun.exe10241024Asynchronous83.99 us1.020.33605 KB
ReadAsync\base\corerun.exe10241024Asynchronous81.98 us1.000.65455 KB
WriteAsync\cache\corerun.exe10241024Asynchronous487.70 us1.02-5 KB
WriteAsync\true\corerun.exe10241024Asynchronous476.89 us1.00-5 KB
ReadAsync\cache\corerun.exe1048576512Asynchronous2,649.16 us1.0910.4167127 KB
ReadAsync\true\corerun.exe1048576512Asynchronous2,421.94 us1.008.928683 KB
WriteAsync\cache\corerun.exe1048576512Asynchronous4,527.33 us1.1115.6250119 KB
WriteAsync\true\corerun.exe1048576512Asynchronous4,100.58 us1.00-75 KB
ReadAsync\cache\corerun.exe10485764096Asynchronous2,321.45 us1.018.928671 KB
ReadAsync\true\corerun.exe10485764096Asynchronous2,308.26 us1.008.928669 KB
WriteAsync\cache\corerun.exe10485764096Asynchronous4,191.59 us1.03-97 KB
WriteAsync\true\corerun.exe10485764096Asynchronous4,077.05 us1.00-74 KB
ReadAsync_NoBuffering\cache\corerun.exe104857616384Asynchronous730.67 us1.00-18 KB
ReadAsync_NoBuffering\true\corerun.exe104857616384Asynchronous732.58 us1.00-17 KB
WriteAsync_NoBuffering\cache\corerun.exe104857616384Asynchronous2,747.11 us0.99-18 KB
WriteAsync_NoBuffering\true\corerun.exe104857616384Asynchronous2,787.66 us1.00-17 KB
ReadAsync\cache\corerun.exe1048576004096Asynchronous250,998.78 us1.01-7,001 KB
ReadAsync\true\corerun.exe1048576004096Asynchronous249,280.08 us1.00-6,801 KB
WriteAsync\cache\corerun.exe1048576004096Asynchronous382,734.73 us1.041000.00009,205 KB
WriteAsync\true\corerun.exe1048576004096Asynchronous368,403.91 us1.00-6,905 KB
ReadAsync_NoBuffering\cache\corerun.exe10485760016384Asynchronous80,072.69 us0.97-1,750 KB
ReadAsync_NoBuffering\true\corerun.exe10485760016384Asynchronous82,376.92 us1.00-1,700 KB
WriteAsync_NoBuffering\cache\corerun.exe10485760016384Asynchronous120,385.98 us0.98-1,751 KB
WriteAsync_NoBuffering\true\corerun.exe10485760016384Asynchronous123,215.33 us1.00-1,701 KB

it looks like we are allocating less when buffering is disabled, but more than before when it's enabled

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall looks good to me, but we need to track and solve the allocation regression which is visible in the scenarios where buffering is enabled

…to the moment after _source.SetException|SetResult is called
@stephentoub

Copy link
Copy Markdown
Member

The interlocked operation that was trying to enable the instance to be reused was backwards... nothing was being pooled. The numbers will need to be re-run after that's fixed ;-)

@adamsitnik

Copy link
Copy Markdown
Member

The interlocked operation that was trying to enable the instance to be reused was backwards... nothing was being pooled.

You are right, that was the reason for the regression. The latest numbers show that allocations have been reduced:

MethodJobfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsyncafter10241024Asynchronous82.48 us1.005 KB
ReadAsyncbefore10241024Asynchronous82.12 us1.005 KB
WriteAsyncafter10241024Asynchronous500.23 us1.005 KB
WriteAsyncbefore10241024Asynchronous498.59 us1.005 KB
CopyToFileAsyncafter1024?None520.65 us1.005 KB
CopyToFileAsyncbefore1024?None522.37 us1.005 KB
CopyToFileAsyncafter1024?Asynchronous548.74 us1.006 KB
CopyToFileAsyncbefore1024?Asynchronous548.45 us1.006 KB
ReadAsyncafter1048576512Asynchronous2,354.25 us0.9857 KB
ReadAsyncbefore1048576512Asynchronous2,396.26 us1.0083 KB
WriteAsyncafter1048576512Asynchronous4,100.29 us0.9849 KB
WriteAsyncbefore1048576512Asynchronous4,183.45 us1.0075 KB
ReadAsyncafter10485764096Asynchronous2,355.74 us1.0137 KB
ReadAsyncbefore10485764096Asynchronous2,341.08 us1.0069 KB
WriteAsyncafter10485764096Asynchronous4,045.21 us0.9945 KB
WriteAsyncbefore10485764096Asynchronous4,073.02 us1.0074 KB
ReadAsync_NoBufferingafter104857616384Asynchronous729.91 us1.0210 KB
ReadAsync_NoBufferingbefore104857616384Asynchronous717.17 us1.0017 KB
WriteAsync_NoBufferingafter104857616384Asynchronous2,820.46 us1.0110 KB
WriteAsync_NoBufferingbefore104857616384Asynchronous2,793.28 us1.0017 KB
CopyToFileAsyncafter1048576?None2,338.90 us1.003 KB
CopyToFileAsyncbefore1048576?None2,343.55 us1.003 KB
CopyToFileAsyncafter1048576?Asynchronous2,574.37 us1.013 KB
CopyToFileAsyncbefore1048576?Asynchronous2,564.43 us1.004 KB
ReadAsyncafter1048576004096Asynchronous248,390.61 us0.983,601 KB
ReadAsyncbefore1048576004096Asynchronous252,787.95 us1.006,801 KB
WriteAsyncafter1048576004096Asynchronous357,904.15 us0.994,005 KB
WriteAsyncbefore1048576004096Asynchronous360,081.89 us1.006,905 KB
ReadAsync_NoBufferingafter10485760016384Asynchronous80,097.02 us0.99901 KB
ReadAsync_NoBufferingbefore10485760016384Asynchronous81,006.52 us1.001,701 KB
WriteAsync_NoBufferingafter10485760016384Asynchronous120,078.07 us0.97901 KB
WriteAsync_NoBufferingbefore10485760016384Asynchronous123,329.13 us1.001,701 KB
CopyToFileAsyncafter104857600?None73,213.21 us0.99177 KB
CopyToFileAsyncbefore104857600?None73,740.59 us1.00177 KB
CopyToFileAsyncafter104857600?Asynchronous87,047.37 us1.00114 KB
CopyToFileAsyncbefore104857600?Asynchronous87,193.75 us1.00214 KB

@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub your suggestions were great!

MethodJobfileSizeuserBufferSizeoptionsMeanRatioGen 0Allocated
ReadAsyncafter10241024Asynchronous87.48 us1.040.36135,240 B
ReadAsyncbefore10241024Asynchronous84.38 us1.000.34725,216 B
WriteAsyncafter10241024Asynchronous494.43 us0.99-4,960 B
WriteAsyncbefore10241024Asynchronous501.52 us1.00-4,936 B
CopyToFileAsyncafter1024?None513.31 us1.01-5,593 B
CopyToFileAsyncbefore1024?None510.34 us1.00-5,593 B
CopyToFileAsyncafter1024?Asynchronous539.68 us1.00-6,336 B
CopyToFileAsyncbefore1024?Asynchronous541.15 us1.00-6,320 B
ReadAsyncafter1048576512Asynchronous2,436.66 us0.98-58,279 B
ReadAsyncbefore1048576512Asynchronous2,480.83 us1.008.928684,775 B
WriteAsyncafter1048576512Asynchronous4,142.67 us0.92-50,029 B
WriteAsyncbefore1048576512Asynchronous4,511.11 us1.00-76,527 B
ReadAsyncafter10485764096Asynchronous2,153.31 us0.90-916 B
ReadAsyncbefore10485764096Asynchronous2,397.80 us1.008.928670,241 B
WriteAsyncafter10485764096Asynchronous3,976.06 us0.97-27,596 B
WriteAsyncbefore10485764096Asynchronous4,100.25 us1.00-75,562 B
ReadAsync_NoBufferingafter104857616384Asynchronous689.04 us0.94-760 B
ReadAsync_NoBufferingbefore104857616384Asynchronous733.21 us1.00-17,864 B
WriteAsync_NoBufferingafter104857616384Asynchronous2,781.44 us0.99-762 B
WriteAsync_NoBufferingbefore104857616384Asynchronous2,808.65 us1.00-17,866 B
CopyToFileAsyncafter1048576?None2,360.16 us0.98-3,244 B
CopyToFileAsyncbefore1048576?None2,405.43 us1.00-3,244 B
CopyToFileAsyncafter1048576?Asynchronous2,593.22 us1.01-2,044 B
CopyToFileAsyncbefore1048576?Asynchronous2,568.19 us1.00-3,924 B
ReadAsyncafter1048576004096Asynchronous247,853.42 us0.95-1,368 B
ReadAsyncbefore1048576004096Asynchronous259,775.67 us1.00-6,963,952 B
WriteAsyncafter1048576004096Asynchronous358,502.58 us0.97-2,258,912 B
WriteAsyncbefore1048576004096Asynchronous371,005.92 us1.00-7,070,648 B
ReadAsync_NoBufferingafter10485760016384Asynchronous76,073.40 us0.94-796 B
ReadAsync_NoBufferingbefore10485760016384Asynchronous81,316.51 us1.00-1,741,292 B
WriteAsync_NoBufferingafter10485760016384Asynchronous114,458.43 us0.88-832 B
WriteAsync_NoBufferingbefore10485760016384Asynchronous130,608.83 us1.00-1,741,328 B
CopyToFileAsyncafter104857600?None74,758.01 us1.01-180,828 B
CopyToFileAsyncbefore104857600?None73,939.22 us1.00-180,828 B
CopyToFileAsyncafter104857600?Asynchronous86,110.81 us0.98-2,220 B
CopyToFileAsyncbefore104857600?Asynchronous87,546.69 us1.00-219,524 B

@adamsitnik
adamsitnik marked this pull request as ready for review April 16, 2021 16:30
@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub I believe I have addressed all your feedback, PTAL one more time. I hope that we can merge it today and include it in Preview 4

@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub we have addressed the feedback, please take a look. I am going to post the benchmark results in 20-30 minutes

@stephentoub

Copy link
Copy Markdown
Member

This now also fixes #25074

@adamsitnik

Copy link
Copy Markdown
Member

The results (see the Allocated column)

MethodJobfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsyncafter10241024Asynchronous84.39 us0.985,240 B
ReadAsyncbefore10241024Asynchronous85.86 us1.005,216 B
WriteAsyncafter10241024Asynchronous483.68 us1.014,960 B
WriteAsyncbefore10241024Asynchronous478.92 us1.004,936 B
CopyToFileAsyncafter1024?None492.53 us1.015,593 B
CopyToFileAsyncbefore1024?None489.18 us1.005,593 B
CopyToFileAsyncafter1024?Asynchronous529.35 us1.026,336 B
CopyToFileAsyncbefore1024?Asynchronous521.85 us1.006,311 B
ReadAsyncafter1048576512Asynchronous2,371.68 us1.0058,279 B
ReadAsyncbefore1048576512Asynchronous2,372.78 us1.0084,775 B
WriteAsyncafter1048576512Asynchronous4,081.33 us0.9750,028 B
WriteAsyncbefore1048576512Asynchronous4,214.84 us1.0076,517 B
ReadAsyncafter10485764096Asynchronous2,138.36 us0.92913 B
ReadAsyncbefore10485764096Asynchronous2,332.66 us1.0070,241 B
WriteAsyncafter10485764096Asynchronous3,951.54 us0.9527,562 B
WriteAsyncbefore10485764096Asynchronous4,153.60 us1.0075,562 B
ReadAsync_NoBufferingafter104857616384Asynchronous674.58 us0.91760 B
ReadAsync_NoBufferingbefore104857616384Asynchronous740.07 us1.0017,864 B
WriteAsync_NoBufferingafter104857616384Asynchronous2,711.34 us0.99762 B
WriteAsync_NoBufferingbefore104857616384Asynchronous2,736.59 us1.0017,866 B
CopyToFileAsyncafter1048576?None1,961.68 us0.953,243 B
CopyToFileAsyncbefore1048576?None2,080.32 us1.003,244 B
CopyToFileAsyncafter1048576?Asynchronous2,283.20 us1.082,044 B
CopyToFileAsyncbefore1048576?Asynchronous2,161.78 us1.003,924 B
ReadAsyncafter1048576004096Asynchronous228,777.04 us0.931,056 B
ReadAsyncbefore1048576004096Asynchronous245,615.52 us1.006,963,952 B
WriteAsyncafter1048576004096Asynchronous353,822.32 us0.962,257,976 B
WriteAsyncbefore1048576004096Asynchronous370,913.51 us1.007,070,648 B
ReadAsync_NoBufferingafter10485760016384Asynchronous74,983.01 us0.95796 B
ReadAsync_NoBufferingbefore10485760016384Asynchronous79,064.04 us1.001,741,292 B
WriteAsync_NoBufferingafter10485760016384Asynchronous116,660.24 us0.95832 B
WriteAsync_NoBufferingbefore10485760016384Asynchronous123,323.67 us1.001,741,328 B
CopyToFileAsyncafter104857600?None73,465.73 us0.99180,828 B
CopyToFileAsyncbefore104857600?None74,040.22 us1.00180,828 B
CopyToFileAsyncafter104857600?Asynchronous85,207.68 us0.982,220 B
CopyToFileAsyncbefore104857600?Asynchronous87,037.81 us1.00219,524 B

@jeffhandley

Copy link
Copy Markdown
Member

Wowza some of those allocation improvements are incredible!

@Anipik
Anipik merged commit 7878130 into dotnet:mainApr 16, 2021
@carlossanlop
carlossanlop deleted the Caching branch April 16, 2021 23:07
@danmoseley

Copy link
Copy Markdown
Contributor

Nice.

{
_strategy.OnBufferAllocated(_buffer = new byte[_bufferSize]);
_buffer = GC.AllocateUninitializedArray<byte>(_bufferSize,
pinned: true); // this allows us to avoid pinning when the buffer is used for the syscalls

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That correct, but pinned: true also allocates the array in Gen2 as side-effect so this may actually hurt real-world scenarios at the end..

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can allocate it and use a GCHandle if that ends up being better. Previously it was pinned as part of a PreallocatedOverlapped.

(It's still not at all obvious when this newfangled POH should be used. )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can also investigate not pinning at all, in which case the code this interacts with will just create a gchandle for each operation.

And/or look at using a pool array, but we'd want to ensure enough synchronization was in place to minimize erroneous usage causing us to return an array still in use. We do that in a few other streams.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Here is a simple test:

using System;
using System.IO;
for (int i = 0; i < 100_000; i++)
{
using (var f = new FileStream("test", FileMode.Create))
{
f.WriteByte(42);
}
}
Console.WriteLine($"Allocated: {GC.GetTotalAllocatedBytes()} Gen2 GCs: {GC.CollectionCount(2)}");
  • .NET 5: Allocated: 442474096 Gen2 GCs: 0
  • This PR: Allocated: 448051624 Gen2 GCs: 103

It will be interesting to see whether these excessive Gen2 GCs hit performance gates of services trying .NET 6 previews.

It's still not at all obvious when this newfangled POH should be used.

Agree. It is very hard to use.

we'd want to ensure enough synchronization was in place to minimize erroneous usage causing us to return an array still in use

If you can cover all these cases, it may be better to use unmanaged buffer. It is pinned too, and it does not cause excessive Gen2 GCs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So allocating on the POH contributes to the gen2 budget. This is why we disable the buffer using size 1, that still works right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nothing in this file is used at all if buffer size is 1.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm looking forward to taking another stab at optimizing static files in .NET 6

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you can cover all these cases, it may be better to use unmanaged buffer. It is pinned too, and it does not cause excessive Gen2 GCs.

I'm going to start with a GCHandle and a normally allocated array. I believe in that case I can mostly restrict synchronization to the async code paths (plus disposal). If we use a native buffer, we'll need to protect the sync code paths as well. We can start with this and then see if it makes sense to use a pooled or native buffer as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Well, actually, I'm going to start by not pinning here at all (it'll then pin/unpin in the rest of the implementation per operation). If there's no measurable impact, we can stick with that for now.

@adamsitnikadamsitnik mentioned this pull request Apr 17, 2021
5 tasks
@ghostghost locked as resolved and limited conversation to collaborators May 17, 2021
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cache IValueTaskSource in AsyncWindowsFileStreamStrategy Use PreallocatedOverlapped when internal FileStream buffer isn't being used on Windows

9 participants

@carlossanlop@adamsitnik@stephentoub@jeffhandley@danmoseley@davidfowl@jkotas@jozkee@Anipik
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

FileStream rewrite: Caching the ValueTaskSource in AsyncWindowsFileStreamStrategy - #51363

Merged
Anipik merged 15 commits into
dotnet:mainfrom
carlossanlop:Caching
Apr 16, 2021
Merged

FileStream rewrite: Caching the ValueTaskSource in AsyncWindowsFileStreamStrategy#51363
Anipik merged 15 commits into
dotnet:mainfrom
carlossanlop:Caching

Conversation

@carlossanlop

@carlossanlopcarlossanlop commented Apr 16, 2021

Copy link
Copy Markdown
Contributor

Fixes#50972
Fixes#25074

When AsyncWindowsFileStreamStrategy is wrapped by a BufferedFileStreamStrategy, we need to make sure the ValueTaskSource instance is cached to reduce the number of allocations when calling ReadAsync or WriteAsync multiple times in a row.

This PR is a continuation of #50802, in which we switched from using TaskCompletionSource to IValueTaskSource.

Changes:

  • Moved the PreAllocatedOverlapped instance inside ValueTaskSource, so the latter becomes its owner. This was done because we are only supposed to have an instance of a PreAllocatedOverlapped if the ValueTaskSource was created from OnBufferedAllocated, which is a method called only by BufferedFileStreamStrategy right before writing or reading.
  • Removed MemoryValueTaskSource and moved the cases handled by it to ValueTaskSource.
  • Created a method that refreshes the value of the NativeOverlapped*. This is done every time we call ReadAsync/WriteAsync, to make sure we are pinning the memory passed by the user.

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @carlossanlop
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #50972

DAFT: Pending benchmarks.

When AsyncWindowsFileStreamStrategy is wrapped by a BufferedFileStreamStrategy, we need to make sure the ValueTaskSource instance is cached to reduce the number of allocations when calling ReadAsync or WriteAsync multiple times in a row.

This PR is a continuation of #50802, in which we switched from using TaskCompletionSource to IValueTaskSource.

Changes:

  • Moved the PreAllocatedOverlapped instance inside ValueTaskSource, so the latter becomes its owner. This was done because we are only supposed to have an instance of a PreAllocatedOverlapped if the ValueTaskSource was created from OnBufferedAllocated, which is a method called only by BufferedFileStreamStrategy right before writing or reading.
  • Removed MemoryValueTaskSource and moved the cases handled by it to ValueTaskSource.
  • Created a method that refreshes the value of the NativeOverlapped*. This is done every time we call ReadAsync/WriteAsync, to make sure we are pinning the memory passed by the user.
Author:carlossanlop
Assignees:carlossanlop
Labels:

area-System.IO

Milestone:6.0.0

@adamsitnik

Copy link
Copy Markdown
Member

Initial benchmark results (base is #50802):

Details
MethodToolchainfileSizeuserBufferSizeoptionsMeanRatioGen 0Allocated
ReadAsync\cache\corerun.exe10241024Asynchronous83.99 us1.020.33605 KB
ReadAsync\base\corerun.exe10241024Asynchronous81.98 us1.000.65455 KB
WriteAsync\cache\corerun.exe10241024Asynchronous487.70 us1.02-5 KB
WriteAsync\true\corerun.exe10241024Asynchronous476.89 us1.00-5 KB
ReadAsync\cache\corerun.exe1048576512Asynchronous2,649.16 us1.0910.4167127 KB
ReadAsync\true\corerun.exe1048576512Asynchronous2,421.94 us1.008.928683 KB
WriteAsync\cache\corerun.exe1048576512Asynchronous4,527.33 us1.1115.6250119 KB
WriteAsync\true\corerun.exe1048576512Asynchronous4,100.58 us1.00-75 KB
ReadAsync\cache\corerun.exe10485764096Asynchronous2,321.45 us1.018.928671 KB
ReadAsync\true\corerun.exe10485764096Asynchronous2,308.26 us1.008.928669 KB
WriteAsync\cache\corerun.exe10485764096Asynchronous4,191.59 us1.03-97 KB
WriteAsync\true\corerun.exe10485764096Asynchronous4,077.05 us1.00-74 KB
ReadAsync_NoBuffering\cache\corerun.exe104857616384Asynchronous730.67 us1.00-18 KB
ReadAsync_NoBuffering\true\corerun.exe104857616384Asynchronous732.58 us1.00-17 KB
WriteAsync_NoBuffering\cache\corerun.exe104857616384Asynchronous2,747.11 us0.99-18 KB
WriteAsync_NoBuffering\true\corerun.exe104857616384Asynchronous2,787.66 us1.00-17 KB
ReadAsync\cache\corerun.exe1048576004096Asynchronous250,998.78 us1.01-7,001 KB
ReadAsync\true\corerun.exe1048576004096Asynchronous249,280.08 us1.00-6,801 KB
WriteAsync\cache\corerun.exe1048576004096Asynchronous382,734.73 us1.041000.00009,205 KB
WriteAsync\true\corerun.exe1048576004096Asynchronous368,403.91 us1.00-6,905 KB
ReadAsync_NoBuffering\cache\corerun.exe10485760016384Asynchronous80,072.69 us0.97-1,750 KB
ReadAsync_NoBuffering\true\corerun.exe10485760016384Asynchronous82,376.92 us1.00-1,700 KB
WriteAsync_NoBuffering\cache\corerun.exe10485760016384Asynchronous120,385.98 us0.98-1,751 KB
WriteAsync_NoBuffering\true\corerun.exe10485760016384Asynchronous123,215.33 us1.00-1,701 KB

it looks like we are allocating less when buffering is disabled, but more than before when it's enabled

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall looks good to me, but we need to track and solve the allocation regression which is visible in the scenarios where buffering is enabled

…to the moment after _source.SetException|SetResult is called
@stephentoub

Copy link
Copy Markdown
Member

The interlocked operation that was trying to enable the instance to be reused was backwards... nothing was being pooled. The numbers will need to be re-run after that's fixed ;-)

@adamsitnik

Copy link
Copy Markdown
Member

The interlocked operation that was trying to enable the instance to be reused was backwards... nothing was being pooled.

You are right, that was the reason for the regression. The latest numbers show that allocations have been reduced:

MethodJobfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsyncafter10241024Asynchronous82.48 us1.005 KB
ReadAsyncbefore10241024Asynchronous82.12 us1.005 KB
WriteAsyncafter10241024Asynchronous500.23 us1.005 KB
WriteAsyncbefore10241024Asynchronous498.59 us1.005 KB
CopyToFileAsyncafter1024?None520.65 us1.005 KB
CopyToFileAsyncbefore1024?None522.37 us1.005 KB
CopyToFileAsyncafter1024?Asynchronous548.74 us1.006 KB
CopyToFileAsyncbefore1024?Asynchronous548.45 us1.006 KB
ReadAsyncafter1048576512Asynchronous2,354.25 us0.9857 KB
ReadAsyncbefore1048576512Asynchronous2,396.26 us1.0083 KB
WriteAsyncafter1048576512Asynchronous4,100.29 us0.9849 KB
WriteAsyncbefore1048576512Asynchronous4,183.45 us1.0075 KB
ReadAsyncafter10485764096Asynchronous2,355.74 us1.0137 KB
ReadAsyncbefore10485764096Asynchronous2,341.08 us1.0069 KB
WriteAsyncafter10485764096Asynchronous4,045.21 us0.9945 KB
WriteAsyncbefore10485764096Asynchronous4,073.02 us1.0074 KB
ReadAsync_NoBufferingafter104857616384Asynchronous729.91 us1.0210 KB
ReadAsync_NoBufferingbefore104857616384Asynchronous717.17 us1.0017 KB
WriteAsync_NoBufferingafter104857616384Asynchronous2,820.46 us1.0110 KB
WriteAsync_NoBufferingbefore104857616384Asynchronous2,793.28 us1.0017 KB
CopyToFileAsyncafter1048576?None2,338.90 us1.003 KB
CopyToFileAsyncbefore1048576?None2,343.55 us1.003 KB
CopyToFileAsyncafter1048576?Asynchronous2,574.37 us1.013 KB
CopyToFileAsyncbefore1048576?Asynchronous2,564.43 us1.004 KB
ReadAsyncafter1048576004096Asynchronous248,390.61 us0.983,601 KB
ReadAsyncbefore1048576004096Asynchronous252,787.95 us1.006,801 KB
WriteAsyncafter1048576004096Asynchronous357,904.15 us0.994,005 KB
WriteAsyncbefore1048576004096Asynchronous360,081.89 us1.006,905 KB
ReadAsync_NoBufferingafter10485760016384Asynchronous80,097.02 us0.99901 KB
ReadAsync_NoBufferingbefore10485760016384Asynchronous81,006.52 us1.001,701 KB
WriteAsync_NoBufferingafter10485760016384Asynchronous120,078.07 us0.97901 KB
WriteAsync_NoBufferingbefore10485760016384Asynchronous123,329.13 us1.001,701 KB
CopyToFileAsyncafter104857600?None73,213.21 us0.99177 KB
CopyToFileAsyncbefore104857600?None73,740.59 us1.00177 KB
CopyToFileAsyncafter104857600?Asynchronous87,047.37 us1.00114 KB
CopyToFileAsyncbefore104857600?Asynchronous87,193.75 us1.00214 KB

@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub your suggestions were great!

MethodJobfileSizeuserBufferSizeoptionsMeanRatioGen 0Allocated
ReadAsyncafter10241024Asynchronous87.48 us1.040.36135,240 B
ReadAsyncbefore10241024Asynchronous84.38 us1.000.34725,216 B
WriteAsyncafter10241024Asynchronous494.43 us0.99-4,960 B
WriteAsyncbefore10241024Asynchronous501.52 us1.00-4,936 B
CopyToFileAsyncafter1024?None513.31 us1.01-5,593 B
CopyToFileAsyncbefore1024?None510.34 us1.00-5,593 B
CopyToFileAsyncafter1024?Asynchronous539.68 us1.00-6,336 B
CopyToFileAsyncbefore1024?Asynchronous541.15 us1.00-6,320 B
ReadAsyncafter1048576512Asynchronous2,436.66 us0.98-58,279 B
ReadAsyncbefore1048576512Asynchronous2,480.83 us1.008.928684,775 B
WriteAsyncafter1048576512Asynchronous4,142.67 us0.92-50,029 B
WriteAsyncbefore1048576512Asynchronous4,511.11 us1.00-76,527 B
ReadAsyncafter10485764096Asynchronous2,153.31 us0.90-916 B
ReadAsyncbefore10485764096Asynchronous2,397.80 us1.008.928670,241 B
WriteAsyncafter10485764096Asynchronous3,976.06 us0.97-27,596 B
WriteAsyncbefore10485764096Asynchronous4,100.25 us1.00-75,562 B
ReadAsync_NoBufferingafter104857616384Asynchronous689.04 us0.94-760 B
ReadAsync_NoBufferingbefore104857616384Asynchronous733.21 us1.00-17,864 B
WriteAsync_NoBufferingafter104857616384Asynchronous2,781.44 us0.99-762 B
WriteAsync_NoBufferingbefore104857616384Asynchronous2,808.65 us1.00-17,866 B
CopyToFileAsyncafter1048576?None2,360.16 us0.98-3,244 B
CopyToFileAsyncbefore1048576?None2,405.43 us1.00-3,244 B
CopyToFileAsyncafter1048576?Asynchronous2,593.22 us1.01-2,044 B
CopyToFileAsyncbefore1048576?Asynchronous2,568.19 us1.00-3,924 B
ReadAsyncafter1048576004096Asynchronous247,853.42 us0.95-1,368 B
ReadAsyncbefore1048576004096Asynchronous259,775.67 us1.00-6,963,952 B
WriteAsyncafter1048576004096Asynchronous358,502.58 us0.97-2,258,912 B
WriteAsyncbefore1048576004096Asynchronous371,005.92 us1.00-7,070,648 B
ReadAsync_NoBufferingafter10485760016384Asynchronous76,073.40 us0.94-796 B
ReadAsync_NoBufferingbefore10485760016384Asynchronous81,316.51 us1.00-1,741,292 B
WriteAsync_NoBufferingafter10485760016384Asynchronous114,458.43 us0.88-832 B
WriteAsync_NoBufferingbefore10485760016384Asynchronous130,608.83 us1.00-1,741,328 B
CopyToFileAsyncafter104857600?None74,758.01 us1.01-180,828 B
CopyToFileAsyncbefore104857600?None73,939.22 us1.00-180,828 B
CopyToFileAsyncafter104857600?Asynchronous86,110.81 us0.98-2,220 B
CopyToFileAsyncbefore104857600?Asynchronous87,546.69 us1.00-219,524 B

@adamsitnik
adamsitnik marked this pull request as ready for review April 16, 2021 16:30
@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub I believe I have addressed all your feedback, PTAL one more time. I hope that we can merge it today and include it in Preview 4

@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub we have addressed the feedback, please take a look. I am going to post the benchmark results in 20-30 minutes

@stephentoub

Copy link
Copy Markdown
Member

This now also fixes #25074

@adamsitnik

Copy link
Copy Markdown
Member

The results (see the Allocated column)

MethodJobfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsyncafter10241024Asynchronous84.39 us0.985,240 B
ReadAsyncbefore10241024Asynchronous85.86 us1.005,216 B
WriteAsyncafter10241024Asynchronous483.68 us1.014,960 B
WriteAsyncbefore10241024Asynchronous478.92 us1.004,936 B
CopyToFileAsyncafter1024?None492.53 us1.015,593 B
CopyToFileAsyncbefore1024?None489.18 us1.005,593 B
CopyToFileAsyncafter1024?Asynchronous529.35 us1.026,336 B
CopyToFileAsyncbefore1024?Asynchronous521.85 us1.006,311 B
ReadAsyncafter1048576512Asynchronous2,371.68 us1.0058,279 B
ReadAsyncbefore1048576512Asynchronous2,372.78 us1.0084,775 B
WriteAsyncafter1048576512Asynchronous4,081.33 us0.9750,028 B
WriteAsyncbefore1048576512Asynchronous4,214.84 us1.0076,517 B
ReadAsyncafter10485764096Asynchronous2,138.36 us0.92913 B
ReadAsyncbefore10485764096Asynchronous2,332.66 us1.0070,241 B
WriteAsyncafter10485764096Asynchronous3,951.54 us0.9527,562 B
WriteAsyncbefore10485764096Asynchronous4,153.60 us1.0075,562 B
ReadAsync_NoBufferingafter104857616384Asynchronous674.58 us0.91760 B
ReadAsync_NoBufferingbefore104857616384Asynchronous740.07 us1.0017,864 B
WriteAsync_NoBufferingafter104857616384Asynchronous2,711.34 us0.99762 B
WriteAsync_NoBufferingbefore104857616384Asynchronous2,736.59 us1.0017,866 B
CopyToFileAsyncafter1048576?None1,961.68 us0.953,243 B
CopyToFileAsyncbefore1048576?None2,080.32 us1.003,244 B
CopyToFileAsyncafter1048576?Asynchronous2,283.20 us1.082,044 B
CopyToFileAsyncbefore1048576?Asynchronous2,161.78 us1.003,924 B
ReadAsyncafter1048576004096Asynchronous228,777.04 us0.931,056 B
ReadAsyncbefore1048576004096Asynchronous245,615.52 us1.006,963,952 B
WriteAsyncafter1048576004096Asynchronous353,822.32 us0.962,257,976 B
WriteAsyncbefore1048576004096Asynchronous370,913.51 us1.007,070,648 B
ReadAsync_NoBufferingafter10485760016384Asynchronous74,983.01 us0.95796 B
ReadAsync_NoBufferingbefore10485760016384Asynchronous79,064.04 us1.001,741,292 B
WriteAsync_NoBufferingafter10485760016384Asynchronous116,660.24 us0.95832 B
WriteAsync_NoBufferingbefore10485760016384Asynchronous123,323.67 us1.001,741,328 B
CopyToFileAsyncafter104857600?None73,465.73 us0.99180,828 B
CopyToFileAsyncbefore104857600?None74,040.22 us1.00180,828 B
CopyToFileAsyncafter104857600?Asynchronous85,207.68 us0.982,220 B
CopyToFileAsyncbefore104857600?Asynchronous87,037.81 us1.00219,524 B

@jeffhandley

Copy link
Copy Markdown
Member

Wowza some of those allocation improvements are incredible!

@Anipik
Anipik merged commit 7878130 into dotnet:mainApr 16, 2021
@carlossanlop
carlossanlop deleted the Caching branch April 16, 2021 23:07
@danmoseley

Copy link
Copy Markdown
Contributor

Nice.

{
_strategy.OnBufferAllocated(_buffer = new byte[_bufferSize]);
_buffer = GC.AllocateUninitializedArray<byte>(_bufferSize,
pinned: true); // this allows us to avoid pinning when the buffer is used for the syscalls

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That correct, but pinned: true also allocates the array in Gen2 as side-effect so this may actually hurt real-world scenarios at the end..

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can allocate it and use a GCHandle if that ends up being better. Previously it was pinned as part of a PreallocatedOverlapped.

(It's still not at all obvious when this newfangled POH should be used. )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can also investigate not pinning at all, in which case the code this interacts with will just create a gchandle for each operation.

And/or look at using a pool array, but we'd want to ensure enough synchronization was in place to minimize erroneous usage causing us to return an array still in use. We do that in a few other streams.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Here is a simple test:

using System;
using System.IO;
for (int i = 0; i < 100_000; i++)
{
using (var f = new FileStream("test", FileMode.Create))
{
f.WriteByte(42);
}
}
Console.WriteLine($"Allocated: {GC.GetTotalAllocatedBytes()} Gen2 GCs: {GC.CollectionCount(2)}");
  • .NET 5: Allocated: 442474096 Gen2 GCs: 0
  • This PR: Allocated: 448051624 Gen2 GCs: 103

It will be interesting to see whether these excessive Gen2 GCs hit performance gates of services trying .NET 6 previews.

It's still not at all obvious when this newfangled POH should be used.

Agree. It is very hard to use.

we'd want to ensure enough synchronization was in place to minimize erroneous usage causing us to return an array still in use

If you can cover all these cases, it may be better to use unmanaged buffer. It is pinned too, and it does not cause excessive Gen2 GCs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So allocating on the POH contributes to the gen2 budget. This is why we disable the buffer using size 1, that still works right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nothing in this file is used at all if buffer size is 1.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm looking forward to taking another stab at optimizing static files in .NET 6

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you can cover all these cases, it may be better to use unmanaged buffer. It is pinned too, and it does not cause excessive Gen2 GCs.

I'm going to start with a GCHandle and a normally allocated array. I believe in that case I can mostly restrict synchronization to the async code paths (plus disposal). If we use a native buffer, we'll need to protect the sync code paths as well. We can start with this and then see if it makes sense to use a pooled or native buffer as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Well, actually, I'm going to start by not pinning here at all (it'll then pin/unpin in the rest of the implementation per operation). If there's no measurable impact, we can stick with that for now.

@adamsitnikadamsitnik mentioned this pull request Apr 17, 2021
5 tasks
@ghostghost locked as resolved and limited conversation to collaborators May 17, 2021
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cache IValueTaskSource in AsyncWindowsFileStreamStrategy Use PreallocatedOverlapped when internal FileStream buffer isn't being used on Windows

9 participants

@carlossanlop@adamsitnik@stephentoub@jeffhandley@danmoseley@davidfowl@jkotas@jozkee@Anipik
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

FileStream rewrite: Caching the ValueTaskSource in AsyncWindowsFileStreamStrategy - #51363

Merged
Anipik merged 15 commits into
dotnet:mainfrom
carlossanlop:Caching
Apr 16, 2021
Merged

FileStream rewrite: Caching the ValueTaskSource in AsyncWindowsFileStreamStrategy#51363
Anipik merged 15 commits into
dotnet:mainfrom
carlossanlop:Caching

Conversation

@carlossanlop

@carlossanlopcarlossanlop commented Apr 16, 2021

Copy link
Copy Markdown
Contributor

Fixes#50972
Fixes#25074

When AsyncWindowsFileStreamStrategy is wrapped by a BufferedFileStreamStrategy, we need to make sure the ValueTaskSource instance is cached to reduce the number of allocations when calling ReadAsync or WriteAsync multiple times in a row.

This PR is a continuation of #50802, in which we switched from using TaskCompletionSource to IValueTaskSource.

Changes:

  • Moved the PreAllocatedOverlapped instance inside ValueTaskSource, so the latter becomes its owner. This was done because we are only supposed to have an instance of a PreAllocatedOverlapped if the ValueTaskSource was created from OnBufferedAllocated, which is a method called only by BufferedFileStreamStrategy right before writing or reading.
  • Removed MemoryValueTaskSource and moved the cases handled by it to ValueTaskSource.
  • Created a method that refreshes the value of the NativeOverlapped*. This is done every time we call ReadAsync/WriteAsync, to make sure we are pinning the memory passed by the user.

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @carlossanlop
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #50972

DAFT: Pending benchmarks.

When AsyncWindowsFileStreamStrategy is wrapped by a BufferedFileStreamStrategy, we need to make sure the ValueTaskSource instance is cached to reduce the number of allocations when calling ReadAsync or WriteAsync multiple times in a row.

This PR is a continuation of #50802, in which we switched from using TaskCompletionSource to IValueTaskSource.

Changes:

  • Moved the PreAllocatedOverlapped instance inside ValueTaskSource, so the latter becomes its owner. This was done because we are only supposed to have an instance of a PreAllocatedOverlapped if the ValueTaskSource was created from OnBufferedAllocated, which is a method called only by BufferedFileStreamStrategy right before writing or reading.
  • Removed MemoryValueTaskSource and moved the cases handled by it to ValueTaskSource.
  • Created a method that refreshes the value of the NativeOverlapped*. This is done every time we call ReadAsync/WriteAsync, to make sure we are pinning the memory passed by the user.
Author:carlossanlop
Assignees:carlossanlop
Labels:

area-System.IO

Milestone:6.0.0

@adamsitnik

Copy link
Copy Markdown
Member

Initial benchmark results (base is #50802):

Details
MethodToolchainfileSizeuserBufferSizeoptionsMeanRatioGen 0Allocated
ReadAsync\cache\corerun.exe10241024Asynchronous83.99 us1.020.33605 KB
ReadAsync\base\corerun.exe10241024Asynchronous81.98 us1.000.65455 KB
WriteAsync\cache\corerun.exe10241024Asynchronous487.70 us1.02-5 KB
WriteAsync\true\corerun.exe10241024Asynchronous476.89 us1.00-5 KB
ReadAsync\cache\corerun.exe1048576512Asynchronous2,649.16 us1.0910.4167127 KB
ReadAsync\true\corerun.exe1048576512Asynchronous2,421.94 us1.008.928683 KB
WriteAsync\cache\corerun.exe1048576512Asynchronous4,527.33 us1.1115.6250119 KB
WriteAsync\true\corerun.exe1048576512Asynchronous4,100.58 us1.00-75 KB
ReadAsync\cache\corerun.exe10485764096Asynchronous2,321.45 us1.018.928671 KB
ReadAsync\true\corerun.exe10485764096Asynchronous2,308.26 us1.008.928669 KB
WriteAsync\cache\corerun.exe10485764096Asynchronous4,191.59 us1.03-97 KB
WriteAsync\true\corerun.exe10485764096Asynchronous4,077.05 us1.00-74 KB
ReadAsync_NoBuffering\cache\corerun.exe104857616384Asynchronous730.67 us1.00-18 KB
ReadAsync_NoBuffering\true\corerun.exe104857616384Asynchronous732.58 us1.00-17 KB
WriteAsync_NoBuffering\cache\corerun.exe104857616384Asynchronous2,747.11 us0.99-18 KB
WriteAsync_NoBuffering\true\corerun.exe104857616384Asynchronous2,787.66 us1.00-17 KB
ReadAsync\cache\corerun.exe1048576004096Asynchronous250,998.78 us1.01-7,001 KB
ReadAsync\true\corerun.exe1048576004096Asynchronous249,280.08 us1.00-6,801 KB
WriteAsync\cache\corerun.exe1048576004096Asynchronous382,734.73 us1.041000.00009,205 KB
WriteAsync\true\corerun.exe1048576004096Asynchronous368,403.91 us1.00-6,905 KB
ReadAsync_NoBuffering\cache\corerun.exe10485760016384Asynchronous80,072.69 us0.97-1,750 KB
ReadAsync_NoBuffering\true\corerun.exe10485760016384Asynchronous82,376.92 us1.00-1,700 KB
WriteAsync_NoBuffering\cache\corerun.exe10485760016384Asynchronous120,385.98 us0.98-1,751 KB
WriteAsync_NoBuffering\true\corerun.exe10485760016384Asynchronous123,215.33 us1.00-1,701 KB

it looks like we are allocating less when buffering is disabled, but more than before when it's enabled

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall looks good to me, but we need to track and solve the allocation regression which is visible in the scenarios where buffering is enabled

…to the moment after _source.SetException|SetResult is called
@stephentoub

Copy link
Copy Markdown
Member

The interlocked operation that was trying to enable the instance to be reused was backwards... nothing was being pooled. The numbers will need to be re-run after that's fixed ;-)

@adamsitnik

Copy link
Copy Markdown
Member

The interlocked operation that was trying to enable the instance to be reused was backwards... nothing was being pooled.

You are right, that was the reason for the regression. The latest numbers show that allocations have been reduced:

MethodJobfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsyncafter10241024Asynchronous82.48 us1.005 KB
ReadAsyncbefore10241024Asynchronous82.12 us1.005 KB
WriteAsyncafter10241024Asynchronous500.23 us1.005 KB
WriteAsyncbefore10241024Asynchronous498.59 us1.005 KB
CopyToFileAsyncafter1024?None520.65 us1.005 KB
CopyToFileAsyncbefore1024?None522.37 us1.005 KB
CopyToFileAsyncafter1024?Asynchronous548.74 us1.006 KB
CopyToFileAsyncbefore1024?Asynchronous548.45 us1.006 KB
ReadAsyncafter1048576512Asynchronous2,354.25 us0.9857 KB
ReadAsyncbefore1048576512Asynchronous2,396.26 us1.0083 KB
WriteAsyncafter1048576512Asynchronous4,100.29 us0.9849 KB
WriteAsyncbefore1048576512Asynchronous4,183.45 us1.0075 KB
ReadAsyncafter10485764096Asynchronous2,355.74 us1.0137 KB
ReadAsyncbefore10485764096Asynchronous2,341.08 us1.0069 KB
WriteAsyncafter10485764096Asynchronous4,045.21 us0.9945 KB
WriteAsyncbefore10485764096Asynchronous4,073.02 us1.0074 KB
ReadAsync_NoBufferingafter104857616384Asynchronous729.91 us1.0210 KB
ReadAsync_NoBufferingbefore104857616384Asynchronous717.17 us1.0017 KB
WriteAsync_NoBufferingafter104857616384Asynchronous2,820.46 us1.0110 KB
WriteAsync_NoBufferingbefore104857616384Asynchronous2,793.28 us1.0017 KB
CopyToFileAsyncafter1048576?None2,338.90 us1.003 KB
CopyToFileAsyncbefore1048576?None2,343.55 us1.003 KB
CopyToFileAsyncafter1048576?Asynchronous2,574.37 us1.013 KB
CopyToFileAsyncbefore1048576?Asynchronous2,564.43 us1.004 KB
ReadAsyncafter1048576004096Asynchronous248,390.61 us0.983,601 KB
ReadAsyncbefore1048576004096Asynchronous252,787.95 us1.006,801 KB
WriteAsyncafter1048576004096Asynchronous357,904.15 us0.994,005 KB
WriteAsyncbefore1048576004096Asynchronous360,081.89 us1.006,905 KB
ReadAsync_NoBufferingafter10485760016384Asynchronous80,097.02 us0.99901 KB
ReadAsync_NoBufferingbefore10485760016384Asynchronous81,006.52 us1.001,701 KB
WriteAsync_NoBufferingafter10485760016384Asynchronous120,078.07 us0.97901 KB
WriteAsync_NoBufferingbefore10485760016384Asynchronous123,329.13 us1.001,701 KB
CopyToFileAsyncafter104857600?None73,213.21 us0.99177 KB
CopyToFileAsyncbefore104857600?None73,740.59 us1.00177 KB
CopyToFileAsyncafter104857600?Asynchronous87,047.37 us1.00114 KB
CopyToFileAsyncbefore104857600?Asynchronous87,193.75 us1.00214 KB

@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub your suggestions were great!

MethodJobfileSizeuserBufferSizeoptionsMeanRatioGen 0Allocated
ReadAsyncafter10241024Asynchronous87.48 us1.040.36135,240 B
ReadAsyncbefore10241024Asynchronous84.38 us1.000.34725,216 B
WriteAsyncafter10241024Asynchronous494.43 us0.99-4,960 B
WriteAsyncbefore10241024Asynchronous501.52 us1.00-4,936 B
CopyToFileAsyncafter1024?None513.31 us1.01-5,593 B
CopyToFileAsyncbefore1024?None510.34 us1.00-5,593 B
CopyToFileAsyncafter1024?Asynchronous539.68 us1.00-6,336 B
CopyToFileAsyncbefore1024?Asynchronous541.15 us1.00-6,320 B
ReadAsyncafter1048576512Asynchronous2,436.66 us0.98-58,279 B
ReadAsyncbefore1048576512Asynchronous2,480.83 us1.008.928684,775 B
WriteAsyncafter1048576512Asynchronous4,142.67 us0.92-50,029 B
WriteAsyncbefore1048576512Asynchronous4,511.11 us1.00-76,527 B
ReadAsyncafter10485764096Asynchronous2,153.31 us0.90-916 B
ReadAsyncbefore10485764096Asynchronous2,397.80 us1.008.928670,241 B
WriteAsyncafter10485764096Asynchronous3,976.06 us0.97-27,596 B
WriteAsyncbefore10485764096Asynchronous4,100.25 us1.00-75,562 B
ReadAsync_NoBufferingafter104857616384Asynchronous689.04 us0.94-760 B
ReadAsync_NoBufferingbefore104857616384Asynchronous733.21 us1.00-17,864 B
WriteAsync_NoBufferingafter104857616384Asynchronous2,781.44 us0.99-762 B
WriteAsync_NoBufferingbefore104857616384Asynchronous2,808.65 us1.00-17,866 B
CopyToFileAsyncafter1048576?None2,360.16 us0.98-3,244 B
CopyToFileAsyncbefore1048576?None2,405.43 us1.00-3,244 B
CopyToFileAsyncafter1048576?Asynchronous2,593.22 us1.01-2,044 B
CopyToFileAsyncbefore1048576?Asynchronous2,568.19 us1.00-3,924 B
ReadAsyncafter1048576004096Asynchronous247,853.42 us0.95-1,368 B
ReadAsyncbefore1048576004096Asynchronous259,775.67 us1.00-6,963,952 B
WriteAsyncafter1048576004096Asynchronous358,502.58 us0.97-2,258,912 B
WriteAsyncbefore1048576004096Asynchronous371,005.92 us1.00-7,070,648 B
ReadAsync_NoBufferingafter10485760016384Asynchronous76,073.40 us0.94-796 B
ReadAsync_NoBufferingbefore10485760016384Asynchronous81,316.51 us1.00-1,741,292 B
WriteAsync_NoBufferingafter10485760016384Asynchronous114,458.43 us0.88-832 B
WriteAsync_NoBufferingbefore10485760016384Asynchronous130,608.83 us1.00-1,741,328 B
CopyToFileAsyncafter104857600?None74,758.01 us1.01-180,828 B
CopyToFileAsyncbefore104857600?None73,939.22 us1.00-180,828 B
CopyToFileAsyncafter104857600?Asynchronous86,110.81 us0.98-2,220 B
CopyToFileAsyncbefore104857600?Asynchronous87,546.69 us1.00-219,524 B

@adamsitnik
adamsitnik marked this pull request as ready for review April 16, 2021 16:30
@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub I believe I have addressed all your feedback, PTAL one more time. I hope that we can merge it today and include it in Preview 4

@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub we have addressed the feedback, please take a look. I am going to post the benchmark results in 20-30 minutes

@stephentoub

Copy link
Copy Markdown
Member

This now also fixes #25074

@adamsitnik

Copy link
Copy Markdown
Member

The results (see the Allocated column)

MethodJobfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsyncafter10241024Asynchronous84.39 us0.985,240 B
ReadAsyncbefore10241024Asynchronous85.86 us1.005,216 B
WriteAsyncafter10241024Asynchronous483.68 us1.014,960 B
WriteAsyncbefore10241024Asynchronous478.92 us1.004,936 B
CopyToFileAsyncafter1024?None492.53 us1.015,593 B
CopyToFileAsyncbefore1024?None489.18 us1.005,593 B
CopyToFileAsyncafter1024?Asynchronous529.35 us1.026,336 B
CopyToFileAsyncbefore1024?Asynchronous521.85 us1.006,311 B
ReadAsyncafter1048576512Asynchronous2,371.68 us1.0058,279 B
ReadAsyncbefore1048576512Asynchronous2,372.78 us1.0084,775 B
WriteAsyncafter1048576512Asynchronous4,081.33 us0.9750,028 B
WriteAsyncbefore1048576512Asynchronous4,214.84 us1.0076,517 B
ReadAsyncafter10485764096Asynchronous2,138.36 us0.92913 B
ReadAsyncbefore10485764096Asynchronous2,332.66 us1.0070,241 B
WriteAsyncafter10485764096Asynchronous3,951.54 us0.9527,562 B
WriteAsyncbefore10485764096Asynchronous4,153.60 us1.0075,562 B
ReadAsync_NoBufferingafter104857616384Asynchronous674.58 us0.91760 B
ReadAsync_NoBufferingbefore104857616384Asynchronous740.07 us1.0017,864 B
WriteAsync_NoBufferingafter104857616384Asynchronous2,711.34 us0.99762 B
WriteAsync_NoBufferingbefore104857616384Asynchronous2,736.59 us1.0017,866 B
CopyToFileAsyncafter1048576?None1,961.68 us0.953,243 B
CopyToFileAsyncbefore1048576?None2,080.32 us1.003,244 B
CopyToFileAsyncafter1048576?Asynchronous2,283.20 us1.082,044 B
CopyToFileAsyncbefore1048576?Asynchronous2,161.78 us1.003,924 B
ReadAsyncafter1048576004096Asynchronous228,777.04 us0.931,056 B
ReadAsyncbefore1048576004096Asynchronous245,615.52 us1.006,963,952 B
WriteAsyncafter1048576004096Asynchronous353,822.32 us0.962,257,976 B
WriteAsyncbefore1048576004096Asynchronous370,913.51 us1.007,070,648 B
ReadAsync_NoBufferingafter10485760016384Asynchronous74,983.01 us0.95796 B
ReadAsync_NoBufferingbefore10485760016384Asynchronous79,064.04 us1.001,741,292 B
WriteAsync_NoBufferingafter10485760016384Asynchronous116,660.24 us0.95832 B
WriteAsync_NoBufferingbefore10485760016384Asynchronous123,323.67 us1.001,741,328 B
CopyToFileAsyncafter104857600?None73,465.73 us0.99180,828 B
CopyToFileAsyncbefore104857600?None74,040.22 us1.00180,828 B
CopyToFileAsyncafter104857600?Asynchronous85,207.68 us0.982,220 B
CopyToFileAsyncbefore104857600?Asynchronous87,037.81 us1.00219,524 B

@jeffhandley

Copy link
Copy Markdown
Member

Wowza some of those allocation improvements are incredible!

@Anipik
Anipik merged commit 7878130 into dotnet:mainApr 16, 2021
@carlossanlop
carlossanlop deleted the Caching branch April 16, 2021 23:07
@danmoseley

Copy link
Copy Markdown
Contributor

Nice.

{
_strategy.OnBufferAllocated(_buffer = new byte[_bufferSize]);
_buffer = GC.AllocateUninitializedArray<byte>(_bufferSize,
pinned: true); // this allows us to avoid pinning when the buffer is used for the syscalls

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That correct, but pinned: true also allocates the array in Gen2 as side-effect so this may actually hurt real-world scenarios at the end..

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can allocate it and use a GCHandle if that ends up being better. Previously it was pinned as part of a PreallocatedOverlapped.

(It's still not at all obvious when this newfangled POH should be used. )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can also investigate not pinning at all, in which case the code this interacts with will just create a gchandle for each operation.

And/or look at using a pool array, but we'd want to ensure enough synchronization was in place to minimize erroneous usage causing us to return an array still in use. We do that in a few other streams.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Here is a simple test:

using System;
using System.IO;
for (int i = 0; i < 100_000; i++)
{
using (var f = new FileStream("test", FileMode.Create))
{
f.WriteByte(42);
}
}
Console.WriteLine($"Allocated: {GC.GetTotalAllocatedBytes()} Gen2 GCs: {GC.CollectionCount(2)}");
  • .NET 5: Allocated: 442474096 Gen2 GCs: 0
  • This PR: Allocated: 448051624 Gen2 GCs: 103

It will be interesting to see whether these excessive Gen2 GCs hit performance gates of services trying .NET 6 previews.

It's still not at all obvious when this newfangled POH should be used.

Agree. It is very hard to use.

we'd want to ensure enough synchronization was in place to minimize erroneous usage causing us to return an array still in use

If you can cover all these cases, it may be better to use unmanaged buffer. It is pinned too, and it does not cause excessive Gen2 GCs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So allocating on the POH contributes to the gen2 budget. This is why we disable the buffer using size 1, that still works right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nothing in this file is used at all if buffer size is 1.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm looking forward to taking another stab at optimizing static files in .NET 6

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you can cover all these cases, it may be better to use unmanaged buffer. It is pinned too, and it does not cause excessive Gen2 GCs.

I'm going to start with a GCHandle and a normally allocated array. I believe in that case I can mostly restrict synchronization to the async code paths (plus disposal). If we use a native buffer, we'll need to protect the sync code paths as well. We can start with this and then see if it makes sense to use a pooled or native buffer as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Well, actually, I'm going to start by not pinning here at all (it'll then pin/unpin in the rest of the implementation per operation). If there's no measurable impact, we can stick with that for now.

@adamsitnikadamsitnik mentioned this pull request Apr 17, 2021
5 tasks
@ghostghost locked as resolved and limited conversation to collaborators May 17, 2021
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cache IValueTaskSource in AsyncWindowsFileStreamStrategy Use PreallocatedOverlapped when internal FileStream buffer isn't being used on Windows

9 participants

@carlossanlop@adamsitnik@stephentoub@jeffhandley@danmoseley@davidfowl@jkotas@jozkee@Anipik
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

FileStream rewrite: Caching the ValueTaskSource in AsyncWindowsFileStreamStrategy - #51363

Merged
Anipik merged 15 commits into
dotnet:mainfrom
carlossanlop:Caching
Apr 16, 2021
Merged

FileStream rewrite: Caching the ValueTaskSource in AsyncWindowsFileStreamStrategy#51363
Anipik merged 15 commits into
dotnet:mainfrom
carlossanlop:Caching

Conversation

@carlossanlop

@carlossanlopcarlossanlop commented Apr 16, 2021

Copy link
Copy Markdown
Contributor

Fixes#50972
Fixes#25074

When AsyncWindowsFileStreamStrategy is wrapped by a BufferedFileStreamStrategy, we need to make sure the ValueTaskSource instance is cached to reduce the number of allocations when calling ReadAsync or WriteAsync multiple times in a row.

This PR is a continuation of #50802, in which we switched from using TaskCompletionSource to IValueTaskSource.

Changes:

  • Moved the PreAllocatedOverlapped instance inside ValueTaskSource, so the latter becomes its owner. This was done because we are only supposed to have an instance of a PreAllocatedOverlapped if the ValueTaskSource was created from OnBufferedAllocated, which is a method called only by BufferedFileStreamStrategy right before writing or reading.
  • Removed MemoryValueTaskSource and moved the cases handled by it to ValueTaskSource.
  • Created a method that refreshes the value of the NativeOverlapped*. This is done every time we call ReadAsync/WriteAsync, to make sure we are pinning the memory passed by the user.

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @carlossanlop
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #50972

DAFT: Pending benchmarks.

When AsyncWindowsFileStreamStrategy is wrapped by a BufferedFileStreamStrategy, we need to make sure the ValueTaskSource instance is cached to reduce the number of allocations when calling ReadAsync or WriteAsync multiple times in a row.

This PR is a continuation of #50802, in which we switched from using TaskCompletionSource to IValueTaskSource.

Changes:

  • Moved the PreAllocatedOverlapped instance inside ValueTaskSource, so the latter becomes its owner. This was done because we are only supposed to have an instance of a PreAllocatedOverlapped if the ValueTaskSource was created from OnBufferedAllocated, which is a method called only by BufferedFileStreamStrategy right before writing or reading.
  • Removed MemoryValueTaskSource and moved the cases handled by it to ValueTaskSource.
  • Created a method that refreshes the value of the NativeOverlapped*. This is done every time we call ReadAsync/WriteAsync, to make sure we are pinning the memory passed by the user.
Author:carlossanlop
Assignees:carlossanlop
Labels:

area-System.IO

Milestone:6.0.0

@adamsitnik

Copy link
Copy Markdown
Member

Initial benchmark results (base is #50802):

Details
MethodToolchainfileSizeuserBufferSizeoptionsMeanRatioGen 0Allocated
ReadAsync\cache\corerun.exe10241024Asynchronous83.99 us1.020.33605 KB
ReadAsync\base\corerun.exe10241024Asynchronous81.98 us1.000.65455 KB
WriteAsync\cache\corerun.exe10241024Asynchronous487.70 us1.02-5 KB
WriteAsync\true\corerun.exe10241024Asynchronous476.89 us1.00-5 KB
ReadAsync\cache\corerun.exe1048576512Asynchronous2,649.16 us1.0910.4167127 KB
ReadAsync\true\corerun.exe1048576512Asynchronous2,421.94 us1.008.928683 KB
WriteAsync\cache\corerun.exe1048576512Asynchronous4,527.33 us1.1115.6250119 KB
WriteAsync\true\corerun.exe1048576512Asynchronous4,100.58 us1.00-75 KB
ReadAsync\cache\corerun.exe10485764096Asynchronous2,321.45 us1.018.928671 KB
ReadAsync\true\corerun.exe10485764096Asynchronous2,308.26 us1.008.928669 KB
WriteAsync\cache\corerun.exe10485764096Asynchronous4,191.59 us1.03-97 KB
WriteAsync\true\corerun.exe10485764096Asynchronous4,077.05 us1.00-74 KB
ReadAsync_NoBuffering\cache\corerun.exe104857616384Asynchronous730.67 us1.00-18 KB
ReadAsync_NoBuffering\true\corerun.exe104857616384Asynchronous732.58 us1.00-17 KB
WriteAsync_NoBuffering\cache\corerun.exe104857616384Asynchronous2,747.11 us0.99-18 KB
WriteAsync_NoBuffering\true\corerun.exe104857616384Asynchronous2,787.66 us1.00-17 KB
ReadAsync\cache\corerun.exe1048576004096Asynchronous250,998.78 us1.01-7,001 KB
ReadAsync\true\corerun.exe1048576004096Asynchronous249,280.08 us1.00-6,801 KB
WriteAsync\cache\corerun.exe1048576004096Asynchronous382,734.73 us1.041000.00009,205 KB
WriteAsync\true\corerun.exe1048576004096Asynchronous368,403.91 us1.00-6,905 KB
ReadAsync_NoBuffering\cache\corerun.exe10485760016384Asynchronous80,072.69 us0.97-1,750 KB
ReadAsync_NoBuffering\true\corerun.exe10485760016384Asynchronous82,376.92 us1.00-1,700 KB
WriteAsync_NoBuffering\cache\corerun.exe10485760016384Asynchronous120,385.98 us0.98-1,751 KB
WriteAsync_NoBuffering\true\corerun.exe10485760016384Asynchronous123,215.33 us1.00-1,701 KB

it looks like we are allocating less when buffering is disabled, but more than before when it's enabled

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall looks good to me, but we need to track and solve the allocation regression which is visible in the scenarios where buffering is enabled

…to the moment after _source.SetException|SetResult is called
@stephentoub

Copy link
Copy Markdown
Member

The interlocked operation that was trying to enable the instance to be reused was backwards... nothing was being pooled. The numbers will need to be re-run after that's fixed ;-)

@adamsitnik

Copy link
Copy Markdown
Member

The interlocked operation that was trying to enable the instance to be reused was backwards... nothing was being pooled.

You are right, that was the reason for the regression. The latest numbers show that allocations have been reduced:

MethodJobfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsyncafter10241024Asynchronous82.48 us1.005 KB
ReadAsyncbefore10241024Asynchronous82.12 us1.005 KB
WriteAsyncafter10241024Asynchronous500.23 us1.005 KB
WriteAsyncbefore10241024Asynchronous498.59 us1.005 KB
CopyToFileAsyncafter1024?None520.65 us1.005 KB
CopyToFileAsyncbefore1024?None522.37 us1.005 KB
CopyToFileAsyncafter1024?Asynchronous548.74 us1.006 KB
CopyToFileAsyncbefore1024?Asynchronous548.45 us1.006 KB
ReadAsyncafter1048576512Asynchronous2,354.25 us0.9857 KB
ReadAsyncbefore1048576512Asynchronous2,396.26 us1.0083 KB
WriteAsyncafter1048576512Asynchronous4,100.29 us0.9849 KB
WriteAsyncbefore1048576512Asynchronous4,183.45 us1.0075 KB
ReadAsyncafter10485764096Asynchronous2,355.74 us1.0137 KB
ReadAsyncbefore10485764096Asynchronous2,341.08 us1.0069 KB
WriteAsyncafter10485764096Asynchronous4,045.21 us0.9945 KB
WriteAsyncbefore10485764096Asynchronous4,073.02 us1.0074 KB
ReadAsync_NoBufferingafter104857616384Asynchronous729.91 us1.0210 KB
ReadAsync_NoBufferingbefore104857616384Asynchronous717.17 us1.0017 KB
WriteAsync_NoBufferingafter104857616384Asynchronous2,820.46 us1.0110 KB
WriteAsync_NoBufferingbefore104857616384Asynchronous2,793.28 us1.0017 KB
CopyToFileAsyncafter1048576?None2,338.90 us1.003 KB
CopyToFileAsyncbefore1048576?None2,343.55 us1.003 KB
CopyToFileAsyncafter1048576?Asynchronous2,574.37 us1.013 KB
CopyToFileAsyncbefore1048576?Asynchronous2,564.43 us1.004 KB
ReadAsyncafter1048576004096Asynchronous248,390.61 us0.983,601 KB
ReadAsyncbefore1048576004096Asynchronous252,787.95 us1.006,801 KB
WriteAsyncafter1048576004096Asynchronous357,904.15 us0.994,005 KB
WriteAsyncbefore1048576004096Asynchronous360,081.89 us1.006,905 KB
ReadAsync_NoBufferingafter10485760016384Asynchronous80,097.02 us0.99901 KB
ReadAsync_NoBufferingbefore10485760016384Asynchronous81,006.52 us1.001,701 KB
WriteAsync_NoBufferingafter10485760016384Asynchronous120,078.07 us0.97901 KB
WriteAsync_NoBufferingbefore10485760016384Asynchronous123,329.13 us1.001,701 KB
CopyToFileAsyncafter104857600?None73,213.21 us0.99177 KB
CopyToFileAsyncbefore104857600?None73,740.59 us1.00177 KB
CopyToFileAsyncafter104857600?Asynchronous87,047.37 us1.00114 KB
CopyToFileAsyncbefore104857600?Asynchronous87,193.75 us1.00214 KB

@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub your suggestions were great!

MethodJobfileSizeuserBufferSizeoptionsMeanRatioGen 0Allocated
ReadAsyncafter10241024Asynchronous87.48 us1.040.36135,240 B
ReadAsyncbefore10241024Asynchronous84.38 us1.000.34725,216 B
WriteAsyncafter10241024Asynchronous494.43 us0.99-4,960 B
WriteAsyncbefore10241024Asynchronous501.52 us1.00-4,936 B
CopyToFileAsyncafter1024?None513.31 us1.01-5,593 B
CopyToFileAsyncbefore1024?None510.34 us1.00-5,593 B
CopyToFileAsyncafter1024?Asynchronous539.68 us1.00-6,336 B
CopyToFileAsyncbefore1024?Asynchronous541.15 us1.00-6,320 B
ReadAsyncafter1048576512Asynchronous2,436.66 us0.98-58,279 B
ReadAsyncbefore1048576512Asynchronous2,480.83 us1.008.928684,775 B
WriteAsyncafter1048576512Asynchronous4,142.67 us0.92-50,029 B
WriteAsyncbefore1048576512Asynchronous4,511.11 us1.00-76,527 B
ReadAsyncafter10485764096Asynchronous2,153.31 us0.90-916 B
ReadAsyncbefore10485764096Asynchronous2,397.80 us1.008.928670,241 B
WriteAsyncafter10485764096Asynchronous3,976.06 us0.97-27,596 B
WriteAsyncbefore10485764096Asynchronous4,100.25 us1.00-75,562 B
ReadAsync_NoBufferingafter104857616384Asynchronous689.04 us0.94-760 B
ReadAsync_NoBufferingbefore104857616384Asynchronous733.21 us1.00-17,864 B
WriteAsync_NoBufferingafter104857616384Asynchronous2,781.44 us0.99-762 B
WriteAsync_NoBufferingbefore104857616384Asynchronous2,808.65 us1.00-17,866 B
CopyToFileAsyncafter1048576?None2,360.16 us0.98-3,244 B
CopyToFileAsyncbefore1048576?None2,405.43 us1.00-3,244 B
CopyToFileAsyncafter1048576?Asynchronous2,593.22 us1.01-2,044 B
CopyToFileAsyncbefore1048576?Asynchronous2,568.19 us1.00-3,924 B
ReadAsyncafter1048576004096Asynchronous247,853.42 us0.95-1,368 B
ReadAsyncbefore1048576004096Asynchronous259,775.67 us1.00-6,963,952 B
WriteAsyncafter1048576004096Asynchronous358,502.58 us0.97-2,258,912 B
WriteAsyncbefore1048576004096Asynchronous371,005.92 us1.00-7,070,648 B
ReadAsync_NoBufferingafter10485760016384Asynchronous76,073.40 us0.94-796 B
ReadAsync_NoBufferingbefore10485760016384Asynchronous81,316.51 us1.00-1,741,292 B
WriteAsync_NoBufferingafter10485760016384Asynchronous114,458.43 us0.88-832 B
WriteAsync_NoBufferingbefore10485760016384Asynchronous130,608.83 us1.00-1,741,328 B
CopyToFileAsyncafter104857600?None74,758.01 us1.01-180,828 B
CopyToFileAsyncbefore104857600?None73,939.22 us1.00-180,828 B
CopyToFileAsyncafter104857600?Asynchronous86,110.81 us0.98-2,220 B
CopyToFileAsyncbefore104857600?Asynchronous87,546.69 us1.00-219,524 B

@adamsitnik
adamsitnik marked this pull request as ready for review April 16, 2021 16:30
@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub I believe I have addressed all your feedback, PTAL one more time. I hope that we can merge it today and include it in Preview 4

@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub we have addressed the feedback, please take a look. I am going to post the benchmark results in 20-30 minutes

@stephentoub

Copy link
Copy Markdown
Member

This now also fixes #25074

@adamsitnik

Copy link
Copy Markdown
Member

The results (see the Allocated column)

MethodJobfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsyncafter10241024Asynchronous84.39 us0.985,240 B
ReadAsyncbefore10241024Asynchronous85.86 us1.005,216 B
WriteAsyncafter10241024Asynchronous483.68 us1.014,960 B
WriteAsyncbefore10241024Asynchronous478.92 us1.004,936 B
CopyToFileAsyncafter1024?None492.53 us1.015,593 B
CopyToFileAsyncbefore1024?None489.18 us1.005,593 B
CopyToFileAsyncafter1024?Asynchronous529.35 us1.026,336 B
CopyToFileAsyncbefore1024?Asynchronous521.85 us1.006,311 B
ReadAsyncafter1048576512Asynchronous2,371.68 us1.0058,279 B
ReadAsyncbefore1048576512Asynchronous2,372.78 us1.0084,775 B
WriteAsyncafter1048576512Asynchronous4,081.33 us0.9750,028 B
WriteAsyncbefore1048576512Asynchronous4,214.84 us1.0076,517 B
ReadAsyncafter10485764096Asynchronous2,138.36 us0.92913 B
ReadAsyncbefore10485764096Asynchronous2,332.66 us1.0070,241 B
WriteAsyncafter10485764096Asynchronous3,951.54 us0.9527,562 B
WriteAsyncbefore10485764096Asynchronous4,153.60 us1.0075,562 B
ReadAsync_NoBufferingafter104857616384Asynchronous674.58 us0.91760 B
ReadAsync_NoBufferingbefore104857616384Asynchronous740.07 us1.0017,864 B
WriteAsync_NoBufferingafter104857616384Asynchronous2,711.34 us0.99762 B
WriteAsync_NoBufferingbefore104857616384Asynchronous2,736.59 us1.0017,866 B
CopyToFileAsyncafter1048576?None1,961.68 us0.953,243 B
CopyToFileAsyncbefore1048576?None2,080.32 us1.003,244 B
CopyToFileAsyncafter1048576?Asynchronous2,283.20 us1.082,044 B
CopyToFileAsyncbefore1048576?Asynchronous2,161.78 us1.003,924 B
ReadAsyncafter1048576004096Asynchronous228,777.04 us0.931,056 B
ReadAsyncbefore1048576004096Asynchronous245,615.52 us1.006,963,952 B
WriteAsyncafter1048576004096Asynchronous353,822.32 us0.962,257,976 B
WriteAsyncbefore1048576004096Asynchronous370,913.51 us1.007,070,648 B
ReadAsync_NoBufferingafter10485760016384Asynchronous74,983.01 us0.95796 B
ReadAsync_NoBufferingbefore10485760016384Asynchronous79,064.04 us1.001,741,292 B
WriteAsync_NoBufferingafter10485760016384Asynchronous116,660.24 us0.95832 B
WriteAsync_NoBufferingbefore10485760016384Asynchronous123,323.67 us1.001,741,328 B
CopyToFileAsyncafter104857600?None73,465.73 us0.99180,828 B
CopyToFileAsyncbefore104857600?None74,040.22 us1.00180,828 B
CopyToFileAsyncafter104857600?Asynchronous85,207.68 us0.982,220 B
CopyToFileAsyncbefore104857600?Asynchronous87,037.81 us1.00219,524 B

@jeffhandley

Copy link
Copy Markdown
Member

Wowza some of those allocation improvements are incredible!

@Anipik
Anipik merged commit 7878130 into dotnet:mainApr 16, 2021
@carlossanlop
carlossanlop deleted the Caching branch April 16, 2021 23:07
@danmoseley

Copy link
Copy Markdown
Contributor

Nice.

{
_strategy.OnBufferAllocated(_buffer = new byte[_bufferSize]);
_buffer = GC.AllocateUninitializedArray<byte>(_bufferSize,
pinned: true); // this allows us to avoid pinning when the buffer is used for the syscalls

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That correct, but pinned: true also allocates the array in Gen2 as side-effect so this may actually hurt real-world scenarios at the end..

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can allocate it and use a GCHandle if that ends up being better. Previously it was pinned as part of a PreallocatedOverlapped.

(It's still not at all obvious when this newfangled POH should be used. )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can also investigate not pinning at all, in which case the code this interacts with will just create a gchandle for each operation.

And/or look at using a pool array, but we'd want to ensure enough synchronization was in place to minimize erroneous usage causing us to return an array still in use. We do that in a few other streams.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Here is a simple test:

using System;
using System.IO;
for (int i = 0; i < 100_000; i++)
{
using (var f = new FileStream("test", FileMode.Create))
{
f.WriteByte(42);
}
}
Console.WriteLine($"Allocated: {GC.GetTotalAllocatedBytes()} Gen2 GCs: {GC.CollectionCount(2)}");
  • .NET 5: Allocated: 442474096 Gen2 GCs: 0
  • This PR: Allocated: 448051624 Gen2 GCs: 103

It will be interesting to see whether these excessive Gen2 GCs hit performance gates of services trying .NET 6 previews.

It's still not at all obvious when this newfangled POH should be used.

Agree. It is very hard to use.

we'd want to ensure enough synchronization was in place to minimize erroneous usage causing us to return an array still in use

If you can cover all these cases, it may be better to use unmanaged buffer. It is pinned too, and it does not cause excessive Gen2 GCs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So allocating on the POH contributes to the gen2 budget. This is why we disable the buffer using size 1, that still works right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nothing in this file is used at all if buffer size is 1.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm looking forward to taking another stab at optimizing static files in .NET 6

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you can cover all these cases, it may be better to use unmanaged buffer. It is pinned too, and it does not cause excessive Gen2 GCs.

I'm going to start with a GCHandle and a normally allocated array. I believe in that case I can mostly restrict synchronization to the async code paths (plus disposal). If we use a native buffer, we'll need to protect the sync code paths as well. We can start with this and then see if it makes sense to use a pooled or native buffer as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Well, actually, I'm going to start by not pinning here at all (it'll then pin/unpin in the rest of the implementation per operation). If there's no measurable impact, we can stick with that for now.

@adamsitnikadamsitnik mentioned this pull request Apr 17, 2021
5 tasks
@ghostghost locked as resolved and limited conversation to collaborators May 17, 2021
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cache IValueTaskSource in AsyncWindowsFileStreamStrategy Use PreallocatedOverlapped when internal FileStream buffer isn't being used on Windows

9 participants

@carlossanlop@adamsitnik@stephentoub@jeffhandley@danmoseley@davidfowl@jkotas@jozkee@Anipik
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

FileStream rewrite: Caching the ValueTaskSource in AsyncWindowsFileStreamStrategy - #51363

Merged
Anipik merged 15 commits into
dotnet:mainfrom
carlossanlop:Caching
Apr 16, 2021
Merged

FileStream rewrite: Caching the ValueTaskSource in AsyncWindowsFileStreamStrategy#51363
Anipik merged 15 commits into
dotnet:mainfrom
carlossanlop:Caching

Conversation

@carlossanlop

@carlossanlopcarlossanlop commented Apr 16, 2021

Copy link
Copy Markdown
Contributor

Fixes#50972
Fixes#25074

When AsyncWindowsFileStreamStrategy is wrapped by a BufferedFileStreamStrategy, we need to make sure the ValueTaskSource instance is cached to reduce the number of allocations when calling ReadAsync or WriteAsync multiple times in a row.

This PR is a continuation of #50802, in which we switched from using TaskCompletionSource to IValueTaskSource.

Changes:

  • Moved the PreAllocatedOverlapped instance inside ValueTaskSource, so the latter becomes its owner. This was done because we are only supposed to have an instance of a PreAllocatedOverlapped if the ValueTaskSource was created from OnBufferedAllocated, which is a method called only by BufferedFileStreamStrategy right before writing or reading.
  • Removed MemoryValueTaskSource and moved the cases handled by it to ValueTaskSource.
  • Created a method that refreshes the value of the NativeOverlapped*. This is done every time we call ReadAsync/WriteAsync, to make sure we are pinning the memory passed by the user.

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @carlossanlop
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #50972

DAFT: Pending benchmarks.

When AsyncWindowsFileStreamStrategy is wrapped by a BufferedFileStreamStrategy, we need to make sure the ValueTaskSource instance is cached to reduce the number of allocations when calling ReadAsync or WriteAsync multiple times in a row.

This PR is a continuation of #50802, in which we switched from using TaskCompletionSource to IValueTaskSource.

Changes:

  • Moved the PreAllocatedOverlapped instance inside ValueTaskSource, so the latter becomes its owner. This was done because we are only supposed to have an instance of a PreAllocatedOverlapped if the ValueTaskSource was created from OnBufferedAllocated, which is a method called only by BufferedFileStreamStrategy right before writing or reading.
  • Removed MemoryValueTaskSource and moved the cases handled by it to ValueTaskSource.
  • Created a method that refreshes the value of the NativeOverlapped*. This is done every time we call ReadAsync/WriteAsync, to make sure we are pinning the memory passed by the user.
Author:carlossanlop
Assignees:carlossanlop
Labels:

area-System.IO

Milestone:6.0.0

@adamsitnik

Copy link
Copy Markdown
Member

Initial benchmark results (base is #50802):

Details
MethodToolchainfileSizeuserBufferSizeoptionsMeanRatioGen 0Allocated
ReadAsync\cache\corerun.exe10241024Asynchronous83.99 us1.020.33605 KB
ReadAsync\base\corerun.exe10241024Asynchronous81.98 us1.000.65455 KB
WriteAsync\cache\corerun.exe10241024Asynchronous487.70 us1.02-5 KB
WriteAsync\true\corerun.exe10241024Asynchronous476.89 us1.00-5 KB
ReadAsync\cache\corerun.exe1048576512Asynchronous2,649.16 us1.0910.4167127 KB
ReadAsync\true\corerun.exe1048576512Asynchronous2,421.94 us1.008.928683 KB
WriteAsync\cache\corerun.exe1048576512Asynchronous4,527.33 us1.1115.6250119 KB
WriteAsync\true\corerun.exe1048576512Asynchronous4,100.58 us1.00-75 KB
ReadAsync\cache\corerun.exe10485764096Asynchronous2,321.45 us1.018.928671 KB
ReadAsync\true\corerun.exe10485764096Asynchronous2,308.26 us1.008.928669 KB
WriteAsync\cache\corerun.exe10485764096Asynchronous4,191.59 us1.03-97 KB
WriteAsync\true\corerun.exe10485764096Asynchronous4,077.05 us1.00-74 KB
ReadAsync_NoBuffering\cache\corerun.exe104857616384Asynchronous730.67 us1.00-18 KB
ReadAsync_NoBuffering\true\corerun.exe104857616384Asynchronous732.58 us1.00-17 KB
WriteAsync_NoBuffering\cache\corerun.exe104857616384Asynchronous2,747.11 us0.99-18 KB
WriteAsync_NoBuffering\true\corerun.exe104857616384Asynchronous2,787.66 us1.00-17 KB
ReadAsync\cache\corerun.exe1048576004096Asynchronous250,998.78 us1.01-7,001 KB
ReadAsync\true\corerun.exe1048576004096Asynchronous249,280.08 us1.00-6,801 KB
WriteAsync\cache\corerun.exe1048576004096Asynchronous382,734.73 us1.041000.00009,205 KB
WriteAsync\true\corerun.exe1048576004096Asynchronous368,403.91 us1.00-6,905 KB
ReadAsync_NoBuffering\cache\corerun.exe10485760016384Asynchronous80,072.69 us0.97-1,750 KB
ReadAsync_NoBuffering\true\corerun.exe10485760016384Asynchronous82,376.92 us1.00-1,700 KB
WriteAsync_NoBuffering\cache\corerun.exe10485760016384Asynchronous120,385.98 us0.98-1,751 KB
WriteAsync_NoBuffering\true\corerun.exe10485760016384Asynchronous123,215.33 us1.00-1,701 KB

it looks like we are allocating less when buffering is disabled, but more than before when it's enabled

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall looks good to me, but we need to track and solve the allocation regression which is visible in the scenarios where buffering is enabled

…to the moment after _source.SetException|SetResult is called
@stephentoub

Copy link
Copy Markdown
Member

The interlocked operation that was trying to enable the instance to be reused was backwards... nothing was being pooled. The numbers will need to be re-run after that's fixed ;-)

@adamsitnik

Copy link
Copy Markdown
Member

The interlocked operation that was trying to enable the instance to be reused was backwards... nothing was being pooled.

You are right, that was the reason for the regression. The latest numbers show that allocations have been reduced:

MethodJobfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsyncafter10241024Asynchronous82.48 us1.005 KB
ReadAsyncbefore10241024Asynchronous82.12 us1.005 KB
WriteAsyncafter10241024Asynchronous500.23 us1.005 KB
WriteAsyncbefore10241024Asynchronous498.59 us1.005 KB
CopyToFileAsyncafter1024?None520.65 us1.005 KB
CopyToFileAsyncbefore1024?None522.37 us1.005 KB
CopyToFileAsyncafter1024?Asynchronous548.74 us1.006 KB
CopyToFileAsyncbefore1024?Asynchronous548.45 us1.006 KB
ReadAsyncafter1048576512Asynchronous2,354.25 us0.9857 KB
ReadAsyncbefore1048576512Asynchronous2,396.26 us1.0083 KB
WriteAsyncafter1048576512Asynchronous4,100.29 us0.9849 KB
WriteAsyncbefore1048576512Asynchronous4,183.45 us1.0075 KB
ReadAsyncafter10485764096Asynchronous2,355.74 us1.0137 KB
ReadAsyncbefore10485764096Asynchronous2,341.08 us1.0069 KB
WriteAsyncafter10485764096Asynchronous4,045.21 us0.9945 KB
WriteAsyncbefore10485764096Asynchronous4,073.02 us1.0074 KB
ReadAsync_NoBufferingafter104857616384Asynchronous729.91 us1.0210 KB
ReadAsync_NoBufferingbefore104857616384Asynchronous717.17 us1.0017 KB
WriteAsync_NoBufferingafter104857616384Asynchronous2,820.46 us1.0110 KB
WriteAsync_NoBufferingbefore104857616384Asynchronous2,793.28 us1.0017 KB
CopyToFileAsyncafter1048576?None2,338.90 us1.003 KB
CopyToFileAsyncbefore1048576?None2,343.55 us1.003 KB
CopyToFileAsyncafter1048576?Asynchronous2,574.37 us1.013 KB
CopyToFileAsyncbefore1048576?Asynchronous2,564.43 us1.004 KB
ReadAsyncafter1048576004096Asynchronous248,390.61 us0.983,601 KB
ReadAsyncbefore1048576004096Asynchronous252,787.95 us1.006,801 KB
WriteAsyncafter1048576004096Asynchronous357,904.15 us0.994,005 KB
WriteAsyncbefore1048576004096Asynchronous360,081.89 us1.006,905 KB
ReadAsync_NoBufferingafter10485760016384Asynchronous80,097.02 us0.99901 KB
ReadAsync_NoBufferingbefore10485760016384Asynchronous81,006.52 us1.001,701 KB
WriteAsync_NoBufferingafter10485760016384Asynchronous120,078.07 us0.97901 KB
WriteAsync_NoBufferingbefore10485760016384Asynchronous123,329.13 us1.001,701 KB
CopyToFileAsyncafter104857600?None73,213.21 us0.99177 KB
CopyToFileAsyncbefore104857600?None73,740.59 us1.00177 KB
CopyToFileAsyncafter104857600?Asynchronous87,047.37 us1.00114 KB
CopyToFileAsyncbefore104857600?Asynchronous87,193.75 us1.00214 KB

@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub your suggestions were great!

MethodJobfileSizeuserBufferSizeoptionsMeanRatioGen 0Allocated
ReadAsyncafter10241024Asynchronous87.48 us1.040.36135,240 B
ReadAsyncbefore10241024Asynchronous84.38 us1.000.34725,216 B
WriteAsyncafter10241024Asynchronous494.43 us0.99-4,960 B
WriteAsyncbefore10241024Asynchronous501.52 us1.00-4,936 B
CopyToFileAsyncafter1024?None513.31 us1.01-5,593 B
CopyToFileAsyncbefore1024?None510.34 us1.00-5,593 B
CopyToFileAsyncafter1024?Asynchronous539.68 us1.00-6,336 B
CopyToFileAsyncbefore1024?Asynchronous541.15 us1.00-6,320 B
ReadAsyncafter1048576512Asynchronous2,436.66 us0.98-58,279 B
ReadAsyncbefore1048576512Asynchronous2,480.83 us1.008.928684,775 B
WriteAsyncafter1048576512Asynchronous4,142.67 us0.92-50,029 B
WriteAsyncbefore1048576512Asynchronous4,511.11 us1.00-76,527 B
ReadAsyncafter10485764096Asynchronous2,153.31 us0.90-916 B
ReadAsyncbefore10485764096Asynchronous2,397.80 us1.008.928670,241 B
WriteAsyncafter10485764096Asynchronous3,976.06 us0.97-27,596 B
WriteAsyncbefore10485764096Asynchronous4,100.25 us1.00-75,562 B
ReadAsync_NoBufferingafter104857616384Asynchronous689.04 us0.94-760 B
ReadAsync_NoBufferingbefore104857616384Asynchronous733.21 us1.00-17,864 B
WriteAsync_NoBufferingafter104857616384Asynchronous2,781.44 us0.99-762 B
WriteAsync_NoBufferingbefore104857616384Asynchronous2,808.65 us1.00-17,866 B
CopyToFileAsyncafter1048576?None2,360.16 us0.98-3,244 B
CopyToFileAsyncbefore1048576?None2,405.43 us1.00-3,244 B
CopyToFileAsyncafter1048576?Asynchronous2,593.22 us1.01-2,044 B
CopyToFileAsyncbefore1048576?Asynchronous2,568.19 us1.00-3,924 B
ReadAsyncafter1048576004096Asynchronous247,853.42 us0.95-1,368 B
ReadAsyncbefore1048576004096Asynchronous259,775.67 us1.00-6,963,952 B
WriteAsyncafter1048576004096Asynchronous358,502.58 us0.97-2,258,912 B
WriteAsyncbefore1048576004096Asynchronous371,005.92 us1.00-7,070,648 B
ReadAsync_NoBufferingafter10485760016384Asynchronous76,073.40 us0.94-796 B
ReadAsync_NoBufferingbefore10485760016384Asynchronous81,316.51 us1.00-1,741,292 B
WriteAsync_NoBufferingafter10485760016384Asynchronous114,458.43 us0.88-832 B
WriteAsync_NoBufferingbefore10485760016384Asynchronous130,608.83 us1.00-1,741,328 B
CopyToFileAsyncafter104857600?None74,758.01 us1.01-180,828 B
CopyToFileAsyncbefore104857600?None73,939.22 us1.00-180,828 B
CopyToFileAsyncafter104857600?Asynchronous86,110.81 us0.98-2,220 B
CopyToFileAsyncbefore104857600?Asynchronous87,546.69 us1.00-219,524 B

@adamsitnik
adamsitnik marked this pull request as ready for review April 16, 2021 16:30
@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub I believe I have addressed all your feedback, PTAL one more time. I hope that we can merge it today and include it in Preview 4

@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub we have addressed the feedback, please take a look. I am going to post the benchmark results in 20-30 minutes

@stephentoub

Copy link
Copy Markdown
Member

This now also fixes #25074

@adamsitnik

Copy link
Copy Markdown
Member

The results (see the Allocated column)

MethodJobfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsyncafter10241024Asynchronous84.39 us0.985,240 B
ReadAsyncbefore10241024Asynchronous85.86 us1.005,216 B
WriteAsyncafter10241024Asynchronous483.68 us1.014,960 B
WriteAsyncbefore10241024Asynchronous478.92 us1.004,936 B
CopyToFileAsyncafter1024?None492.53 us1.015,593 B
CopyToFileAsyncbefore1024?None489.18 us1.005,593 B
CopyToFileAsyncafter1024?Asynchronous529.35 us1.026,336 B
CopyToFileAsyncbefore1024?Asynchronous521.85 us1.006,311 B
ReadAsyncafter1048576512Asynchronous2,371.68 us1.0058,279 B
ReadAsyncbefore1048576512Asynchronous2,372.78 us1.0084,775 B
WriteAsyncafter1048576512Asynchronous4,081.33 us0.9750,028 B
WriteAsyncbefore1048576512Asynchronous4,214.84 us1.0076,517 B
ReadAsyncafter10485764096Asynchronous2,138.36 us0.92913 B
ReadAsyncbefore10485764096Asynchronous2,332.66 us1.0070,241 B
WriteAsyncafter10485764096Asynchronous3,951.54 us0.9527,562 B
WriteAsyncbefore10485764096Asynchronous4,153.60 us1.0075,562 B
ReadAsync_NoBufferingafter104857616384Asynchronous674.58 us0.91760 B
ReadAsync_NoBufferingbefore104857616384Asynchronous740.07 us1.0017,864 B
WriteAsync_NoBufferingafter104857616384Asynchronous2,711.34 us0.99762 B
WriteAsync_NoBufferingbefore104857616384Asynchronous2,736.59 us1.0017,866 B
CopyToFileAsyncafter1048576?None1,961.68 us0.953,243 B
CopyToFileAsyncbefore1048576?None2,080.32 us1.003,244 B
CopyToFileAsyncafter1048576?Asynchronous2,283.20 us1.082,044 B
CopyToFileAsyncbefore1048576?Asynchronous2,161.78 us1.003,924 B
ReadAsyncafter1048576004096Asynchronous228,777.04 us0.931,056 B
ReadAsyncbefore1048576004096Asynchronous245,615.52 us1.006,963,952 B
WriteAsyncafter1048576004096Asynchronous353,822.32 us0.962,257,976 B
WriteAsyncbefore1048576004096Asynchronous370,913.51 us1.007,070,648 B
ReadAsync_NoBufferingafter10485760016384Asynchronous74,983.01 us0.95796 B
ReadAsync_NoBufferingbefore10485760016384Asynchronous79,064.04 us1.001,741,292 B
WriteAsync_NoBufferingafter10485760016384Asynchronous116,660.24 us0.95832 B
WriteAsync_NoBufferingbefore10485760016384Asynchronous123,323.67 us1.001,741,328 B
CopyToFileAsyncafter104857600?None73,465.73 us0.99180,828 B
CopyToFileAsyncbefore104857600?None74,040.22 us1.00180,828 B
CopyToFileAsyncafter104857600?Asynchronous85,207.68 us0.982,220 B
CopyToFileAsyncbefore104857600?Asynchronous87,037.81 us1.00219,524 B

@jeffhandley

Copy link
Copy Markdown
Member

Wowza some of those allocation improvements are incredible!

@Anipik
Anipik merged commit 7878130 into dotnet:mainApr 16, 2021
@carlossanlop
carlossanlop deleted the Caching branch April 16, 2021 23:07
@danmoseley

Copy link
Copy Markdown
Contributor

Nice.

{
_strategy.OnBufferAllocated(_buffer = new byte[_bufferSize]);
_buffer = GC.AllocateUninitializedArray<byte>(_bufferSize,
pinned: true); // this allows us to avoid pinning when the buffer is used for the syscalls

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That correct, but pinned: true also allocates the array in Gen2 as side-effect so this may actually hurt real-world scenarios at the end..

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can allocate it and use a GCHandle if that ends up being better. Previously it was pinned as part of a PreallocatedOverlapped.

(It's still not at all obvious when this newfangled POH should be used. )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can also investigate not pinning at all, in which case the code this interacts with will just create a gchandle for each operation.

And/or look at using a pool array, but we'd want to ensure enough synchronization was in place to minimize erroneous usage causing us to return an array still in use. We do that in a few other streams.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Here is a simple test:

using System;
using System.IO;
for (int i = 0; i < 100_000; i++)
{
using (var f = new FileStream("test", FileMode.Create))
{
f.WriteByte(42);
}
}
Console.WriteLine($"Allocated: {GC.GetTotalAllocatedBytes()} Gen2 GCs: {GC.CollectionCount(2)}");
  • .NET 5: Allocated: 442474096 Gen2 GCs: 0
  • This PR: Allocated: 448051624 Gen2 GCs: 103

It will be interesting to see whether these excessive Gen2 GCs hit performance gates of services trying .NET 6 previews.

It's still not at all obvious when this newfangled POH should be used.

Agree. It is very hard to use.

we'd want to ensure enough synchronization was in place to minimize erroneous usage causing us to return an array still in use

If you can cover all these cases, it may be better to use unmanaged buffer. It is pinned too, and it does not cause excessive Gen2 GCs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So allocating on the POH contributes to the gen2 budget. This is why we disable the buffer using size 1, that still works right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nothing in this file is used at all if buffer size is 1.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm looking forward to taking another stab at optimizing static files in .NET 6

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you can cover all these cases, it may be better to use unmanaged buffer. It is pinned too, and it does not cause excessive Gen2 GCs.

I'm going to start with a GCHandle and a normally allocated array. I believe in that case I can mostly restrict synchronization to the async code paths (plus disposal). If we use a native buffer, we'll need to protect the sync code paths as well. We can start with this and then see if it makes sense to use a pooled or native buffer as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Well, actually, I'm going to start by not pinning here at all (it'll then pin/unpin in the rest of the implementation per operation). If there's no measurable impact, we can stick with that for now.

@adamsitnikadamsitnik mentioned this pull request Apr 17, 2021
5 tasks
@ghostghost locked as resolved and limited conversation to collaborators May 17, 2021
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cache IValueTaskSource in AsyncWindowsFileStreamStrategy Use PreallocatedOverlapped when internal FileStream buffer isn't being used on Windows

9 participants

@carlossanlop@adamsitnik@stephentoub@jeffhandley@danmoseley@davidfowl@jkotas@jozkee@Anipik
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

FileStream rewrite: Caching the ValueTaskSource in AsyncWindowsFileStreamStrategy - #51363

Merged
Anipik merged 15 commits into
dotnet:mainfrom
carlossanlop:Caching
Apr 16, 2021
Merged

FileStream rewrite: Caching the ValueTaskSource in AsyncWindowsFileStreamStrategy#51363
Anipik merged 15 commits into
dotnet:mainfrom
carlossanlop:Caching

Conversation

@carlossanlop

@carlossanlopcarlossanlop commented Apr 16, 2021

Copy link
Copy Markdown
Contributor

Fixes#50972
Fixes#25074

When AsyncWindowsFileStreamStrategy is wrapped by a BufferedFileStreamStrategy, we need to make sure the ValueTaskSource instance is cached to reduce the number of allocations when calling ReadAsync or WriteAsync multiple times in a row.

This PR is a continuation of #50802, in which we switched from using TaskCompletionSource to IValueTaskSource.

Changes:

  • Moved the PreAllocatedOverlapped instance inside ValueTaskSource, so the latter becomes its owner. This was done because we are only supposed to have an instance of a PreAllocatedOverlapped if the ValueTaskSource was created from OnBufferedAllocated, which is a method called only by BufferedFileStreamStrategy right before writing or reading.
  • Removed MemoryValueTaskSource and moved the cases handled by it to ValueTaskSource.
  • Created a method that refreshes the value of the NativeOverlapped*. This is done every time we call ReadAsync/WriteAsync, to make sure we are pinning the memory passed by the user.

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @carlossanlop
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #50972

DAFT: Pending benchmarks.

When AsyncWindowsFileStreamStrategy is wrapped by a BufferedFileStreamStrategy, we need to make sure the ValueTaskSource instance is cached to reduce the number of allocations when calling ReadAsync or WriteAsync multiple times in a row.

This PR is a continuation of #50802, in which we switched from using TaskCompletionSource to IValueTaskSource.

Changes:

  • Moved the PreAllocatedOverlapped instance inside ValueTaskSource, so the latter becomes its owner. This was done because we are only supposed to have an instance of a PreAllocatedOverlapped if the ValueTaskSource was created from OnBufferedAllocated, which is a method called only by BufferedFileStreamStrategy right before writing or reading.
  • Removed MemoryValueTaskSource and moved the cases handled by it to ValueTaskSource.
  • Created a method that refreshes the value of the NativeOverlapped*. This is done every time we call ReadAsync/WriteAsync, to make sure we are pinning the memory passed by the user.
Author:carlossanlop
Assignees:carlossanlop
Labels:

area-System.IO

Milestone:6.0.0

@adamsitnik

Copy link
Copy Markdown
Member

Initial benchmark results (base is #50802):

Details
MethodToolchainfileSizeuserBufferSizeoptionsMeanRatioGen 0Allocated
ReadAsync\cache\corerun.exe10241024Asynchronous83.99 us1.020.33605 KB
ReadAsync\base\corerun.exe10241024Asynchronous81.98 us1.000.65455 KB
WriteAsync\cache\corerun.exe10241024Asynchronous487.70 us1.02-5 KB
WriteAsync\true\corerun.exe10241024Asynchronous476.89 us1.00-5 KB
ReadAsync\cache\corerun.exe1048576512Asynchronous2,649.16 us1.0910.4167127 KB
ReadAsync\true\corerun.exe1048576512Asynchronous2,421.94 us1.008.928683 KB
WriteAsync\cache\corerun.exe1048576512Asynchronous4,527.33 us1.1115.6250119 KB
WriteAsync\true\corerun.exe1048576512Asynchronous4,100.58 us1.00-75 KB
ReadAsync\cache\corerun.exe10485764096Asynchronous2,321.45 us1.018.928671 KB
ReadAsync\true\corerun.exe10485764096Asynchronous2,308.26 us1.008.928669 KB
WriteAsync\cache\corerun.exe10485764096Asynchronous4,191.59 us1.03-97 KB
WriteAsync\true\corerun.exe10485764096Asynchronous4,077.05 us1.00-74 KB
ReadAsync_NoBuffering\cache\corerun.exe104857616384Asynchronous730.67 us1.00-18 KB
ReadAsync_NoBuffering\true\corerun.exe104857616384Asynchronous732.58 us1.00-17 KB
WriteAsync_NoBuffering\cache\corerun.exe104857616384Asynchronous2,747.11 us0.99-18 KB
WriteAsync_NoBuffering\true\corerun.exe104857616384Asynchronous2,787.66 us1.00-17 KB
ReadAsync\cache\corerun.exe1048576004096Asynchronous250,998.78 us1.01-7,001 KB
ReadAsync\true\corerun.exe1048576004096Asynchronous249,280.08 us1.00-6,801 KB
WriteAsync\cache\corerun.exe1048576004096Asynchronous382,734.73 us1.041000.00009,205 KB
WriteAsync\true\corerun.exe1048576004096Asynchronous368,403.91 us1.00-6,905 KB
ReadAsync_NoBuffering\cache\corerun.exe10485760016384Asynchronous80,072.69 us0.97-1,750 KB
ReadAsync_NoBuffering\true\corerun.exe10485760016384Asynchronous82,376.92 us1.00-1,700 KB
WriteAsync_NoBuffering\cache\corerun.exe10485760016384Asynchronous120,385.98 us0.98-1,751 KB
WriteAsync_NoBuffering\true\corerun.exe10485760016384Asynchronous123,215.33 us1.00-1,701 KB

it looks like we are allocating less when buffering is disabled, but more than before when it's enabled

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall looks good to me, but we need to track and solve the allocation regression which is visible in the scenarios where buffering is enabled

…to the moment after _source.SetException|SetResult is called
@stephentoub

Copy link
Copy Markdown
Member

The interlocked operation that was trying to enable the instance to be reused was backwards... nothing was being pooled. The numbers will need to be re-run after that's fixed ;-)

@adamsitnik

Copy link
Copy Markdown
Member

The interlocked operation that was trying to enable the instance to be reused was backwards... nothing was being pooled.

You are right, that was the reason for the regression. The latest numbers show that allocations have been reduced:

MethodJobfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsyncafter10241024Asynchronous82.48 us1.005 KB
ReadAsyncbefore10241024Asynchronous82.12 us1.005 KB
WriteAsyncafter10241024Asynchronous500.23 us1.005 KB
WriteAsyncbefore10241024Asynchronous498.59 us1.005 KB
CopyToFileAsyncafter1024?None520.65 us1.005 KB
CopyToFileAsyncbefore1024?None522.37 us1.005 KB
CopyToFileAsyncafter1024?Asynchronous548.74 us1.006 KB
CopyToFileAsyncbefore1024?Asynchronous548.45 us1.006 KB
ReadAsyncafter1048576512Asynchronous2,354.25 us0.9857 KB
ReadAsyncbefore1048576512Asynchronous2,396.26 us1.0083 KB
WriteAsyncafter1048576512Asynchronous4,100.29 us0.9849 KB
WriteAsyncbefore1048576512Asynchronous4,183.45 us1.0075 KB
ReadAsyncafter10485764096Asynchronous2,355.74 us1.0137 KB
ReadAsyncbefore10485764096Asynchronous2,341.08 us1.0069 KB
WriteAsyncafter10485764096Asynchronous4,045.21 us0.9945 KB
WriteAsyncbefore10485764096Asynchronous4,073.02 us1.0074 KB
ReadAsync_NoBufferingafter104857616384Asynchronous729.91 us1.0210 KB
ReadAsync_NoBufferingbefore104857616384Asynchronous717.17 us1.0017 KB
WriteAsync_NoBufferingafter104857616384Asynchronous2,820.46 us1.0110 KB
WriteAsync_NoBufferingbefore104857616384Asynchronous2,793.28 us1.0017 KB
CopyToFileAsyncafter1048576?None2,338.90 us1.003 KB
CopyToFileAsyncbefore1048576?None2,343.55 us1.003 KB
CopyToFileAsyncafter1048576?Asynchronous2,574.37 us1.013 KB
CopyToFileAsyncbefore1048576?Asynchronous2,564.43 us1.004 KB
ReadAsyncafter1048576004096Asynchronous248,390.61 us0.983,601 KB
ReadAsyncbefore1048576004096Asynchronous252,787.95 us1.006,801 KB
WriteAsyncafter1048576004096Asynchronous357,904.15 us0.994,005 KB
WriteAsyncbefore1048576004096Asynchronous360,081.89 us1.006,905 KB
ReadAsync_NoBufferingafter10485760016384Asynchronous80,097.02 us0.99901 KB
ReadAsync_NoBufferingbefore10485760016384Asynchronous81,006.52 us1.001,701 KB
WriteAsync_NoBufferingafter10485760016384Asynchronous120,078.07 us0.97901 KB
WriteAsync_NoBufferingbefore10485760016384Asynchronous123,329.13 us1.001,701 KB
CopyToFileAsyncafter104857600?None73,213.21 us0.99177 KB
CopyToFileAsyncbefore104857600?None73,740.59 us1.00177 KB
CopyToFileAsyncafter104857600?Asynchronous87,047.37 us1.00114 KB
CopyToFileAsyncbefore104857600?Asynchronous87,193.75 us1.00214 KB

@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub your suggestions were great!

MethodJobfileSizeuserBufferSizeoptionsMeanRatioGen 0Allocated
ReadAsyncafter10241024Asynchronous87.48 us1.040.36135,240 B
ReadAsyncbefore10241024Asynchronous84.38 us1.000.34725,216 B
WriteAsyncafter10241024Asynchronous494.43 us0.99-4,960 B
WriteAsyncbefore10241024Asynchronous501.52 us1.00-4,936 B
CopyToFileAsyncafter1024?None513.31 us1.01-5,593 B
CopyToFileAsyncbefore1024?None510.34 us1.00-5,593 B
CopyToFileAsyncafter1024?Asynchronous539.68 us1.00-6,336 B
CopyToFileAsyncbefore1024?Asynchronous541.15 us1.00-6,320 B
ReadAsyncafter1048576512Asynchronous2,436.66 us0.98-58,279 B
ReadAsyncbefore1048576512Asynchronous2,480.83 us1.008.928684,775 B
WriteAsyncafter1048576512Asynchronous4,142.67 us0.92-50,029 B
WriteAsyncbefore1048576512Asynchronous4,511.11 us1.00-76,527 B
ReadAsyncafter10485764096Asynchronous2,153.31 us0.90-916 B
ReadAsyncbefore10485764096Asynchronous2,397.80 us1.008.928670,241 B
WriteAsyncafter10485764096Asynchronous3,976.06 us0.97-27,596 B
WriteAsyncbefore10485764096Asynchronous4,100.25 us1.00-75,562 B
ReadAsync_NoBufferingafter104857616384Asynchronous689.04 us0.94-760 B
ReadAsync_NoBufferingbefore104857616384Asynchronous733.21 us1.00-17,864 B
WriteAsync_NoBufferingafter104857616384Asynchronous2,781.44 us0.99-762 B
WriteAsync_NoBufferingbefore104857616384Asynchronous2,808.65 us1.00-17,866 B
CopyToFileAsyncafter1048576?None2,360.16 us0.98-3,244 B
CopyToFileAsyncbefore1048576?None2,405.43 us1.00-3,244 B
CopyToFileAsyncafter1048576?Asynchronous2,593.22 us1.01-2,044 B
CopyToFileAsyncbefore1048576?Asynchronous2,568.19 us1.00-3,924 B
ReadAsyncafter1048576004096Asynchronous247,853.42 us0.95-1,368 B
ReadAsyncbefore1048576004096Asynchronous259,775.67 us1.00-6,963,952 B
WriteAsyncafter1048576004096Asynchronous358,502.58 us0.97-2,258,912 B
WriteAsyncbefore1048576004096Asynchronous371,005.92 us1.00-7,070,648 B
ReadAsync_NoBufferingafter10485760016384Asynchronous76,073.40 us0.94-796 B
ReadAsync_NoBufferingbefore10485760016384Asynchronous81,316.51 us1.00-1,741,292 B
WriteAsync_NoBufferingafter10485760016384Asynchronous114,458.43 us0.88-832 B
WriteAsync_NoBufferingbefore10485760016384Asynchronous130,608.83 us1.00-1,741,328 B
CopyToFileAsyncafter104857600?None74,758.01 us1.01-180,828 B
CopyToFileAsyncbefore104857600?None73,939.22 us1.00-180,828 B
CopyToFileAsyncafter104857600?Asynchronous86,110.81 us0.98-2,220 B
CopyToFileAsyncbefore104857600?Asynchronous87,546.69 us1.00-219,524 B

@adamsitnik
adamsitnik marked this pull request as ready for review April 16, 2021 16:30
@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub I believe I have addressed all your feedback, PTAL one more time. I hope that we can merge it today and include it in Preview 4

@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub we have addressed the feedback, please take a look. I am going to post the benchmark results in 20-30 minutes

@stephentoub

Copy link
Copy Markdown
Member

This now also fixes #25074

@adamsitnik

Copy link
Copy Markdown
Member

The results (see the Allocated column)

MethodJobfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsyncafter10241024Asynchronous84.39 us0.985,240 B
ReadAsyncbefore10241024Asynchronous85.86 us1.005,216 B
WriteAsyncafter10241024Asynchronous483.68 us1.014,960 B
WriteAsyncbefore10241024Asynchronous478.92 us1.004,936 B
CopyToFileAsyncafter1024?None492.53 us1.015,593 B
CopyToFileAsyncbefore1024?None489.18 us1.005,593 B
CopyToFileAsyncafter1024?Asynchronous529.35 us1.026,336 B
CopyToFileAsyncbefore1024?Asynchronous521.85 us1.006,311 B
ReadAsyncafter1048576512Asynchronous2,371.68 us1.0058,279 B
ReadAsyncbefore1048576512Asynchronous2,372.78 us1.0084,775 B
WriteAsyncafter1048576512Asynchronous4,081.33 us0.9750,028 B
WriteAsyncbefore1048576512Asynchronous4,214.84 us1.0076,517 B
ReadAsyncafter10485764096Asynchronous2,138.36 us0.92913 B
ReadAsyncbefore10485764096Asynchronous2,332.66 us1.0070,241 B
WriteAsyncafter10485764096Asynchronous3,951.54 us0.9527,562 B
WriteAsyncbefore10485764096Asynchronous4,153.60 us1.0075,562 B
ReadAsync_NoBufferingafter104857616384Asynchronous674.58 us0.91760 B
ReadAsync_NoBufferingbefore104857616384Asynchronous740.07 us1.0017,864 B
WriteAsync_NoBufferingafter104857616384Asynchronous2,711.34 us0.99762 B
WriteAsync_NoBufferingbefore104857616384Asynchronous2,736.59 us1.0017,866 B
CopyToFileAsyncafter1048576?None1,961.68 us0.953,243 B
CopyToFileAsyncbefore1048576?None2,080.32 us1.003,244 B
CopyToFileAsyncafter1048576?Asynchronous2,283.20 us1.082,044 B
CopyToFileAsyncbefore1048576?Asynchronous2,161.78 us1.003,924 B
ReadAsyncafter1048576004096Asynchronous228,777.04 us0.931,056 B
ReadAsyncbefore1048576004096Asynchronous245,615.52 us1.006,963,952 B
WriteAsyncafter1048576004096Asynchronous353,822.32 us0.962,257,976 B
WriteAsyncbefore1048576004096Asynchronous370,913.51 us1.007,070,648 B
ReadAsync_NoBufferingafter10485760016384Asynchronous74,983.01 us0.95796 B
ReadAsync_NoBufferingbefore10485760016384Asynchronous79,064.04 us1.001,741,292 B
WriteAsync_NoBufferingafter10485760016384Asynchronous116,660.24 us0.95832 B
WriteAsync_NoBufferingbefore10485760016384Asynchronous123,323.67 us1.001,741,328 B
CopyToFileAsyncafter104857600?None73,465.73 us0.99180,828 B
CopyToFileAsyncbefore104857600?None74,040.22 us1.00180,828 B
CopyToFileAsyncafter104857600?Asynchronous85,207.68 us0.982,220 B
CopyToFileAsyncbefore104857600?Asynchronous87,037.81 us1.00219,524 B

@jeffhandley

Copy link
Copy Markdown
Member

Wowza some of those allocation improvements are incredible!

@Anipik
Anipik merged commit 7878130 into dotnet:mainApr 16, 2021
@carlossanlop
carlossanlop deleted the Caching branch April 16, 2021 23:07
@danmoseley

Copy link
Copy Markdown
Contributor

Nice.

{
_strategy.OnBufferAllocated(_buffer = new byte[_bufferSize]);
_buffer = GC.AllocateUninitializedArray<byte>(_bufferSize,
pinned: true); // this allows us to avoid pinning when the buffer is used for the syscalls

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That correct, but pinned: true also allocates the array in Gen2 as side-effect so this may actually hurt real-world scenarios at the end..

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can allocate it and use a GCHandle if that ends up being better. Previously it was pinned as part of a PreallocatedOverlapped.

(It's still not at all obvious when this newfangled POH should be used. )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can also investigate not pinning at all, in which case the code this interacts with will just create a gchandle for each operation.

And/or look at using a pool array, but we'd want to ensure enough synchronization was in place to minimize erroneous usage causing us to return an array still in use. We do that in a few other streams.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Here is a simple test:

using System;
using System.IO;
for (int i = 0; i < 100_000; i++)
{
using (var f = new FileStream("test", FileMode.Create))
{
f.WriteByte(42);
}
}
Console.WriteLine($"Allocated: {GC.GetTotalAllocatedBytes()} Gen2 GCs: {GC.CollectionCount(2)}");
  • .NET 5: Allocated: 442474096 Gen2 GCs: 0
  • This PR: Allocated: 448051624 Gen2 GCs: 103

It will be interesting to see whether these excessive Gen2 GCs hit performance gates of services trying .NET 6 previews.

It's still not at all obvious when this newfangled POH should be used.

Agree. It is very hard to use.

we'd want to ensure enough synchronization was in place to minimize erroneous usage causing us to return an array still in use

If you can cover all these cases, it may be better to use unmanaged buffer. It is pinned too, and it does not cause excessive Gen2 GCs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So allocating on the POH contributes to the gen2 budget. This is why we disable the buffer using size 1, that still works right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nothing in this file is used at all if buffer size is 1.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm looking forward to taking another stab at optimizing static files in .NET 6

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you can cover all these cases, it may be better to use unmanaged buffer. It is pinned too, and it does not cause excessive Gen2 GCs.

I'm going to start with a GCHandle and a normally allocated array. I believe in that case I can mostly restrict synchronization to the async code paths (plus disposal). If we use a native buffer, we'll need to protect the sync code paths as well. We can start with this and then see if it makes sense to use a pooled or native buffer as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Well, actually, I'm going to start by not pinning here at all (it'll then pin/unpin in the rest of the implementation per operation). If there's no measurable impact, we can stick with that for now.

@adamsitnikadamsitnik mentioned this pull request Apr 17, 2021
5 tasks
@ghostghost locked as resolved and limited conversation to collaborators May 17, 2021
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cache IValueTaskSource in AsyncWindowsFileStreamStrategy Use PreallocatedOverlapped when internal FileStream buffer isn't being used on Windows

9 participants

@carlossanlop@adamsitnik@stephentoub@jeffhandley@danmoseley@davidfowl@jkotas@jozkee@Anipik
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

FileStream rewrite: Caching the ValueTaskSource in AsyncWindowsFileStreamStrategy - #51363

Merged
Anipik merged 15 commits into
dotnet:mainfrom
carlossanlop:Caching
Apr 16, 2021
Merged

FileStream rewrite: Caching the ValueTaskSource in AsyncWindowsFileStreamStrategy#51363
Anipik merged 15 commits into
dotnet:mainfrom
carlossanlop:Caching

Conversation

@carlossanlop

@carlossanlopcarlossanlop commented Apr 16, 2021

Copy link
Copy Markdown
Contributor

Fixes#50972
Fixes#25074

When AsyncWindowsFileStreamStrategy is wrapped by a BufferedFileStreamStrategy, we need to make sure the ValueTaskSource instance is cached to reduce the number of allocations when calling ReadAsync or WriteAsync multiple times in a row.

This PR is a continuation of #50802, in which we switched from using TaskCompletionSource to IValueTaskSource.

Changes:

  • Moved the PreAllocatedOverlapped instance inside ValueTaskSource, so the latter becomes its owner. This was done because we are only supposed to have an instance of a PreAllocatedOverlapped if the ValueTaskSource was created from OnBufferedAllocated, which is a method called only by BufferedFileStreamStrategy right before writing or reading.
  • Removed MemoryValueTaskSource and moved the cases handled by it to ValueTaskSource.
  • Created a method that refreshes the value of the NativeOverlapped*. This is done every time we call ReadAsync/WriteAsync, to make sure we are pinning the memory passed by the user.

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @carlossanlop
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #50972

DAFT: Pending benchmarks.

When AsyncWindowsFileStreamStrategy is wrapped by a BufferedFileStreamStrategy, we need to make sure the ValueTaskSource instance is cached to reduce the number of allocations when calling ReadAsync or WriteAsync multiple times in a row.

This PR is a continuation of #50802, in which we switched from using TaskCompletionSource to IValueTaskSource.

Changes:

  • Moved the PreAllocatedOverlapped instance inside ValueTaskSource, so the latter becomes its owner. This was done because we are only supposed to have an instance of a PreAllocatedOverlapped if the ValueTaskSource was created from OnBufferedAllocated, which is a method called only by BufferedFileStreamStrategy right before writing or reading.
  • Removed MemoryValueTaskSource and moved the cases handled by it to ValueTaskSource.
  • Created a method that refreshes the value of the NativeOverlapped*. This is done every time we call ReadAsync/WriteAsync, to make sure we are pinning the memory passed by the user.
Author:carlossanlop
Assignees:carlossanlop
Labels:

area-System.IO

Milestone:6.0.0

@adamsitnik

Copy link
Copy Markdown
Member

Initial benchmark results (base is #50802):

Details
MethodToolchainfileSizeuserBufferSizeoptionsMeanRatioGen 0Allocated
ReadAsync\cache\corerun.exe10241024Asynchronous83.99 us1.020.33605 KB
ReadAsync\base\corerun.exe10241024Asynchronous81.98 us1.000.65455 KB
WriteAsync\cache\corerun.exe10241024Asynchronous487.70 us1.02-5 KB
WriteAsync\true\corerun.exe10241024Asynchronous476.89 us1.00-5 KB
ReadAsync\cache\corerun.exe1048576512Asynchronous2,649.16 us1.0910.4167127 KB
ReadAsync\true\corerun.exe1048576512Asynchronous2,421.94 us1.008.928683 KB
WriteAsync\cache\corerun.exe1048576512Asynchronous4,527.33 us1.1115.6250119 KB
WriteAsync\true\corerun.exe1048576512Asynchronous4,100.58 us1.00-75 KB
ReadAsync\cache\corerun.exe10485764096Asynchronous2,321.45 us1.018.928671 KB
ReadAsync\true\corerun.exe10485764096Asynchronous2,308.26 us1.008.928669 KB
WriteAsync\cache\corerun.exe10485764096Asynchronous4,191.59 us1.03-97 KB
WriteAsync\true\corerun.exe10485764096Asynchronous4,077.05 us1.00-74 KB
ReadAsync_NoBuffering\cache\corerun.exe104857616384Asynchronous730.67 us1.00-18 KB
ReadAsync_NoBuffering\true\corerun.exe104857616384Asynchronous732.58 us1.00-17 KB
WriteAsync_NoBuffering\cache\corerun.exe104857616384Asynchronous2,747.11 us0.99-18 KB
WriteAsync_NoBuffering\true\corerun.exe104857616384Asynchronous2,787.66 us1.00-17 KB
ReadAsync\cache\corerun.exe1048576004096Asynchronous250,998.78 us1.01-7,001 KB
ReadAsync\true\corerun.exe1048576004096Asynchronous249,280.08 us1.00-6,801 KB
WriteAsync\cache\corerun.exe1048576004096Asynchronous382,734.73 us1.041000.00009,205 KB
WriteAsync\true\corerun.exe1048576004096Asynchronous368,403.91 us1.00-6,905 KB
ReadAsync_NoBuffering\cache\corerun.exe10485760016384Asynchronous80,072.69 us0.97-1,750 KB
ReadAsync_NoBuffering\true\corerun.exe10485760016384Asynchronous82,376.92 us1.00-1,700 KB
WriteAsync_NoBuffering\cache\corerun.exe10485760016384Asynchronous120,385.98 us0.98-1,751 KB
WriteAsync_NoBuffering\true\corerun.exe10485760016384Asynchronous123,215.33 us1.00-1,701 KB

it looks like we are allocating less when buffering is disabled, but more than before when it's enabled

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall looks good to me, but we need to track and solve the allocation regression which is visible in the scenarios where buffering is enabled

…to the moment after _source.SetException|SetResult is called
@stephentoub

Copy link
Copy Markdown
Member

The interlocked operation that was trying to enable the instance to be reused was backwards... nothing was being pooled. The numbers will need to be re-run after that's fixed ;-)

@adamsitnik

Copy link
Copy Markdown
Member

The interlocked operation that was trying to enable the instance to be reused was backwards... nothing was being pooled.

You are right, that was the reason for the regression. The latest numbers show that allocations have been reduced:

MethodJobfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsyncafter10241024Asynchronous82.48 us1.005 KB
ReadAsyncbefore10241024Asynchronous82.12 us1.005 KB
WriteAsyncafter10241024Asynchronous500.23 us1.005 KB
WriteAsyncbefore10241024Asynchronous498.59 us1.005 KB
CopyToFileAsyncafter1024?None520.65 us1.005 KB
CopyToFileAsyncbefore1024?None522.37 us1.005 KB
CopyToFileAsyncafter1024?Asynchronous548.74 us1.006 KB
CopyToFileAsyncbefore1024?Asynchronous548.45 us1.006 KB
ReadAsyncafter1048576512Asynchronous2,354.25 us0.9857 KB
ReadAsyncbefore1048576512Asynchronous2,396.26 us1.0083 KB
WriteAsyncafter1048576512Asynchronous4,100.29 us0.9849 KB
WriteAsyncbefore1048576512Asynchronous4,183.45 us1.0075 KB
ReadAsyncafter10485764096Asynchronous2,355.74 us1.0137 KB
ReadAsyncbefore10485764096Asynchronous2,341.08 us1.0069 KB
WriteAsyncafter10485764096Asynchronous4,045.21 us0.9945 KB
WriteAsyncbefore10485764096Asynchronous4,073.02 us1.0074 KB
ReadAsync_NoBufferingafter104857616384Asynchronous729.91 us1.0210 KB
ReadAsync_NoBufferingbefore104857616384Asynchronous717.17 us1.0017 KB
WriteAsync_NoBufferingafter104857616384Asynchronous2,820.46 us1.0110 KB
WriteAsync_NoBufferingbefore104857616384Asynchronous2,793.28 us1.0017 KB
CopyToFileAsyncafter1048576?None2,338.90 us1.003 KB
CopyToFileAsyncbefore1048576?None2,343.55 us1.003 KB
CopyToFileAsyncafter1048576?Asynchronous2,574.37 us1.013 KB
CopyToFileAsyncbefore1048576?Asynchronous2,564.43 us1.004 KB
ReadAsyncafter1048576004096Asynchronous248,390.61 us0.983,601 KB
ReadAsyncbefore1048576004096Asynchronous252,787.95 us1.006,801 KB
WriteAsyncafter1048576004096Asynchronous357,904.15 us0.994,005 KB
WriteAsyncbefore1048576004096Asynchronous360,081.89 us1.006,905 KB
ReadAsync_NoBufferingafter10485760016384Asynchronous80,097.02 us0.99901 KB
ReadAsync_NoBufferingbefore10485760016384Asynchronous81,006.52 us1.001,701 KB
WriteAsync_NoBufferingafter10485760016384Asynchronous120,078.07 us0.97901 KB
WriteAsync_NoBufferingbefore10485760016384Asynchronous123,329.13 us1.001,701 KB
CopyToFileAsyncafter104857600?None73,213.21 us0.99177 KB
CopyToFileAsyncbefore104857600?None73,740.59 us1.00177 KB
CopyToFileAsyncafter104857600?Asynchronous87,047.37 us1.00114 KB
CopyToFileAsyncbefore104857600?Asynchronous87,193.75 us1.00214 KB

@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub your suggestions were great!

MethodJobfileSizeuserBufferSizeoptionsMeanRatioGen 0Allocated
ReadAsyncafter10241024Asynchronous87.48 us1.040.36135,240 B
ReadAsyncbefore10241024Asynchronous84.38 us1.000.34725,216 B
WriteAsyncafter10241024Asynchronous494.43 us0.99-4,960 B
WriteAsyncbefore10241024Asynchronous501.52 us1.00-4,936 B
CopyToFileAsyncafter1024?None513.31 us1.01-5,593 B
CopyToFileAsyncbefore1024?None510.34 us1.00-5,593 B
CopyToFileAsyncafter1024?Asynchronous539.68 us1.00-6,336 B
CopyToFileAsyncbefore1024?Asynchronous541.15 us1.00-6,320 B
ReadAsyncafter1048576512Asynchronous2,436.66 us0.98-58,279 B
ReadAsyncbefore1048576512Asynchronous2,480.83 us1.008.928684,775 B
WriteAsyncafter1048576512Asynchronous4,142.67 us0.92-50,029 B
WriteAsyncbefore1048576512Asynchronous4,511.11 us1.00-76,527 B
ReadAsyncafter10485764096Asynchronous2,153.31 us0.90-916 B
ReadAsyncbefore10485764096Asynchronous2,397.80 us1.008.928670,241 B
WriteAsyncafter10485764096Asynchronous3,976.06 us0.97-27,596 B
WriteAsyncbefore10485764096Asynchronous4,100.25 us1.00-75,562 B
ReadAsync_NoBufferingafter104857616384Asynchronous689.04 us0.94-760 B
ReadAsync_NoBufferingbefore104857616384Asynchronous733.21 us1.00-17,864 B
WriteAsync_NoBufferingafter104857616384Asynchronous2,781.44 us0.99-762 B
WriteAsync_NoBufferingbefore104857616384Asynchronous2,808.65 us1.00-17,866 B
CopyToFileAsyncafter1048576?None2,360.16 us0.98-3,244 B
CopyToFileAsyncbefore1048576?None2,405.43 us1.00-3,244 B
CopyToFileAsyncafter1048576?Asynchronous2,593.22 us1.01-2,044 B
CopyToFileAsyncbefore1048576?Asynchronous2,568.19 us1.00-3,924 B
ReadAsyncafter1048576004096Asynchronous247,853.42 us0.95-1,368 B
ReadAsyncbefore1048576004096Asynchronous259,775.67 us1.00-6,963,952 B
WriteAsyncafter1048576004096Asynchronous358,502.58 us0.97-2,258,912 B
WriteAsyncbefore1048576004096Asynchronous371,005.92 us1.00-7,070,648 B
ReadAsync_NoBufferingafter10485760016384Asynchronous76,073.40 us0.94-796 B
ReadAsync_NoBufferingbefore10485760016384Asynchronous81,316.51 us1.00-1,741,292 B
WriteAsync_NoBufferingafter10485760016384Asynchronous114,458.43 us0.88-832 B
WriteAsync_NoBufferingbefore10485760016384Asynchronous130,608.83 us1.00-1,741,328 B
CopyToFileAsyncafter104857600?None74,758.01 us1.01-180,828 B
CopyToFileAsyncbefore104857600?None73,939.22 us1.00-180,828 B
CopyToFileAsyncafter104857600?Asynchronous86,110.81 us0.98-2,220 B
CopyToFileAsyncbefore104857600?Asynchronous87,546.69 us1.00-219,524 B

@adamsitnik
adamsitnik marked this pull request as ready for review April 16, 2021 16:30
@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub I believe I have addressed all your feedback, PTAL one more time. I hope that we can merge it today and include it in Preview 4

@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub we have addressed the feedback, please take a look. I am going to post the benchmark results in 20-30 minutes

@stephentoub

Copy link
Copy Markdown
Member

This now also fixes #25074

@adamsitnik

Copy link
Copy Markdown
Member

The results (see the Allocated column)

MethodJobfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsyncafter10241024Asynchronous84.39 us0.985,240 B
ReadAsyncbefore10241024Asynchronous85.86 us1.005,216 B
WriteAsyncafter10241024Asynchronous483.68 us1.014,960 B
WriteAsyncbefore10241024Asynchronous478.92 us1.004,936 B
CopyToFileAsyncafter1024?None492.53 us1.015,593 B
CopyToFileAsyncbefore1024?None489.18 us1.005,593 B
CopyToFileAsyncafter1024?Asynchronous529.35 us1.026,336 B
CopyToFileAsyncbefore1024?Asynchronous521.85 us1.006,311 B
ReadAsyncafter1048576512Asynchronous2,371.68 us1.0058,279 B
ReadAsyncbefore1048576512Asynchronous2,372.78 us1.0084,775 B
WriteAsyncafter1048576512Asynchronous4,081.33 us0.9750,028 B
WriteAsyncbefore1048576512Asynchronous4,214.84 us1.0076,517 B
ReadAsyncafter10485764096Asynchronous2,138.36 us0.92913 B
ReadAsyncbefore10485764096Asynchronous2,332.66 us1.0070,241 B
WriteAsyncafter10485764096Asynchronous3,951.54 us0.9527,562 B
WriteAsyncbefore10485764096Asynchronous4,153.60 us1.0075,562 B
ReadAsync_NoBufferingafter104857616384Asynchronous674.58 us0.91760 B
ReadAsync_NoBufferingbefore104857616384Asynchronous740.07 us1.0017,864 B
WriteAsync_NoBufferingafter104857616384Asynchronous2,711.34 us0.99762 B
WriteAsync_NoBufferingbefore104857616384Asynchronous2,736.59 us1.0017,866 B
CopyToFileAsyncafter1048576?None1,961.68 us0.953,243 B
CopyToFileAsyncbefore1048576?None2,080.32 us1.003,244 B
CopyToFileAsyncafter1048576?Asynchronous2,283.20 us1.082,044 B
CopyToFileAsyncbefore1048576?Asynchronous2,161.78 us1.003,924 B
ReadAsyncafter1048576004096Asynchronous228,777.04 us0.931,056 B
ReadAsyncbefore1048576004096Asynchronous245,615.52 us1.006,963,952 B
WriteAsyncafter1048576004096Asynchronous353,822.32 us0.962,257,976 B
WriteAsyncbefore1048576004096Asynchronous370,913.51 us1.007,070,648 B
ReadAsync_NoBufferingafter10485760016384Asynchronous74,983.01 us0.95796 B
ReadAsync_NoBufferingbefore10485760016384Asynchronous79,064.04 us1.001,741,292 B
WriteAsync_NoBufferingafter10485760016384Asynchronous116,660.24 us0.95832 B
WriteAsync_NoBufferingbefore10485760016384Asynchronous123,323.67 us1.001,741,328 B
CopyToFileAsyncafter104857600?None73,465.73 us0.99180,828 B
CopyToFileAsyncbefore104857600?None74,040.22 us1.00180,828 B
CopyToFileAsyncafter104857600?Asynchronous85,207.68 us0.982,220 B
CopyToFileAsyncbefore104857600?Asynchronous87,037.81 us1.00219,524 B

@jeffhandley

Copy link
Copy Markdown
Member

Wowza some of those allocation improvements are incredible!

@Anipik
Anipik merged commit 7878130 into dotnet:mainApr 16, 2021
@carlossanlop
carlossanlop deleted the Caching branch April 16, 2021 23:07
@danmoseley

Copy link
Copy Markdown
Contributor

Nice.

{
_strategy.OnBufferAllocated(_buffer = new byte[_bufferSize]);
_buffer = GC.AllocateUninitializedArray<byte>(_bufferSize,
pinned: true); // this allows us to avoid pinning when the buffer is used for the syscalls

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That correct, but pinned: true also allocates the array in Gen2 as side-effect so this may actually hurt real-world scenarios at the end..

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can allocate it and use a GCHandle if that ends up being better. Previously it was pinned as part of a PreallocatedOverlapped.

(It's still not at all obvious when this newfangled POH should be used. )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can also investigate not pinning at all, in which case the code this interacts with will just create a gchandle for each operation.

And/or look at using a pool array, but we'd want to ensure enough synchronization was in place to minimize erroneous usage causing us to return an array still in use. We do that in a few other streams.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Here is a simple test:

using System;
using System.IO;
for (int i = 0; i < 100_000; i++)
{
using (var f = new FileStream("test", FileMode.Create))
{
f.WriteByte(42);
}
}
Console.WriteLine($"Allocated: {GC.GetTotalAllocatedBytes()} Gen2 GCs: {GC.CollectionCount(2)}");
  • .NET 5: Allocated: 442474096 Gen2 GCs: 0
  • This PR: Allocated: 448051624 Gen2 GCs: 103

It will be interesting to see whether these excessive Gen2 GCs hit performance gates of services trying .NET 6 previews.

It's still not at all obvious when this newfangled POH should be used.

Agree. It is very hard to use.

we'd want to ensure enough synchronization was in place to minimize erroneous usage causing us to return an array still in use

If you can cover all these cases, it may be better to use unmanaged buffer. It is pinned too, and it does not cause excessive Gen2 GCs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So allocating on the POH contributes to the gen2 budget. This is why we disable the buffer using size 1, that still works right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nothing in this file is used at all if buffer size is 1.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm looking forward to taking another stab at optimizing static files in .NET 6

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you can cover all these cases, it may be better to use unmanaged buffer. It is pinned too, and it does not cause excessive Gen2 GCs.

I'm going to start with a GCHandle and a normally allocated array. I believe in that case I can mostly restrict synchronization to the async code paths (plus disposal). If we use a native buffer, we'll need to protect the sync code paths as well. We can start with this and then see if it makes sense to use a pooled or native buffer as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Well, actually, I'm going to start by not pinning here at all (it'll then pin/unpin in the rest of the implementation per operation). If there's no measurable impact, we can stick with that for now.

@adamsitnikadamsitnik mentioned this pull request Apr 17, 2021
5 tasks
@ghostghost locked as resolved and limited conversation to collaborators May 17, 2021
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cache IValueTaskSource in AsyncWindowsFileStreamStrategy Use PreallocatedOverlapped when internal FileStream buffer isn't being used on Windows

9 participants

@carlossanlop@adamsitnik@stephentoub@jeffhandley@danmoseley@davidfowl@jkotas@jozkee@Anipik
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

FileStream rewrite: Caching the ValueTaskSource in AsyncWindowsFileStreamStrategy - #51363

Merged
Anipik merged 15 commits into
dotnet:mainfrom
carlossanlop:Caching
Apr 16, 2021
Merged

FileStream rewrite: Caching the ValueTaskSource in AsyncWindowsFileStreamStrategy#51363
Anipik merged 15 commits into
dotnet:mainfrom
carlossanlop:Caching

Conversation

@carlossanlop

@carlossanlopcarlossanlop commented Apr 16, 2021

Copy link
Copy Markdown
Contributor

Fixes#50972
Fixes#25074

When AsyncWindowsFileStreamStrategy is wrapped by a BufferedFileStreamStrategy, we need to make sure the ValueTaskSource instance is cached to reduce the number of allocations when calling ReadAsync or WriteAsync multiple times in a row.

This PR is a continuation of #50802, in which we switched from using TaskCompletionSource to IValueTaskSource.

Changes:

  • Moved the PreAllocatedOverlapped instance inside ValueTaskSource, so the latter becomes its owner. This was done because we are only supposed to have an instance of a PreAllocatedOverlapped if the ValueTaskSource was created from OnBufferedAllocated, which is a method called only by BufferedFileStreamStrategy right before writing or reading.
  • Removed MemoryValueTaskSource and moved the cases handled by it to ValueTaskSource.
  • Created a method that refreshes the value of the NativeOverlapped*. This is done every time we call ReadAsync/WriteAsync, to make sure we are pinning the memory passed by the user.

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @carlossanlop
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #50972

DAFT: Pending benchmarks.

When AsyncWindowsFileStreamStrategy is wrapped by a BufferedFileStreamStrategy, we need to make sure the ValueTaskSource instance is cached to reduce the number of allocations when calling ReadAsync or WriteAsync multiple times in a row.

This PR is a continuation of #50802, in which we switched from using TaskCompletionSource to IValueTaskSource.

Changes:

  • Moved the PreAllocatedOverlapped instance inside ValueTaskSource, so the latter becomes its owner. This was done because we are only supposed to have an instance of a PreAllocatedOverlapped if the ValueTaskSource was created from OnBufferedAllocated, which is a method called only by BufferedFileStreamStrategy right before writing or reading.
  • Removed MemoryValueTaskSource and moved the cases handled by it to ValueTaskSource.
  • Created a method that refreshes the value of the NativeOverlapped*. This is done every time we call ReadAsync/WriteAsync, to make sure we are pinning the memory passed by the user.
Author:carlossanlop
Assignees:carlossanlop
Labels:

area-System.IO

Milestone:6.0.0

@adamsitnik

Copy link
Copy Markdown
Member

Initial benchmark results (base is #50802):

Details
MethodToolchainfileSizeuserBufferSizeoptionsMeanRatioGen 0Allocated
ReadAsync\cache\corerun.exe10241024Asynchronous83.99 us1.020.33605 KB
ReadAsync\base\corerun.exe10241024Asynchronous81.98 us1.000.65455 KB
WriteAsync\cache\corerun.exe10241024Asynchronous487.70 us1.02-5 KB
WriteAsync\true\corerun.exe10241024Asynchronous476.89 us1.00-5 KB
ReadAsync\cache\corerun.exe1048576512Asynchronous2,649.16 us1.0910.4167127 KB
ReadAsync\true\corerun.exe1048576512Asynchronous2,421.94 us1.008.928683 KB
WriteAsync\cache\corerun.exe1048576512Asynchronous4,527.33 us1.1115.6250119 KB
WriteAsync\true\corerun.exe1048576512Asynchronous4,100.58 us1.00-75 KB
ReadAsync\cache\corerun.exe10485764096Asynchronous2,321.45 us1.018.928671 KB
ReadAsync\true\corerun.exe10485764096Asynchronous2,308.26 us1.008.928669 KB
WriteAsync\cache\corerun.exe10485764096Asynchronous4,191.59 us1.03-97 KB
WriteAsync\true\corerun.exe10485764096Asynchronous4,077.05 us1.00-74 KB
ReadAsync_NoBuffering\cache\corerun.exe104857616384Asynchronous730.67 us1.00-18 KB
ReadAsync_NoBuffering\true\corerun.exe104857616384Asynchronous732.58 us1.00-17 KB
WriteAsync_NoBuffering\cache\corerun.exe104857616384Asynchronous2,747.11 us0.99-18 KB
WriteAsync_NoBuffering\true\corerun.exe104857616384Asynchronous2,787.66 us1.00-17 KB
ReadAsync\cache\corerun.exe1048576004096Asynchronous250,998.78 us1.01-7,001 KB
ReadAsync\true\corerun.exe1048576004096Asynchronous249,280.08 us1.00-6,801 KB
WriteAsync\cache\corerun.exe1048576004096Asynchronous382,734.73 us1.041000.00009,205 KB
WriteAsync\true\corerun.exe1048576004096Asynchronous368,403.91 us1.00-6,905 KB
ReadAsync_NoBuffering\cache\corerun.exe10485760016384Asynchronous80,072.69 us0.97-1,750 KB
ReadAsync_NoBuffering\true\corerun.exe10485760016384Asynchronous82,376.92 us1.00-1,700 KB
WriteAsync_NoBuffering\cache\corerun.exe10485760016384Asynchronous120,385.98 us0.98-1,751 KB
WriteAsync_NoBuffering\true\corerun.exe10485760016384Asynchronous123,215.33 us1.00-1,701 KB

it looks like we are allocating less when buffering is disabled, but more than before when it's enabled

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall looks good to me, but we need to track and solve the allocation regression which is visible in the scenarios where buffering is enabled

…to the moment after _source.SetException|SetResult is called
@stephentoub

Copy link
Copy Markdown
Member

The interlocked operation that was trying to enable the instance to be reused was backwards... nothing was being pooled. The numbers will need to be re-run after that's fixed ;-)

@adamsitnik

Copy link
Copy Markdown
Member

The interlocked operation that was trying to enable the instance to be reused was backwards... nothing was being pooled.

You are right, that was the reason for the regression. The latest numbers show that allocations have been reduced:

MethodJobfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsyncafter10241024Asynchronous82.48 us1.005 KB
ReadAsyncbefore10241024Asynchronous82.12 us1.005 KB
WriteAsyncafter10241024Asynchronous500.23 us1.005 KB
WriteAsyncbefore10241024Asynchronous498.59 us1.005 KB
CopyToFileAsyncafter1024?None520.65 us1.005 KB
CopyToFileAsyncbefore1024?None522.37 us1.005 KB
CopyToFileAsyncafter1024?Asynchronous548.74 us1.006 KB
CopyToFileAsyncbefore1024?Asynchronous548.45 us1.006 KB
ReadAsyncafter1048576512Asynchronous2,354.25 us0.9857 KB
ReadAsyncbefore1048576512Asynchronous2,396.26 us1.0083 KB
WriteAsyncafter1048576512Asynchronous4,100.29 us0.9849 KB
WriteAsyncbefore1048576512Asynchronous4,183.45 us1.0075 KB
ReadAsyncafter10485764096Asynchronous2,355.74 us1.0137 KB
ReadAsyncbefore10485764096Asynchronous2,341.08 us1.0069 KB
WriteAsyncafter10485764096Asynchronous4,045.21 us0.9945 KB
WriteAsyncbefore10485764096Asynchronous4,073.02 us1.0074 KB
ReadAsync_NoBufferingafter104857616384Asynchronous729.91 us1.0210 KB
ReadAsync_NoBufferingbefore104857616384Asynchronous717.17 us1.0017 KB
WriteAsync_NoBufferingafter104857616384Asynchronous2,820.46 us1.0110 KB
WriteAsync_NoBufferingbefore104857616384Asynchronous2,793.28 us1.0017 KB
CopyToFileAsyncafter1048576?None2,338.90 us1.003 KB
CopyToFileAsyncbefore1048576?None2,343.55 us1.003 KB
CopyToFileAsyncafter1048576?Asynchronous2,574.37 us1.013 KB
CopyToFileAsyncbefore1048576?Asynchronous2,564.43 us1.004 KB
ReadAsyncafter1048576004096Asynchronous248,390.61 us0.983,601 KB
ReadAsyncbefore1048576004096Asynchronous252,787.95 us1.006,801 KB
WriteAsyncafter1048576004096Asynchronous357,904.15 us0.994,005 KB
WriteAsyncbefore1048576004096Asynchronous360,081.89 us1.006,905 KB
ReadAsync_NoBufferingafter10485760016384Asynchronous80,097.02 us0.99901 KB
ReadAsync_NoBufferingbefore10485760016384Asynchronous81,006.52 us1.001,701 KB
WriteAsync_NoBufferingafter10485760016384Asynchronous120,078.07 us0.97901 KB
WriteAsync_NoBufferingbefore10485760016384Asynchronous123,329.13 us1.001,701 KB
CopyToFileAsyncafter104857600?None73,213.21 us0.99177 KB
CopyToFileAsyncbefore104857600?None73,740.59 us1.00177 KB
CopyToFileAsyncafter104857600?Asynchronous87,047.37 us1.00114 KB
CopyToFileAsyncbefore104857600?Asynchronous87,193.75 us1.00214 KB

@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub your suggestions were great!

MethodJobfileSizeuserBufferSizeoptionsMeanRatioGen 0Allocated
ReadAsyncafter10241024Asynchronous87.48 us1.040.36135,240 B
ReadAsyncbefore10241024Asynchronous84.38 us1.000.34725,216 B
WriteAsyncafter10241024Asynchronous494.43 us0.99-4,960 B
WriteAsyncbefore10241024Asynchronous501.52 us1.00-4,936 B
CopyToFileAsyncafter1024?None513.31 us1.01-5,593 B
CopyToFileAsyncbefore1024?None510.34 us1.00-5,593 B
CopyToFileAsyncafter1024?Asynchronous539.68 us1.00-6,336 B
CopyToFileAsyncbefore1024?Asynchronous541.15 us1.00-6,320 B
ReadAsyncafter1048576512Asynchronous2,436.66 us0.98-58,279 B
ReadAsyncbefore1048576512Asynchronous2,480.83 us1.008.928684,775 B
WriteAsyncafter1048576512Asynchronous4,142.67 us0.92-50,029 B
WriteAsyncbefore1048576512Asynchronous4,511.11 us1.00-76,527 B
ReadAsyncafter10485764096Asynchronous2,153.31 us0.90-916 B
ReadAsyncbefore10485764096Asynchronous2,397.80 us1.008.928670,241 B
WriteAsyncafter10485764096Asynchronous3,976.06 us0.97-27,596 B
WriteAsyncbefore10485764096Asynchronous4,100.25 us1.00-75,562 B
ReadAsync_NoBufferingafter104857616384Asynchronous689.04 us0.94-760 B
ReadAsync_NoBufferingbefore104857616384Asynchronous733.21 us1.00-17,864 B
WriteAsync_NoBufferingafter104857616384Asynchronous2,781.44 us0.99-762 B
WriteAsync_NoBufferingbefore104857616384Asynchronous2,808.65 us1.00-17,866 B
CopyToFileAsyncafter1048576?None2,360.16 us0.98-3,244 B
CopyToFileAsyncbefore1048576?None2,405.43 us1.00-3,244 B
CopyToFileAsyncafter1048576?Asynchronous2,593.22 us1.01-2,044 B
CopyToFileAsyncbefore1048576?Asynchronous2,568.19 us1.00-3,924 B
ReadAsyncafter1048576004096Asynchronous247,853.42 us0.95-1,368 B
ReadAsyncbefore1048576004096Asynchronous259,775.67 us1.00-6,963,952 B
WriteAsyncafter1048576004096Asynchronous358,502.58 us0.97-2,258,912 B
WriteAsyncbefore1048576004096Asynchronous371,005.92 us1.00-7,070,648 B
ReadAsync_NoBufferingafter10485760016384Asynchronous76,073.40 us0.94-796 B
ReadAsync_NoBufferingbefore10485760016384Asynchronous81,316.51 us1.00-1,741,292 B
WriteAsync_NoBufferingafter10485760016384Asynchronous114,458.43 us0.88-832 B
WriteAsync_NoBufferingbefore10485760016384Asynchronous130,608.83 us1.00-1,741,328 B
CopyToFileAsyncafter104857600?None74,758.01 us1.01-180,828 B
CopyToFileAsyncbefore104857600?None73,939.22 us1.00-180,828 B
CopyToFileAsyncafter104857600?Asynchronous86,110.81 us0.98-2,220 B
CopyToFileAsyncbefore104857600?Asynchronous87,546.69 us1.00-219,524 B

@adamsitnik
adamsitnik marked this pull request as ready for review April 16, 2021 16:30
@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub I believe I have addressed all your feedback, PTAL one more time. I hope that we can merge it today and include it in Preview 4

@adamsitnik

Copy link
Copy Markdown
Member

@stephentoub we have addressed the feedback, please take a look. I am going to post the benchmark results in 20-30 minutes

@stephentoub

Copy link
Copy Markdown
Member

This now also fixes #25074

@adamsitnik

Copy link
Copy Markdown
Member

The results (see the Allocated column)

MethodJobfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsyncafter10241024Asynchronous84.39 us0.985,240 B
ReadAsyncbefore10241024Asynchronous85.86 us1.005,216 B
WriteAsyncafter10241024Asynchronous483.68 us1.014,960 B
WriteAsyncbefore10241024Asynchronous478.92 us1.004,936 B
CopyToFileAsyncafter1024?None492.53 us1.015,593 B
CopyToFileAsyncbefore1024?None489.18 us1.005,593 B
CopyToFileAsyncafter1024?Asynchronous529.35 us1.026,336 B
CopyToFileAsyncbefore1024?Asynchronous521.85 us1.006,311 B
ReadAsyncafter1048576512Asynchronous2,371.68 us1.0058,279 B
ReadAsyncbefore1048576512Asynchronous2,372.78 us1.0084,775 B
WriteAsyncafter1048576512Asynchronous4,081.33 us0.9750,028 B
WriteAsyncbefore1048576512Asynchronous4,214.84 us1.0076,517 B
ReadAsyncafter10485764096Asynchronous2,138.36 us0.92913 B
ReadAsyncbefore10485764096Asynchronous2,332.66 us1.0070,241 B
WriteAsyncafter10485764096Asynchronous3,951.54 us0.9527,562 B
WriteAsyncbefore10485764096Asynchronous4,153.60 us1.0075,562 B
ReadAsync_NoBufferingafter104857616384Asynchronous674.58 us0.91760 B
ReadAsync_NoBufferingbefore104857616384Asynchronous740.07 us1.0017,864 B
WriteAsync_NoBufferingafter104857616384Asynchronous2,711.34 us0.99762 B
WriteAsync_NoBufferingbefore104857616384Asynchronous2,736.59 us1.0017,866 B
CopyToFileAsyncafter1048576?None1,961.68 us0.953,243 B
CopyToFileAsyncbefore1048576?None2,080.32 us1.003,244 B
CopyToFileAsyncafter1048576?Asynchronous2,283.20 us1.082,044 B
CopyToFileAsyncbefore1048576?Asynchronous2,161.78 us1.003,924 B
ReadAsyncafter1048576004096Asynchronous228,777.04 us0.931,056 B
ReadAsyncbefore1048576004096Asynchronous245,615.52 us1.006,963,952 B
WriteAsyncafter1048576004096Asynchronous353,822.32 us0.962,257,976 B
WriteAsyncbefore1048576004096Asynchronous370,913.51 us1.007,070,648 B
ReadAsync_NoBufferingafter10485760016384Asynchronous74,983.01 us0.95796 B
ReadAsync_NoBufferingbefore10485760016384Asynchronous79,064.04 us1.001,741,292 B
WriteAsync_NoBufferingafter10485760016384Asynchronous116,660.24 us0.95832 B
WriteAsync_NoBufferingbefore10485760016384Asynchronous123,323.67 us1.001,741,328 B
CopyToFileAsyncafter104857600?None73,465.73 us0.99180,828 B
CopyToFileAsyncbefore104857600?None74,040.22 us1.00180,828 B
CopyToFileAsyncafter104857600?Asynchronous85,207.68 us0.982,220 B
CopyToFileAsyncbefore104857600?Asynchronous87,037.81 us1.00219,524 B

@jeffhandley

Copy link
Copy Markdown
Member

Wowza some of those allocation improvements are incredible!

@Anipik
Anipik merged commit 7878130 into dotnet:mainApr 16, 2021
@carlossanlop
carlossanlop deleted the Caching branch April 16, 2021 23:07
@danmoseley

Copy link
Copy Markdown
Contributor

Nice.

{
_strategy.OnBufferAllocated(_buffer = new byte[_bufferSize]);
_buffer = GC.AllocateUninitializedArray<byte>(_bufferSize,
pinned: true); // this allows us to avoid pinning when the buffer is used for the syscalls

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That correct, but pinned: true also allocates the array in Gen2 as side-effect so this may actually hurt real-world scenarios at the end..

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can allocate it and use a GCHandle if that ends up being better. Previously it was pinned as part of a PreallocatedOverlapped.

(It's still not at all obvious when this newfangled POH should be used. )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can also investigate not pinning at all, in which case the code this interacts with will just create a gchandle for each operation.

And/or look at using a pool array, but we'd want to ensure enough synchronization was in place to minimize erroneous usage causing us to return an array still in use. We do that in a few other streams.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Here is a simple test:

using System;
using System.IO;
for (int i = 0; i < 100_000; i++)
{
using (var f = new FileStream("test", FileMode.Create))
{
f.WriteByte(42);
}
}
Console.WriteLine($"Allocated: {GC.GetTotalAllocatedBytes()} Gen2 GCs: {GC.CollectionCount(2)}");
  • .NET 5: Allocated: 442474096 Gen2 GCs: 0
  • This PR: Allocated: 448051624 Gen2 GCs: 103

It will be interesting to see whether these excessive Gen2 GCs hit performance gates of services trying .NET 6 previews.

It's still not at all obvious when this newfangled POH should be used.

Agree. It is very hard to use.

we'd want to ensure enough synchronization was in place to minimize erroneous usage causing us to return an array still in use

If you can cover all these cases, it may be better to use unmanaged buffer. It is pinned too, and it does not cause excessive Gen2 GCs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So allocating on the POH contributes to the gen2 budget. This is why we disable the buffer using size 1, that still works right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nothing in this file is used at all if buffer size is 1.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm looking forward to taking another stab at optimizing static files in .NET 6

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you can cover all these cases, it may be better to use unmanaged buffer. It is pinned too, and it does not cause excessive Gen2 GCs.

I'm going to start with a GCHandle and a normally allocated array. I believe in that case I can mostly restrict synchronization to the async code paths (plus disposal). If we use a native buffer, we'll need to protect the sync code paths as well. We can start with this and then see if it makes sense to use a pooled or native buffer as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Well, actually, I'm going to start by not pinning here at all (it'll then pin/unpin in the rest of the implementation per operation). If there's no measurable impact, we can stick with that for now.

@adamsitnikadamsitnik mentioned this pull request Apr 17, 2021
5 tasks
@ghostghost locked as resolved and limited conversation to collaborators May 17, 2021
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cache IValueTaskSource in AsyncWindowsFileStreamStrategy Use PreallocatedOverlapped when internal FileStream buffer isn't being used on Windows

9 participants

@carlossanlop@adamsitnik@stephentoub@jeffhandley@danmoseley@davidfowl@jkotas@jozkee@Anipik