FileStream rewrite: Use IValueTaskSource instead of TaskCompletionSource - #50802

Merged
carlossanlop merged 19 commits into
dotnet:mainfrom
carlossanlop:IValueTaskSource
Apr 15, 2021
Merged

FileStream rewrite: Use IValueTaskSource instead of TaskCompletionSource#50802
carlossanlop merged 19 commits into
dotnet:mainfrom
carlossanlop:IValueTaskSource

Conversation

@carlossanlop

Copy link
Copy Markdown
Contributor

The purpose of this PR is to remove a large allocation found in FileStream.ReadAsync and FileStream.WriteAsync, which we confirmed in our profiling investigation around the new FileStream rewrite code.

The allocation improvement can be achieved by creating a new type, called FileStreamValueTaskSource, that will have the same purpose as FileStreamCompletionSource, but instead of implementing TaskCompletionSource (which returns a Task to represent the asynchronous work) it implements IValueTaskSource, which returns a ValueTask.

The behavior was largely left untouched, except for the following differences:

  • Net5CompatFileStreamStrategy.Windows will be the only strategy that consumes the FileStreamCompletionSource, so it is now a private nested class.
  • One single new type implements both IValueTaskSource (used for write) and IValueTaskSource<int> (used for read).
  • The logic inside the callback and the cancellation token registration is the same, aside for the code that needs to report exceptions or return values.
  • We had interop errors redefined in multiple places.
  • The constants used by FileStreamCompletionSource were moved to FileStreamHelpers so they could be reused by the new type.

Unit tests

Manually ran unit tests from System.IO.FileSystem, System.IO, System.Runtime and System.Runtime.Extensions, they all passed. I made sure to run them with the Net5Compat flag turned off, to ensure my new code was executed.

Benchmarks

  • Ran the FileStream performance benchmarks against .NET 5.0, then against the code of this PR's baseline commit, and then against this PR's new code. I compared the results, and there are subtle improvements in some benchmark tests:
.NET 5.0 vs .NET 6.0 before this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_before_pr' --threshold 0.0001%

summary:
better: 39, geomean: 2.801
worse: 22, geomean: 2.098
total diff: 61

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.93882252.574348913.28
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.621003671.884633945.31bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.13257218.23805205.31several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.13286859.38896524.31
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.061795459.385498006.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.051868294.535689532.29
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)2.99254148.29758897.83
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.97283860.42843052.43
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.901844293.365353701.04several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.892209384.386395554.17bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.65301619.83798302.63
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.86557062.051035575.21bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.81478821.50868866.12
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.693479778.755891160.42several?
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.66543512.61903388.82bimodal
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.40168291900.00235314600.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.248197915.6310200393.75
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.215313143.756444323.96
Perf_FileStream.Flush(fileSize: 1024, options: None)1.183448111.254052132.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055473900.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.0956033900.0061052537.50
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.03133329050.00137489800.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.645489541.6747470.79
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.902811281.2543319.93
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.822571736.4644479.08
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.29876923.2641197.72
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.062463834750.00486578800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.94152781.6630909.09
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.72153731.8132593.67
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437889.31several?
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.98165696.6841632.18
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.97159526.5840152.10
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.50200423.3657204.05
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889181.93
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.55238920.2293543.27
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.50193629.0877357.59
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.443222202.501318286.88
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.39194808.8781591.76
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.36216916.9892096.58
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.24325116900.00145245400.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.157025697.923271982.50
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.10199647.8995019.35
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.04686776300.00336991400.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9519598723.0810074393.75
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)1.85508393800.00274360000.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.74796591200.00457934500.00bimodal
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.73156593600.0090313262.50
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.7022492768.7513248333.33
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64542213800.00331399500.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.615221529.173244615.63
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114593046.88
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742610.27
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09777023.21
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.191301925.001096436.88
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.12142000.45126451.42
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 512, options: None)1.11162435300.00146191400.00
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.10141476.75128484.00
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.10142083.81129048.19
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00113456750.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0288348175.0086894587.50
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 512, options: None)1.0192483275.0091193650.00
.NET 5.0 vs .NET 6.0 after this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_after_pr' --threshold 0.0001%
summary:
better: 36, geomean: 3.006
worse: 22, geomean: 2.080
total diff: 58

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.92882252.574341552.34
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.601003671.884615128.91bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)3.24283860.42919970.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)3.08254148.29783231.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.061868294.535710579.17
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.05286859.38874430.38
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.04257218.23780960.42
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)2.971795459.385329393.75
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.951844293.365432295.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.922209384.386445677.08
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.52301619.83758761.88
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.95557062.051087235.83
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.73543512.61937893.38bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.66478821.50793440.46bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.473479778.755113488.54several?
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.32168291900.00221660000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.235313143.756512252.08
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.228197915.6310012295.65
Perf_FileStream.Flush(fileSize: 1024, options: None)1.213448111.254163660.16
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1356033900.0063237650.00
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0950055150.0054423625.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0088983100.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.505489541.6747526.62
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.942811281.2543292.23
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.062571736.4645070.47
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.05876923.2641661.14
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.362463834750.00459292800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.95152781.6630850.74
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.75153731.8132349.21
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437926.07several?
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.98159526.5840096.31
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.96165696.6841824.65
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889051.28
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.56238920.2293276.60
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.53193629.0876383.11
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.45194808.8779416.04
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501376330.63
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.31216916.9893711.06
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.28325116900.00142880200.00
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)2.22200423.3690245.25
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.167025697.923250746.88
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.11508393800.00241032100.00
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.11199647.8994675.64
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.06686776300.00333108950.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9319598723.0810171781.82
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.74156593600.0090035450.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.65542213800.00329569650.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.645221529.173182177.50
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64796591200.00486630000.00
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.5222492768.7514797453.85
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114625460.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742460.71
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09781337.50
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.201301925.001085495.19
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.11141476.75127158.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.11142083.81128134.40
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.10142000.45129068.15
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00112942550.00

Profiling

I reused the same code we have in our benchmarks, but I executed it inside a console app that forces the consumption of the compiled runtime code.

You'll notice the top Task allocation went away, and the FileStreamCompletionSource allocations were substituted by FileStreamValueTaskSource allocations.

ReadAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,797
- System.String2,153
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,806
- System.String2,153
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]964
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

ReadAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,013
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,840
- System.String2,153
- System.Object[]955
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,836
- System.String2,144
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>64,235
- System.Threading.Tasks.Task<>64,015
- System.IO.FileStreamCompletionSource64,000
- System.SByte[]2,916
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,812
- System.SByte[]2,909
- System.String2,144
- System.Object[]964
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,798
- System.String2,153
- System.Object[]954
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,812
- System.String2,153
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]958
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,249
- System.IO.MemoryFileStreamCompletionSource32,000
- System.IO.FileStreamCompletionSource32,000
- System.SByte[]2,915
- System.String2,155
- System.Object[]952
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,246
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource32,000
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource32,000
- System.SByte[]2,893
- System.String2,144
- System.Object[]954
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.IO.FileStreamCompletionSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,963
- System.SByte[]2,915
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,830
- System.SByte[]2,912
- System.String2,144
- System.Object[]966
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Threading.Tasks.ValueTask.ValueTaskSourceAsTask249
- System.Threading.QueueUserWorkItemCallbackDefaultContext<>249

@adamsitnik@jozkee@jeffhandley@stephentoub

Also: Don't forget to ignore whitespace changes so it's easier to review the nested type's changes 😃

@ghost

ghost commented Apr 6, 2021

Copy link
Copy Markdown

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

Issue Details

The purpose of this PR is to remove a large allocation found in FileStream.ReadAsync and FileStream.WriteAsync, which we confirmed in our profiling investigation around the new FileStream rewrite code.

The allocation improvement can be achieved by creating a new type, called FileStreamValueTaskSource, that will have the same purpose as FileStreamCompletionSource, but instead of implementing TaskCompletionSource (which returns a Task to represent the asynchronous work) it implements IValueTaskSource, which returns a ValueTask.

The behavior was largely left untouched, except for the following differences:

  • Net5CompatFileStreamStrategy.Windows will be the only strategy that consumes the FileStreamCompletionSource, so it is now a private nested class.
  • One single new type implements both IValueTaskSource (used for write) and IValueTaskSource<int> (used for read).
  • The logic inside the callback and the cancellation token registration is the same, aside for the code that needs to report exceptions or return values.
  • We had interop errors redefined in multiple places.
  • The constants used by FileStreamCompletionSource were moved to FileStreamHelpers so they could be reused by the new type.

Unit tests

Manually ran unit tests from System.IO.FileSystem, System.IO, System.Runtime and System.Runtime.Extensions, they all passed. I made sure to run them with the Net5Compat flag turned off, to ensure my new code was executed.

Benchmarks

  • Ran the FileStream performance benchmarks against .NET 5.0, then against the code of this PR's baseline commit, and then against this PR's new code. I compared the results, and there are subtle improvements in some benchmark tests:
.NET 5.0 vs .NET 6.0 before this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_before_pr' --threshold 0.0001%

summary:
better: 39, geomean: 2.801
worse: 22, geomean: 2.098
total diff: 61

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.93882252.574348913.28
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.621003671.884633945.31bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.13257218.23805205.31several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.13286859.38896524.31
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.061795459.385498006.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.051868294.535689532.29
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)2.99254148.29758897.83
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.97283860.42843052.43
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.901844293.365353701.04several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.892209384.386395554.17bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.65301619.83798302.63
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.86557062.051035575.21bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.81478821.50868866.12
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.693479778.755891160.42several?
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.66543512.61903388.82bimodal
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.40168291900.00235314600.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.248197915.6310200393.75
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.215313143.756444323.96
Perf_FileStream.Flush(fileSize: 1024, options: None)1.183448111.254052132.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055473900.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.0956033900.0061052537.50
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.03133329050.00137489800.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.645489541.6747470.79
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.902811281.2543319.93
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.822571736.4644479.08
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.29876923.2641197.72
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.062463834750.00486578800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.94152781.6630909.09
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.72153731.8132593.67
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437889.31several?
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.98165696.6841632.18
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.97159526.5840152.10
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.50200423.3657204.05
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889181.93
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.55238920.2293543.27
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.50193629.0877357.59
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.443222202.501318286.88
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.39194808.8781591.76
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.36216916.9892096.58
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.24325116900.00145245400.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.157025697.923271982.50
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.10199647.8995019.35
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.04686776300.00336991400.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9519598723.0810074393.75
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)1.85508393800.00274360000.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.74796591200.00457934500.00bimodal
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.73156593600.0090313262.50
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.7022492768.7513248333.33
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64542213800.00331399500.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.615221529.173244615.63
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114593046.88
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742610.27
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09777023.21
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.191301925.001096436.88
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.12142000.45126451.42
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 512, options: None)1.11162435300.00146191400.00
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.10141476.75128484.00
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.10142083.81129048.19
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00113456750.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0288348175.0086894587.50
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 512, options: None)1.0192483275.0091193650.00
.NET 5.0 vs .NET 6.0 after this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_after_pr' --threshold 0.0001%
summary:
better: 36, geomean: 3.006
worse: 22, geomean: 2.080
total diff: 58

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.92882252.574341552.34
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.601003671.884615128.91bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)3.24283860.42919970.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)3.08254148.29783231.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.061868294.535710579.17
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.05286859.38874430.38
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.04257218.23780960.42
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)2.971795459.385329393.75
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.951844293.365432295.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.922209384.386445677.08
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.52301619.83758761.88
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.95557062.051087235.83
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.73543512.61937893.38bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.66478821.50793440.46bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.473479778.755113488.54several?
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.32168291900.00221660000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.235313143.756512252.08
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.228197915.6310012295.65
Perf_FileStream.Flush(fileSize: 1024, options: None)1.213448111.254163660.16
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1356033900.0063237650.00
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0950055150.0054423625.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0088983100.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.505489541.6747526.62
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.942811281.2543292.23
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.062571736.4645070.47
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.05876923.2641661.14
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.362463834750.00459292800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.95152781.6630850.74
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.75153731.8132349.21
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437926.07several?
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.98159526.5840096.31
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.96165696.6841824.65
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889051.28
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.56238920.2293276.60
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.53193629.0876383.11
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.45194808.8779416.04
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501376330.63
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.31216916.9893711.06
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.28325116900.00142880200.00
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)2.22200423.3690245.25
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.167025697.923250746.88
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.11508393800.00241032100.00
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.11199647.8994675.64
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.06686776300.00333108950.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9319598723.0810171781.82
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.74156593600.0090035450.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.65542213800.00329569650.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.645221529.173182177.50
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64796591200.00486630000.00
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.5222492768.7514797453.85
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114625460.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742460.71
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09781337.50
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.201301925.001085495.19
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.11141476.75127158.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.11142083.81128134.40
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.10142000.45129068.15
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00112942550.00

Profiling

I reused the same code we have in our benchmarks, but I executed it inside a console app that forces the consumption of the compiled runtime code.

You'll notice the top Task allocation went away, and the FileStreamCompletionSource allocations were substituted by FileStreamValueTaskSource allocations.

ReadAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,797
- System.String2,153
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,806
- System.String2,153
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]964
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

ReadAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,013
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,840
- System.String2,153
- System.Object[]955
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,836
- System.String2,144
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>64,235
- System.Threading.Tasks.Task<>64,015
- System.IO.FileStreamCompletionSource64,000
- System.SByte[]2,916
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,812
- System.SByte[]2,909
- System.String2,144
- System.Object[]964
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,798
- System.String2,153
- System.Object[]954
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,812
- System.String2,153
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]958
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,249
- System.IO.MemoryFileStreamCompletionSource32,000
- System.IO.FileStreamCompletionSource32,000
- System.SByte[]2,915
- System.String2,155
- System.Object[]952
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,246
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource32,000
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource32,000
- System.SByte[]2,893
- System.String2,144
- System.Object[]954
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.IO.FileStreamCompletionSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,963
- System.SByte[]2,915
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,830
- System.SByte[]2,912
- System.String2,144
- System.Object[]966
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Threading.Tasks.ValueTask.ValueTaskSourceAsTask249
- System.Threading.QueueUserWorkItemCallbackDefaultContext<>249

@adamsitnik@jozkee@jeffhandley@stephentoub

Also: Don't forget to ignore whitespace changes so it's easier to review the nested type's changes 😃
Author:carlossanlop
Assignees:carlossanlop
Labels:

area-System.IO

Milestone:6.0.0

@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

Tagging subscribers to this area: @carlossanlop

Yes, thank you bot... 😆

@jozkeejozkee 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.

Otherwise; LGTM, will approve if/when comments are resolved, most of them are nits and/or questions.

Comment on lines +17 to +22
internal const long NoResult = 0;
internal const long ResultSuccess = (long)1 << 32;
internal const long ResultError = (long)2 << 32;
internal const long RegisteringCancellation = (long)4 << 32;
internal const long CompletedCallback = (long)8 << 32;
internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32;

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.

Consider prefixing this constants with AsyncCompletionSource_ or something alike since these names are too ambiguous or keep them in their respective classes regardless of them being duplicated.

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.

or put them into a nested static class called ReturnCodes:

internalstaticpartialclassFileStreamHelpers{internalstaticclassReturnCodes{internalconstlongNoResult=0;internalconstlongResultSuccess=(long)1<<32;internalconstlongResultError=(long)2<<32;internalconstlongRegisteringCancellation=(long)4<<32;internalconstlongCompletedCallback=(long)8<<32;internalconstulongResultMask=((ulong)uint.MaxValue)<<32;}

but it's a nit (not a blocker)

Comment on lines +66 to +67
_strategy.CompareExchangeCurrentOverlappedOwner(this, null) == null ?
_strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!) : // allocated when buffer was created, and buffer is non-null

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.

Suggested change
_strategy.CompareExchangeCurrentOverlappedOwner(this,null)==null?
_strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!):// allocated when buffer was created, and buffer is non-null
strategy.CompareExchangeCurrentOverlappedOwner(this,null)==null?
strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!):// allocated when buffer was created, and buffer is non-null

// be directly the AwaitableProvider that's completing (in the case where the preallocated
// overlapped was already in use by another operation).
object? state = ThreadPoolBoundHandle.GetNativeOverlappedState(pOverlapped);
Debug.Assert(state is (AsyncWindowsFileStreamStrategy or FileStreamValueTaskSource));

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.

Suggested change
Debug.Assert(stateis(AsyncWindowsFileStreamStrategy or FileStreamValueTaskSource));
Debug.Assert(stateisAsyncWindowsFileStreamStrategy or FileStreamValueTaskSource);

int errorCode = unchecked((int)(packedResult & uint.MaxValue));
if (errorCode == Interop.Errors.ERROR_OPERATION_ABORTED)
{
_source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(new OperationCanceledException(cancellationToken.IsCancellationRequested ? cancellationToken : new CancellationToken(canceled: true))));

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.

Suggested change
_source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(newOperationCanceledException(cancellationToken.IsCancellationRequested?cancellationToken:newCancellationToken(canceled:true))));
Exceptione=newOperationCanceledException(cancellationToken.IsCancellationRequested?cancellationToken:newCancellationToken(canceled:true));
e.SetCurrentStackTrace();
_source.SetException(e);

}

private void Cancel(CancellationToken token)
{

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.

Missing comment and asserts:

// WARNING: This may potentially be called under a lock (during cancellation registration)
Debug.Assert(stateisFileStreamCompletionSource,"Unknown state passed to cancellation");
FileStreamCompletionSourcecompletionSource=(FileStreamCompletionSource)state;
Debug.Assert(completionSource._overlapped!=null&&!completionSource.Task.IsCompleted,"IO should not have completed yet");

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The first assert is not needed because the method argument is not an object? state.

I'll add the assert that checks for overlapped an completed status.

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.

The state param could be added to this method and allows you to conserve the first Debug.Assert.

long packedResult = Interlocked.CompareExchange(ref _result, FileStreamHelpers.RegisteringCancellation, FileStreamHelpers.NoResult);
if (packedResult == FileStreamHelpers.NoResult)
{
_cancellationRegistration = cancellationToken.UnsafeRegister(static (s, token) => ((FileStreamValueTaskSource)s!).Cancel(token), this);

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.

Could we pass the callback on a similar fashion to FileStreamCompletionSource

_cancellationRegistration=cancellationToken.UnsafeRegister(cancelCallback,this);

Or why did you opted for this way instead?

@carlossanlopcarlossanlopApr 8, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It made more sense to pass this as the second argument of UnsafeRegister, which ensures it's a ValueTaskSource instance, and saves us from the check of the object? state parameter like in the old code. The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

We have a similar example in System.Threading.Channels.

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.

It made more sense to pass this as the second argument of UnsafeRegister

FileStreamCompletionSource was also passing this as 2nd argument.

The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

Yes, seems that it was caching the method group to avoid allocating everytime it was passed to UnsafeRegister.
However I think you can avoid the allocation without caching yourself by doing this:

_cancellationRegistration=cancellationToken.UnsafeRegister((s,token)=>Cancel(s,token),this);

Just a suggestion, not something that you should block on.

@davidfowl

Copy link
Copy Markdown
Member

This is the change I was waiting for 👏🏾👏🏾

@davidfowl

Copy link
Copy Markdown
Member

I haven't looked but I assume we'll reuse the operation objects if there's no concurrency? This is what sockets do today so in the 95% case of no overlapping writes or reads, there'll be a single reusable allocation on the FileStream

@stephentoub

Copy link
Copy Markdown
Member

This is the change I was waiting for

It's not, actually. You want the next one where the IValueTaskSource instance is cached.

}
// else: Some other thread stole the result, so now it is responsible to finish the callback
}
// else: Some other thread is registering a cancellation, so it *must* finish the callback

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.

All this logic looks more complicated than it needs to be. I realize you're just following the same flow as what was already there, so nothing needs to be changed in this PR. But we should revisit all of this synchronization later; I suspect we can do it more succinctly and efficiently.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I opened an issue to follow up on this and on the caching of the IValueTaskSource.
#50972

@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 before we merge I would like to see the following benchmark results:

.NET 6 with new strategy enabled: before and after your change. (not .NET 5 vs 6 as there are too many moving targets)

You should be able to achieve that by running the following command in the perf repo;

git clone https://github.com/dotnet/performance.git
cd performance\src\benchmarks\micro
dotnet run -c Release -f net6.0 --filter *Perf_FileStream* --coreRun $pathToCoreRunWithoutYourChanges $pathToCoreRunWithYourChanges --envVars "DOTNET_SYSTEM_IO_USENET5COMPATFILESTREAM:0"

/// <see cref="MemoryHandle"/> when the operation has completed. This should only be used
/// when memory doesn't wrap a byte[].
/// </summary>
private sealed class MemoryAwaitableProvider : FileStreamValueTaskSource

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.

This should only be used when memory doesn't wrap a byte[].

I just realized that we don't have benchmarks for this scenario as Memory used in our micro-benchmarks always wraps a byte array. Could you please add such benchmarks to https://github.com/dotnet/performance/blob/main/src/benchmarks/micro/libraries/System.IO.FileSystem/Perf.FileStream.cs ? One test case should be enough as this should be rare.

@carlossanlopcarlossanlopApr 7, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Some of the benchmarks do consume this type. If you look at some of the profiling tests I executed, you'll see that MemoryValueTaskSource shows up a few times. The profiling tests reuse the code we had in our benchmarks.

I don't mind adding an explicit one though. Dou you want me to add one anyway?

Comment on lines +17 to +22
internal const long NoResult = 0;
internal const long ResultSuccess = (long)1 << 32;
internal const long ResultError = (long)2 << 32;
internal const long RegisteringCancellation = (long)4 << 32;
internal const long CompletedCallback = (long)8 << 32;
internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32;

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.

or put them into a nested static class called ReturnCodes:

internalstaticpartialclassFileStreamHelpers{internalstaticclassReturnCodes{internalconstlongNoResult=0;internalconstlongResultSuccess=(long)1<<32;internalconstlongResultError=(long)2<<32;internalconstlongRegisteringCancellation=(long)4<<32;internalconstlongCompletedCallback=(long)8<<32;internalconstulongResultMask=((ulong)uint.MaxValue)<<32;}

but it's a nit (not a blocker)

Comment on lines 62 to 63

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.

Did you run the perf. benchmarks after doing this change and did you notice any impact?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Pending.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Confirmed - There is a slight improvement.

I made sure to compare apples to apples: I used my latest commit, and built it setting RunContinuationsAsynchronously to true, then to false. I compared it both times against our code in .NET 5.0.

The improvement is clear when setting it to false (or not setting it at all).

RunContinuationsAsynchronously = true

❯ dotnet run -c release --base "D:\fs50" --diff "D:\fs60_NEW_ON_IVTS_asynctrue" --threshold 0.0001%
summary:
better: 24, geomean: 3.402
worse: 19, geomean: 2.244
total diff: 43

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)5.49882252.574840275.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)5.121003671.885136350.00bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)4.44257218.231141827.01
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)4.08286859.381171600.96
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.251795459.385832233.33
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)3.111844293.365741847.92several?
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.111868294.535807825.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)3.082209384.386796443.75
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)2.46557062.051371566.15
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)2.33478821.501116033.48
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)2.28543512.611239956.25bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.613479778.755612725.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.448197915.6311814575.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.33168291900.00224355000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.335313143.757063218.75
Perf_FileStream.Flush(fileSize: 1024, options: None)1.253448111.254313832.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1250055150.0056309025.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1256033900.0062803900.00
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.12133329050.00148833000.00bimodal
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)117.075489541.6746889.31
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)65.932811281.2542642.74
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.842571736.4644462.45
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.09876923.2641586.04
Perf_FileStream.OpenClose(fileSize: 1024, options: None)5.00152781.6630548.62
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.76153731.8132314.04
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)4.032463834750.00611183400.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.93165696.6842208.34
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.52200423.3656865.52
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.62230670.3887990.73
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.58238920.2292478.58
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501377939.84
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.87156593600.0083681962.50
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.6619598723.0811803641.67
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.6322492768.7513818900.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.577025697.924487121.88
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)1.47686776300.00466936500.00bimodal
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55743825.45
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.24975146.09784156.88
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.195221529.174387085.94
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.181301925.001103803.79
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.04116486775.00112407200.00
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.0321953590.9121241122.73
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0087467100.00
RunContinuationsAsynchronously = false

dotnet run -c release --base "D:\fs50" --diff "D:\fs60_NEW_ON_IVTS_asyncfalse" --threshold 0.0001%
summary:
better: 23, geomean: 3.592
worse: 20, geomean: 2.135
total diff: 43

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)5.36882252.574730875.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)5.051003671.885071912.50bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)4.25257218.231094164.38several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)4.05286859.381161160.27
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.231795459.385791481.25
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)3.101844293.365709820.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)3.092209384.386836504.17
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.071868294.535733883.33
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)2.46557062.051368034.90
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)2.31478821.501103696.25
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)2.24543512.611218234.38bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.603479778.755572682.29
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.428197915.6311634850.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.33168291900.00223141800.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.315313143.756949831.25
Perf_FileStream.Flush(fileSize: 1024, options: None)1.263448111.254332915.63
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.13133329050.00150223800.00bimodal
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055699050.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1056033900.0061710550.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0088348175.0088759625.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)116.975489541.6746929.42
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)65.762811281.2542753.29
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.562571736.4644676.15
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.52876923.2640756.47
Perf_FileStream.OpenClose(fileSize: 1024, options: None)5.06152781.6630216.63
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.81153731.8131975.57
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)4.052463834750.00608864800.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.99165696.6841526.73
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.54200423.3656610.08
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.62230670.3887877.10
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.59238920.2292291.75
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.403222202.501345116.25
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.86156593600.0083969875.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.6419598723.0811925173.68
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.6222492768.7513859764.71
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.567025697.924493881.25
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)1.46686776300.00470158300.00bimodal
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.32987460.55750294.64
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.24975146.09784056.56
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.171301925.001108880.36
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.175221529.174470673.44
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.0721953590.9120580725.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.01116486775.00

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Here is the result comparison if we compare the benchmark results of setting it to true vs false:

base: RunContinuationsAsynchronously=true, diff: RunContinuationsAsynchronously=false

dotnet run -c release --base "D:\fs60_NEW_ON_IVTS_asynctrue" --diff "D:\fs60_NEW_ON_IVTS_asyncfalse" --threshold 0.0001%
summary:
better: 14, geomean: 1.018
worse: 11, geomean: 1.017
total diff: 25

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.WriteAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0567143550.0070259187.50
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.02112407200.00115107550.00
Perf_FileStream.Read_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.02240016.67244921.93
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.024387085.944470673.44
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0187467100.0088759625.00
Perf_FileStream.WriteAsync_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.0121684354.5521982809.09
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: Asynchronous)1.011236709.901253242.19
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 1024, options: None)1.0139289.5439730.73
Perf_FileStream.Write_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.0121445427.2721646650.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.01743825.45750294.64
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)1.0044462.4544676.15
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.WriteByte(fileSize: 1024, options: None)1.041141827.011094164.38several?
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0345688612.5044474575.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.021377939.841345116.25
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)1.024840275.004730875.00
Perf_FileStream.SeekForward(fileSize: 1024, options: None)1.0241586.0440756.47
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: Asynchronous)1.02130849050.00128463775.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)1.0242208.3441526.73
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.0211814575.0011634850.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)1.015807825.005733883.33
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)1.015136350.005071912.50
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0156309025.0055699050.00
Perf_FileStream.Read_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0133747814.293338345.14
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)1.0132314.0431975.57
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 1024, options: Asynchronous)1.0081988.0981641.78

long packedResult = Interlocked.CompareExchange(ref _result, FileStreamHelpers.RegisteringCancellation, FileStreamHelpers.NoResult);
if (packedResult == FileStreamHelpers.NoResult)
{
_cancellationRegistration = cancellationToken.UnsafeRegister(static (s, token) => ((FileStreamValueTaskSource)s!).Cancel(token), this);

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.

It made more sense to pass this as the second argument of UnsafeRegister

FileStreamCompletionSource was also passing this as 2nd argument.

The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

Yes, seems that it was caching the method group to avoid allocating everytime it was passed to UnsafeRegister.
However I think you can avoid the allocation without caching yourself by doing this:

_cancellationRegistration=cancellationToken.UnsafeRegister((s,token)=>Cancel(s,token),this);

Just a suggestion, not something that you should block on.

Comment on lines 120 to 122

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.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I tried, but a ValueTask does not have a Result property, like a ValueTask<T> does, because it returns void. So I'm not sure if it's possible to apply an equivalent pattern.

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.

publicoverridevoidWrite(byte[]buffer,intoffset,intcount){ValueTaskvt=WriteAsyncInternal(newReadOnlyMemory<byte>(buffer,offset,count),CancellationToken.None);if(vt.IsCompleted){vt.GetAwaiter().GetResult();}else{vt.AsTask().GetAwaiter().GetResult();}}

That said, it's not much of an issue for this case. The read side is an issue because AsTask() will almost certainly need to allocate a new Task<int> in order to hand back the result, but for the write side, AsTask() on an already completed ValueTask will just return Task.Completed, assuming the operation completed successfully. So the only time the existing code for the write side will allocate is when the operation was canceled or faulted, in which case we're about to throw an exception anyway, and the extra task allocation won't really matter. Up to you whether you want to change it.

@jozkeejozkee 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.

LGTM; thanks Carlos!

@adamsitnik

Copy link
Copy Markdown
Member

I compared it both times against our code in .NET 5.0.

Comparison against 5.0 is a comparison of all of the changes that were merged to main since we have snapped 5.0 with your change against 5.0. What we care about is how your change (and nothing else) is affecting the perf, so we need to isolate the runs to only your change.

To be able to do that you need to run the benchmarks using two CoreRuns:

  • as a baseline the corerun that was built from main when you have created a new branch, without any of your changes
  • your branch with your changes

My personal workflow:

  1. When I create a new branch, I perform a full build in Release:
build -c Release -subset clr+libs
  1. I copy the folder that contains CoreRun.exe (runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0) to a separate folder. Typically I call it "before".
  2. I apply some changes and perform an incremental Release build. In case of System.Private.CoreLib.dll it's:
build -c Release -subset clr.corelib+clr.nativecorelib+libs.PreTest
  1. I run the benchmarks using the old and new CoreRun.exe:
dotnet run -c Release -f net6.0 --filter *ABC* --corerun C:\Projects\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\before\CoreRun.exe C:\Projects\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe

@carlossanlop

carlossanlop commented Apr 13, 2021

Copy link
Copy Markdown
ContributorAuthor
  • as a baseline the corerun that was built from main when you have created a new branch, without any of your changes
  • your branch with your changes

@adamsitnik@jozkee here's the table comparing the before (original) and after of this PR. Most results had allocation improvements, but worse speed when FileOptions = Asynchronous:

Results

Command:

 dotnet run
-c Release
-f net6.0
--corerun
D:\runtime2\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe
D:\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe
--filter System.IO.Tests.Perf_FileStream.*Async*
--statisticalTest 3ms
--minIterationCount 100
--maxIterationCount 101
--artifacts "D:\finalresults"

Note: The runtime2 corerun contains the baseline commit before my changes, the runtime corerun contains this PR's changes.

MethodToolchainfileSizeuserBufferSizeoptionsMeanErrorStdDevMedianMinMaxRatioAllocated
ReadAsyncBEFORE10241024None78.69 μs0.255 μs0.736 μs78.53 μs77.61 μs80.66 μs1.005 KB
ReadAsyncAFTER10241024None79.10 μs0.237 μs0.673 μs79.08 μs76.96 μs80.71 μs1.015 KB
WriteAsyncBEFORE10241024None316.97 μs2.512 μs6.919 μs315.92 μs305.11 μs339.35 μs1.004 KB
WriteAsyncAFTER10241024None300.13 μs5.643 μs15.730 μs297.59 μs272.46 μs348.15 μs0.954 KB
ReadAsyncBEFORE10241024Asynchronous94.13 μs0.259 μs0.731 μs94.02 μs92.27 μs96.16 μs1.005 KB
ReadAsyncAFTER10241024Asynchronous85.48 μs0.842 μs2.291 μs85.11 μs82.58 μs92.09 μs0.915 KB
WriteAsyncBEFORE10241024Asynchronous292.63 μs5.659 μs15.869 μs290.59 μs264.25 μs338.98 μs1.005 KB
WriteAsyncAFTER10241024Asynchronous324.35 μs13.906 μs39.222 μs311.27 μs274.34 μs428.64 μs1.115 KB
FlushAsyncBEFORE1024?None4,606.81 μs46.736 μs133.341 μs4,633.92 μs4,374.04 μs4,952.63 μs1.00269 KB
FlushAsyncAFTER1024?None4,587.88 μs58.046 μs168.403 μs4,638.98 μs4,322.79 μs5,034.48 μs1.00269 KB
CopyToFileAsyncBEFORE1024?None474.57 μs5.497 μs15.141 μs474.66 μs440.31 μs520.31 μs1.005 KB
CopyToFileAsyncAFTER1024?None488.94 μs3.631 μs10.061 μs488.62 μs469.93 μs518.76 μs1.035 KB
FlushAsyncBEFORE1024?Asynchronous14,680.35 μs150.967 μs437.982 μs14,637.76 μs13,623.50 μs15,645.01 μs1.00301 KB
FlushAsyncAFTER1024?Asynchronous20,333.22 μs116.277 μs339.185 μs20,299.93 μs19,724.25 μs21,148.73 μs1.39261 KB
CopyToFileAsyncBEFORE1024?Asynchronous520.97 μs5.982 μs16.674 μs519.35 μs490.05 μs572.29 μs1.006 KB
CopyToFileAsyncAFTER1024?Asynchronous523.30 μs6.294 μs17.650 μs520.91 μs479.14 μs583.85 μs1.006 KB
ReadAsyncBEFORE1048576512None1,375.28 μs5.930 μs17.015 μs1,376.60 μs1,340.44 μs1,413.73 μs1.0085 KB
ReadAsyncAFTER1048576512None1,347.16 μs5.128 μs14.958 μs1,345.14 μs1,323.57 μs1,382.14 μs0.9885 KB
WriteAsyncBEFORE1048576512None3,530.03 μs38.149 μs108.222 μs3,521.78 μs3,299.91 μs3,823.63 μs1.0076 KB
WriteAsyncAFTER1048576512None3,473.85 μs47.943 μs134.438 μs3,469.00 μs3,222.90 μs3,832.17 μs0.9876 KB
ReadAsyncBEFORE1048576512Asynchronous3,278.98 μs30.616 μs87.844 μs3,257.98 μs3,151.46 μs3,507.69 μs1.0093 KB
ReadAsyncAFTER1048576512Asynchronous4,399.34 μs16.153 μs47.119 μs4,401.64 μs4,294.71 μs4,531.75 μs1.3483 KB
WriteAsyncBEFORE1048576512Asynchronous5,304.10 μs73.935 μs214.500 μs5,309.27 μs4,771.82 μs5,852.43 μs1.0085 KB
WriteAsyncAFTER1048576512Asynchronous6,506.15 μs94.189 μs271.756 μs6,505.61 μs6,042.59 μs7,132.02 μs1.2375 KB
ReadAsyncBEFORE10485764096None1,090.98 μs6.156 μs18.152 μs1,096.14 μs1,039.20 μs1,120.19 μs1.0029 KB
ReadAsyncAFTER10485764096None1,084.03 μs8.601 μs25.225 μs1,086.13 μs1,042.07 μs1,131.85 μs0.9929 KB
WriteAsyncBEFORE10485764096None3,247.60 μs43.259 μs124.117 μs3,243.69 μs2,954.42 μs3,556.50 μs1.0055 KB
WriteAsyncAFTER10485764096None3,250.35 μs36.664 μs100.985 μs3,234.77 μs3,039.74 μs3,505.95 μs1.0055 KB
ReadAsyncBEFORE10485764096Asynchronous3,273.61 μs17.591 μs50.753 μs3,264.72 μs3,190.18 μs3,410.97 μs1.0079 KB
ReadAsyncAFTER10485764096Asynchronous4,547.38 μs73.438 μs216.535 μs4,398.34 μs4,327.10 μs5,035.98 μs1.3969 KB
WriteAsyncBEFORE10485764096Asynchronous5,384.46 μs83.792 μs240.415 μs5,346.69 μs4,897.35 μs5,955.92 μs1.0084 KB
WriteAsyncAFTER10485764096Asynchronous6,845.63 μs88.794 μs256.192 μs6,883.75 μs6,079.92 μs7,286.73 μs1.2774 KB
ReadAsync_NoBufferingBEFORE104857616384None406.34 μs2.413 μs7.113 μs403.28 μs387.16 μs419.08 μs1.007 KB
ReadAsync_NoBufferingAFTER104857616384None380.35 μs3.413 μs10.063 μs378.36 μs363.73 μs402.82 μs0.947 KB
WriteAsync_NoBufferingBEFORE104857616384None2,545.17 μs30.174 μs83.613 μs2,526.42 μs2,397.67 μs2,776.47 μs1.007 KB
WriteAsync_NoBufferingAFTER104857616384None2,537.77 μs27.489 μs76.628 μs2,528.02 μs2,421.65 μs2,750.68 μs1.007 KB
ReadAsync_NoBufferingBEFORE104857616384Asynchronous949.54 μs6.336 μs18.077 μs943.69 μs926.29 μs1,002.20 μs1.0020 KB
ReadAsync_NoBufferingAFTER104857616384Asynchronous1,195.21 μs3.070 μs8.507 μs1,194.94 μs1,179.45 μs1,218.92 μs1.2617 KB
WriteAsync_NoBufferingBEFORE104857616384Asynchronous2,965.91 μs40.616 μs114.558 μs2,952.10 μs2,677.93 μs3,244.55 μs1.0020 KB
WriteAsync_NoBufferingAFTER104857616384Asynchronous3,021.90 μs40.697 μs116.110 μs3,000.74 μs2,803.04 μs3,343.84 μs1.0217 KB
CopyToFileAsyncBEFORE1048576?None2,440.43 μs17.056 μs49.754 μs2,437.81 μs2,343.18 μs2,576.37 μs1.003 KB
CopyToFileAsyncAFTER1048576?None2,421.23 μs15.376 μs43.869 μs2,413.43 μs2,350.26 μs2,521.45 μs0.993 KB
CopyToFileAsyncBEFORE1048576?Asynchronous2,878.14 μs16.682 μs47.863 μs2,876.69 μs2,771.42 μs3,001.49 μs1.004 KB
CopyToFileAsyncAFTER1048576?Asynchronous2,769.09 μs15.649 μs43.882 μs2,760.85 μs2,691.56 μs2,885.67 μs0.964 KB
ReadAsyncBEFORE1048576004096None115,657.20 μs352.576 μs1,039.577 μs115,639.45 μs113,670.15 μs117,832.15 μs1.002,801 KB
ReadAsyncAFTER1048576004096None116,612.88 μs249.577 μs731.966 μs116,751.05 μs114,615.00 μs117,903.05 μs1.012,801 KB
WriteAsyncBEFORE1048576004096None205,378.59 μs7,785.258 μs22,462.248 μs194,584.50 μs184,125.30 μs268,874.60 μs1.005,005 KB
WriteAsyncAFTER1048576004096None209,453.56 μs10,355.532 μs30,043.276 μs194,064.20 μs173,778.40 μs293,593.30 μs1.035,005 KB
ReadAsyncBEFORE1048576004096Asynchronous327,225.97 μs1,532.011 μs4,346.059 μs326,564.00 μs320,187.10 μs339,508.20 μs1.007,801 KB
ReadAsyncAFTER1048576004096Asynchronous457,654.89 μs902.286 μs2,559.634 μs457,207.00 μs451,652.60 μs465,093.00 μs1.406,801 KB
WriteAsyncBEFORE1048576004096Asynchronous454,994.00 μs7,661.181 μs21,857.780 μs449,253.50 μs421,512.80 μs511,199.10 μs1.007,905 KB
WriteAsyncAFTER1048576004096Asynchronous600,076.62 μs10,088.501 μs29,268.570 μs592,035.20 μs559,265.50 μs677,403.80 μs1.326,905 KB
ReadAsync_NoBufferingBEFORE10485760016384None45,086.23 μs156.169 μs453.074 μs45,005.65 μs43,497.93 μs46,041.93 μs1.00701 KB
ReadAsync_NoBufferingAFTER10485760016384None42,133.53 μs174.922 μs457.740 μs42,093.18 μs41,187.75 μs43,731.75 μs0.93701 KB
WriteAsync_NoBufferingBEFORE10485760016384None61,814.53 μs685.277 μs1,887.453 μs61,419.69 μs59,079.38 μs68,035.43 μs1.00701 KB
WriteAsync_NoBufferingAFTER10485760016384None61,435.05 μs720.962 μs1,973.622 μs61,127.18 μs58,506.18 μs67,827.25 μs0.99701 KB
ReadAsync_NoBufferingBEFORE10485760016384Asynchronous97,220.19 μs667.147 μs1,881.701 μs96,464.10 μs94,386.65 μs102,972.80 μs1.001,951 KB
ReadAsync_NoBufferingAFTER10485760016384Asynchronous128,169.40 μs556.512 μs1,587.760 μs127,771.80 μs125,455.30 μs133,061.15 μs1.321,701 KB
WriteAsync_NoBufferingBEFORE10485760016384Asynchronous136,593.81 μs1,249.681 μs3,462.860 μs136,822.00 μs127,534.30 μs146,281.60 μs1.001,951 KB
WriteAsync_NoBufferingAFTER10485760016384Asynchronous169,656.11 μs2,245.877 μs6,515.696 μs168,284.30 μs157,404.20 μs189,342.10 μs1.251,701 KB
CopyToFileAsyncBEFORE104857600?None56,537.38 μs245.101 μs695.312 μs56,440.93 μs55,567.65 μs58,581.80 μs1.00177 KB
CopyToFileAsyncAFTER104857600?None56,184.32 μs275.348 μs785.585 μs56,116.43 μs54,790.00 μs58,574.72 μs0.99177 KB
CopyToFileAsyncBEFORE104857600?Asynchronous87,668.39 μs312.004 μs905.181 μs87,555.38 μs85,324.65 μs89,838.85 μs1.00246 KB
CopyToFileAsyncAFTER104857600?Asynchronous78,490.65 μs501.805 μs1,431.678 μs78,350.84 μs75,379.32 μs82,799.10 μs0.90214 KB

…Source only needed by Net5CompatFileStreamStrategy.
… one allocation and get the actual profiling improvement.
Rename FileStreamAwaitableProvider.
… review. Shorter filename for awaitable provider.
…trategy, and its _fileHandle can now be private.
@adamsitnik

Copy link
Copy Markdown
Member

Most results had allocation improvements, but worse speed when FileOptions = Asynchronous:

As discussed offline, such a regression needs to be solved before merging the PR.

I've even built your fork and run the benchmarks myself to be 100% sure, but unfortunately, I've confirmed the regression:

MethodToolchainfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsync_NoBuffering\after\corerun.exe104857616384Asynchronous838.1 us1.1217 KB
ReadAsync_NoBuffering\before\corerun.exe104857616384Asynchronous748.1 us1.0020 KB
ReadAsync_NoBuffering\after\corerun.exe10485760016384Asynchronous93,214.6 us1.121,700 KB
ReadAsync_NoBuffering\before\corerun.exe10485760016384Asynchronous83,445.4 us1.001,950 KB

If I can suggest something, ReadAsync_NoBuffering with fileSize=1048576 and options=Asynchronous should be the best benchmark for creating a repo app and profiling it. It's the simplest benchmark (buffering is not involved) and ReadAsync has a smaller deviation than WriteAsync. If you fail to find the regression with VS Profiler, you can use Perfview.

@adamsitnikadamsitnik added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Apr 14, 2021
@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

I compared the CPU usage before and after, and discovered we spend a considerable amount of time inside Overlapped.PerformIOCompletionCallback.

The new code (left) consumes 62% of the time calling ExecutionContext.RunInternal, while the old code (right) only took 3.82%.

Screenshot

image

The root cause seems to be this line, where we call _source.SetResult:

Screenshot

image

Which is adding up the time it takes to run the rest of the ReadAsync calls in the loop, including the disposal of the FileStream object in the main method.

There's a comment stating that ExecutionContext.RunInternal (part of the callstack) warning that the code is a very hot path:

Screenshot

image

I wasn't expecting all the continuation code to be counted as part of the asynchronous calls. I wonder if this has anything to do with the removal of RunContinuationsAsynchronously = true flag. I'll bring that flag back and compare the results.

cc @jozkee@adamsitnik@stephentoub

@davidfowl

Copy link
Copy Markdown
Member

We don't need to restore the ExecutionContext in the overlapped, that restore should be avoided in these APIs. The async state machine already does it. This is what you need #42549 😄

@stephentoub

stephentoub commented Apr 14, 2021

Copy link
Copy Markdown
Member

The new code (left) consumes 62% of the time calling ExecutionContext.RunInternal, while the old code (right) only took 3.82%.

That's inclusive (what "Total" means in the VS view). The callback is actually running the continuation now (because of RunContinuationsAsynchronously=false), rather than queueing it to be run on a different thread, so the 62% is inclusive of the actual app code.

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 know you're copying this logic from what was there before, but it seems to be unnecessarily complex, with lots of interlocked operations for transitioning from one state to another to another. If we're looking for places to reduce overheads, revisiting this whole implementation is likely a good place to start.

@adamsitnik

Copy link
Copy Markdown
Member

The callback is actually running the continuation now (because of RunContinuationsAsynchronously=false)

I've run all the async benchmarks for 3 configurations:

  • "before" (@carlossanlop fork main branch)
  • "false" (the current state of this PR as RunContinuationsAsynchronously is not set so it's false)
  • "true" ( _source.RunContinuationsAsynchronously = true; in ctor)

With the following settings:

  • 100 instead of 20 iterations (--minIterationCount 100 --maxIterationCount 101)
  • no outliers removal (--outliers DontRemove)

and confirmed that with _source.RunContinuationsAsynchronously = true; we have no regression in the microbenchmarks

@stephentoub should we:

  • set RunContinuationsAsynchronously = true; and include this PR in preview4
  • get some "real world" numbers from ASP.NET benchmarks (if there are none, write some and contribute to https://github.com/aspnet/benchmarks) and see how RunContinuationsAsynchronously setting affects non-micro benchmarks. This means preview5 for this PR.
Details
MethodToolchainfileSizeuserBufferSizeoptionsMeanStdDevMedianMinMaxRatioAllocated
ReadAsync\before\corerun.exe10241024Asynchronous82.69 us1.946 us82.22 us81.60 us94.90 us1.005 KB
ReadAsync\false\corerun.exe10241024Asynchronous78.76 us0.718 us78.56 us78.04 us82.13 us0.955 KB
ReadAsync\true\corerun.exe10241024Asynchronous83.28 us0.825 us83.12 us82.48 us89.82 us1.015 KB
WriteAsync\before\corerun.exe10241024Asynchronous404.05 us22.574 us400.81 us379.20 us494.53 us1.005 KB
WriteAsync\false\corerun.exe10241024Asynchronous400.76 us17.211 us397.80 us380.07 us472.21 us0.995 KB
WriteAsync\true\corerun.exe10241024Asynchronous405.20 us25.469 us401.47 us383.59 us510.74 us1.015 KB
ReadAsync\before\corerun.exe1048576512Asynchronous2,418.83 us55.684 us2,413.59 us2,361.95 us2,804.46 us1.0093 KB
ReadAsync\false\corerun.exe1048576512Asynchronous2,648.89 us73.366 us2,626.45 us2,550.29 us2,959.92 us1.1083 KB
ReadAsync\true\corerun.exe1048576512Asynchronous2,386.25 us9.985 us2,386.16 us2,356.79 us2,419.58 us0.9983 KB
WriteAsync\before\corerun.exe1048576512Asynchronous4,254.70 us277.778 us4,213.63 us3,967.55 us6,203.71 us1.0085 KB
WriteAsync\false\corerun.exe1048576512Asynchronous4,583.08 us183.949 us4,559.42 us4,317.29 us5,334.19 us1.0875 KB
WriteAsync\true\corerun.exe1048576512Asynchronous4,207.77 us179.340 us4,173.53 us3,896.65 us4,865.02 us0.9975 KB
ReadAsync\before\corerun.exe10485764096Asynchronous2,343.06 us35.339 us2,355.62 us2,225.73 us2,379.72 us1.0079 KB
ReadAsync\false\corerun.exe10485764096Asynchronous2,603.91 us40.916 us2,603.11 us2,513.55 us2,715.90 us1.1169 KB
ReadAsync\true\corerun.exe10485764096Asynchronous2,324.68 us38.225 us2,337.29 us2,226.14 us2,374.04 us0.9969 KB
WriteAsync\before\corerun.exe10485764096Asynchronous4,110.48 us265.902 us4,050.53 us3,844.25 us6,007.74 us1.0084 KB
WriteAsync\false\corerun.exe10485764096Asynchronous4,572.16 us320.658 us4,542.09 us4,256.30 us7,440.25 us1.1274 KB
WriteAsync\true\corerun.exe10485764096Asynchronous4,048.01 us184.553 us4,015.01 us3,739.28 us4,754.61 us0.9974 KB
ReadAsync_NoBuffering\before\corerun.exe104857616384Asynchronous747.53 us5.076 us747.87 us729.80 us755.69 us1.0020 KB
ReadAsync_NoBuffering\false\corerun.exe104857616384Asynchronous822.87 us6.877 us822.90 us809.94 us838.18 us1.1017 KB
ReadAsync_NoBuffering\true\corerun.exe104857616384Asynchronous726.82 us11.262 us726.44 us699.96 us754.36 us0.9717 KB
WriteAsync_NoBuffering\before\corerun.exe104857616384Asynchronous2,726.54 us136.755 us2,712.48 us2,534.40 us3,234.12 us1.0020 KB
WriteAsync_NoBuffering\false\corerun.exe104857616384Asynchronous2,761.05 us146.361 us2,792.11 us2,507.70 us3,188.16 us1.0117 KB
WriteAsync_NoBuffering\true\corerun.exe104857616384Asynchronous2,736.46 us194.495 us2,705.98 us2,415.11 us4,022.13 us1.0117 KB
ReadAsync\before\corerun.exe1048576004096Asynchronous250,490.85 us2,604.707 us250,256.45 us244,814.30 us256,607.90 us1.007,801 KB
ReadAsync\false\corerun.exe1048576004096Asynchronous278,260.89 us4,931.050 us278,009.45 us269,882.60 us297,338.40 us1.116,803 KB
ReadAsync\true\corerun.exe1048576004096Asynchronous252,682.05 us19,763.442 us250,702.50 us243,911.20 us447,534.10 us1.016,801 KB
WriteAsync\before\corerun.exe1048576004096Asynchronous370,893.59 us6,590.799 us369,781.05 us352,834.20 us390,366.40 us1.007,905 KB
WriteAsync\false\corerun.exe1048576004096Asynchronous397,619.03 us8,020.098 us397,052.95 us384,609.00 us446,559.70 us1.076,905 KB
WriteAsync\true\corerun.exe1048576004096Asynchronous358,128.05 us6,536.171 us357,755.45 us346,707.00 us382,735.70 us0.976,905 KB

@adamsitnikadamsitnik removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Apr 15, 2021

@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.

LGTM, thank you @carlossanlop !

@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

With the unit tests passing, the micro benchmarks having no regressions after reverting RunContinuationsAsynchronously to true, and having allocation improvements, I feel confident in merging this change to include it in Preview4, and will continue working on #50972 to add caching and improve this code for Preview5.

@carlossanlop
carlossanlop merged commit a38d0c2 into dotnet:mainApr 15, 2021
@carlossanlop
carlossanlop deleted the IValueTaskSource branch April 15, 2021 22:51
@ghostghost locked as resolved and limited conversation to collaborators May 15, 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.

5 participants

@carlossanlop@davidfowl@stephentoub@adamsitnik@jozkee
, '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: Use IValueTaskSource instead of TaskCompletionSource - #50802

Merged
carlossanlop merged 19 commits into
dotnet:mainfrom
carlossanlop:IValueTaskSource
Apr 15, 2021
Merged

FileStream rewrite: Use IValueTaskSource instead of TaskCompletionSource#50802
carlossanlop merged 19 commits into
dotnet:mainfrom
carlossanlop:IValueTaskSource

Conversation

@carlossanlop

Copy link
Copy Markdown
Contributor

The purpose of this PR is to remove a large allocation found in FileStream.ReadAsync and FileStream.WriteAsync, which we confirmed in our profiling investigation around the new FileStream rewrite code.

The allocation improvement can be achieved by creating a new type, called FileStreamValueTaskSource, that will have the same purpose as FileStreamCompletionSource, but instead of implementing TaskCompletionSource (which returns a Task to represent the asynchronous work) it implements IValueTaskSource, which returns a ValueTask.

The behavior was largely left untouched, except for the following differences:

  • Net5CompatFileStreamStrategy.Windows will be the only strategy that consumes the FileStreamCompletionSource, so it is now a private nested class.
  • One single new type implements both IValueTaskSource (used for write) and IValueTaskSource<int> (used for read).
  • The logic inside the callback and the cancellation token registration is the same, aside for the code that needs to report exceptions or return values.
  • We had interop errors redefined in multiple places.
  • The constants used by FileStreamCompletionSource were moved to FileStreamHelpers so they could be reused by the new type.

Unit tests

Manually ran unit tests from System.IO.FileSystem, System.IO, System.Runtime and System.Runtime.Extensions, they all passed. I made sure to run them with the Net5Compat flag turned off, to ensure my new code was executed.

Benchmarks

  • Ran the FileStream performance benchmarks against .NET 5.0, then against the code of this PR's baseline commit, and then against this PR's new code. I compared the results, and there are subtle improvements in some benchmark tests:
.NET 5.0 vs .NET 6.0 before this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_before_pr' --threshold 0.0001%

summary:
better: 39, geomean: 2.801
worse: 22, geomean: 2.098
total diff: 61

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.93882252.574348913.28
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.621003671.884633945.31bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.13257218.23805205.31several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.13286859.38896524.31
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.061795459.385498006.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.051868294.535689532.29
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)2.99254148.29758897.83
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.97283860.42843052.43
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.901844293.365353701.04several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.892209384.386395554.17bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.65301619.83798302.63
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.86557062.051035575.21bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.81478821.50868866.12
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.693479778.755891160.42several?
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.66543512.61903388.82bimodal
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.40168291900.00235314600.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.248197915.6310200393.75
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.215313143.756444323.96
Perf_FileStream.Flush(fileSize: 1024, options: None)1.183448111.254052132.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055473900.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.0956033900.0061052537.50
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.03133329050.00137489800.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.645489541.6747470.79
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.902811281.2543319.93
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.822571736.4644479.08
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.29876923.2641197.72
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.062463834750.00486578800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.94152781.6630909.09
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.72153731.8132593.67
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437889.31several?
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.98165696.6841632.18
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.97159526.5840152.10
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.50200423.3657204.05
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889181.93
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.55238920.2293543.27
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.50193629.0877357.59
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.443222202.501318286.88
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.39194808.8781591.76
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.36216916.9892096.58
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.24325116900.00145245400.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.157025697.923271982.50
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.10199647.8995019.35
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.04686776300.00336991400.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9519598723.0810074393.75
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)1.85508393800.00274360000.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.74796591200.00457934500.00bimodal
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.73156593600.0090313262.50
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.7022492768.7513248333.33
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64542213800.00331399500.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.615221529.173244615.63
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114593046.88
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742610.27
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09777023.21
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.191301925.001096436.88
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.12142000.45126451.42
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 512, options: None)1.11162435300.00146191400.00
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.10141476.75128484.00
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.10142083.81129048.19
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00113456750.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0288348175.0086894587.50
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 512, options: None)1.0192483275.0091193650.00
.NET 5.0 vs .NET 6.0 after this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_after_pr' --threshold 0.0001%
summary:
better: 36, geomean: 3.006
worse: 22, geomean: 2.080
total diff: 58

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.92882252.574341552.34
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.601003671.884615128.91bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)3.24283860.42919970.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)3.08254148.29783231.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.061868294.535710579.17
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.05286859.38874430.38
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.04257218.23780960.42
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)2.971795459.385329393.75
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.951844293.365432295.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.922209384.386445677.08
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.52301619.83758761.88
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.95557062.051087235.83
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.73543512.61937893.38bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.66478821.50793440.46bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.473479778.755113488.54several?
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.32168291900.00221660000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.235313143.756512252.08
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.228197915.6310012295.65
Perf_FileStream.Flush(fileSize: 1024, options: None)1.213448111.254163660.16
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1356033900.0063237650.00
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0950055150.0054423625.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0088983100.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.505489541.6747526.62
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.942811281.2543292.23
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.062571736.4645070.47
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.05876923.2641661.14
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.362463834750.00459292800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.95152781.6630850.74
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.75153731.8132349.21
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437926.07several?
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.98159526.5840096.31
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.96165696.6841824.65
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889051.28
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.56238920.2293276.60
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.53193629.0876383.11
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.45194808.8779416.04
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501376330.63
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.31216916.9893711.06
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.28325116900.00142880200.00
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)2.22200423.3690245.25
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.167025697.923250746.88
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.11508393800.00241032100.00
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.11199647.8994675.64
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.06686776300.00333108950.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9319598723.0810171781.82
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.74156593600.0090035450.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.65542213800.00329569650.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.645221529.173182177.50
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64796591200.00486630000.00
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.5222492768.7514797453.85
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114625460.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742460.71
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09781337.50
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.201301925.001085495.19
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.11141476.75127158.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.11142083.81128134.40
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.10142000.45129068.15
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00112942550.00

Profiling

I reused the same code we have in our benchmarks, but I executed it inside a console app that forces the consumption of the compiled runtime code.

You'll notice the top Task allocation went away, and the FileStreamCompletionSource allocations were substituted by FileStreamValueTaskSource allocations.

ReadAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,797
- System.String2,153
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,806
- System.String2,153
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]964
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

ReadAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,013
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,840
- System.String2,153
- System.Object[]955
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,836
- System.String2,144
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>64,235
- System.Threading.Tasks.Task<>64,015
- System.IO.FileStreamCompletionSource64,000
- System.SByte[]2,916
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,812
- System.SByte[]2,909
- System.String2,144
- System.Object[]964
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,798
- System.String2,153
- System.Object[]954
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,812
- System.String2,153
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]958
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,249
- System.IO.MemoryFileStreamCompletionSource32,000
- System.IO.FileStreamCompletionSource32,000
- System.SByte[]2,915
- System.String2,155
- System.Object[]952
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,246
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource32,000
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource32,000
- System.SByte[]2,893
- System.String2,144
- System.Object[]954
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.IO.FileStreamCompletionSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,963
- System.SByte[]2,915
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,830
- System.SByte[]2,912
- System.String2,144
- System.Object[]966
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Threading.Tasks.ValueTask.ValueTaskSourceAsTask249
- System.Threading.QueueUserWorkItemCallbackDefaultContext<>249

@adamsitnik@jozkee@jeffhandley@stephentoub

Also: Don't forget to ignore whitespace changes so it's easier to review the nested type's changes 😃

@ghost

ghost commented Apr 6, 2021

Copy link
Copy Markdown

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

Issue Details

The purpose of this PR is to remove a large allocation found in FileStream.ReadAsync and FileStream.WriteAsync, which we confirmed in our profiling investigation around the new FileStream rewrite code.

The allocation improvement can be achieved by creating a new type, called FileStreamValueTaskSource, that will have the same purpose as FileStreamCompletionSource, but instead of implementing TaskCompletionSource (which returns a Task to represent the asynchronous work) it implements IValueTaskSource, which returns a ValueTask.

The behavior was largely left untouched, except for the following differences:

  • Net5CompatFileStreamStrategy.Windows will be the only strategy that consumes the FileStreamCompletionSource, so it is now a private nested class.
  • One single new type implements both IValueTaskSource (used for write) and IValueTaskSource<int> (used for read).
  • The logic inside the callback and the cancellation token registration is the same, aside for the code that needs to report exceptions or return values.
  • We had interop errors redefined in multiple places.
  • The constants used by FileStreamCompletionSource were moved to FileStreamHelpers so they could be reused by the new type.

Unit tests

Manually ran unit tests from System.IO.FileSystem, System.IO, System.Runtime and System.Runtime.Extensions, they all passed. I made sure to run them with the Net5Compat flag turned off, to ensure my new code was executed.

Benchmarks

  • Ran the FileStream performance benchmarks against .NET 5.0, then against the code of this PR's baseline commit, and then against this PR's new code. I compared the results, and there are subtle improvements in some benchmark tests:
.NET 5.0 vs .NET 6.0 before this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_before_pr' --threshold 0.0001%

summary:
better: 39, geomean: 2.801
worse: 22, geomean: 2.098
total diff: 61

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.93882252.574348913.28
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.621003671.884633945.31bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.13257218.23805205.31several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.13286859.38896524.31
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.061795459.385498006.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.051868294.535689532.29
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)2.99254148.29758897.83
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.97283860.42843052.43
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.901844293.365353701.04several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.892209384.386395554.17bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.65301619.83798302.63
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.86557062.051035575.21bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.81478821.50868866.12
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.693479778.755891160.42several?
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.66543512.61903388.82bimodal
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.40168291900.00235314600.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.248197915.6310200393.75
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.215313143.756444323.96
Perf_FileStream.Flush(fileSize: 1024, options: None)1.183448111.254052132.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055473900.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.0956033900.0061052537.50
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.03133329050.00137489800.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.645489541.6747470.79
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.902811281.2543319.93
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.822571736.4644479.08
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.29876923.2641197.72
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.062463834750.00486578800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.94152781.6630909.09
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.72153731.8132593.67
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437889.31several?
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.98165696.6841632.18
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.97159526.5840152.10
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.50200423.3657204.05
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889181.93
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.55238920.2293543.27
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.50193629.0877357.59
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.443222202.501318286.88
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.39194808.8781591.76
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.36216916.9892096.58
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.24325116900.00145245400.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.157025697.923271982.50
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.10199647.8995019.35
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.04686776300.00336991400.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9519598723.0810074393.75
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)1.85508393800.00274360000.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.74796591200.00457934500.00bimodal
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.73156593600.0090313262.50
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.7022492768.7513248333.33
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64542213800.00331399500.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.615221529.173244615.63
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114593046.88
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742610.27
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09777023.21
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.191301925.001096436.88
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.12142000.45126451.42
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 512, options: None)1.11162435300.00146191400.00
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.10141476.75128484.00
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.10142083.81129048.19
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00113456750.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0288348175.0086894587.50
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 512, options: None)1.0192483275.0091193650.00
.NET 5.0 vs .NET 6.0 after this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_after_pr' --threshold 0.0001%
summary:
better: 36, geomean: 3.006
worse: 22, geomean: 2.080
total diff: 58

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.92882252.574341552.34
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.601003671.884615128.91bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)3.24283860.42919970.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)3.08254148.29783231.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.061868294.535710579.17
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.05286859.38874430.38
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.04257218.23780960.42
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)2.971795459.385329393.75
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.951844293.365432295.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.922209384.386445677.08
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.52301619.83758761.88
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.95557062.051087235.83
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.73543512.61937893.38bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.66478821.50793440.46bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.473479778.755113488.54several?
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.32168291900.00221660000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.235313143.756512252.08
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.228197915.6310012295.65
Perf_FileStream.Flush(fileSize: 1024, options: None)1.213448111.254163660.16
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1356033900.0063237650.00
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0950055150.0054423625.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0088983100.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.505489541.6747526.62
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.942811281.2543292.23
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.062571736.4645070.47
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.05876923.2641661.14
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.362463834750.00459292800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.95152781.6630850.74
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.75153731.8132349.21
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437926.07several?
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.98159526.5840096.31
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.96165696.6841824.65
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889051.28
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.56238920.2293276.60
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.53193629.0876383.11
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.45194808.8779416.04
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501376330.63
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.31216916.9893711.06
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.28325116900.00142880200.00
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)2.22200423.3690245.25
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.167025697.923250746.88
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.11508393800.00241032100.00
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.11199647.8994675.64
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.06686776300.00333108950.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9319598723.0810171781.82
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.74156593600.0090035450.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.65542213800.00329569650.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.645221529.173182177.50
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64796591200.00486630000.00
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.5222492768.7514797453.85
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114625460.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742460.71
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09781337.50
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.201301925.001085495.19
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.11141476.75127158.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.11142083.81128134.40
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.10142000.45129068.15
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00112942550.00

Profiling

I reused the same code we have in our benchmarks, but I executed it inside a console app that forces the consumption of the compiled runtime code.

You'll notice the top Task allocation went away, and the FileStreamCompletionSource allocations were substituted by FileStreamValueTaskSource allocations.

ReadAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,797
- System.String2,153
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,806
- System.String2,153
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]964
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

ReadAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,013
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,840
- System.String2,153
- System.Object[]955
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,836
- System.String2,144
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>64,235
- System.Threading.Tasks.Task<>64,015
- System.IO.FileStreamCompletionSource64,000
- System.SByte[]2,916
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,812
- System.SByte[]2,909
- System.String2,144
- System.Object[]964
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,798
- System.String2,153
- System.Object[]954
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,812
- System.String2,153
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]958
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,249
- System.IO.MemoryFileStreamCompletionSource32,000
- System.IO.FileStreamCompletionSource32,000
- System.SByte[]2,915
- System.String2,155
- System.Object[]952
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,246
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource32,000
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource32,000
- System.SByte[]2,893
- System.String2,144
- System.Object[]954
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.IO.FileStreamCompletionSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,963
- System.SByte[]2,915
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,830
- System.SByte[]2,912
- System.String2,144
- System.Object[]966
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Threading.Tasks.ValueTask.ValueTaskSourceAsTask249
- System.Threading.QueueUserWorkItemCallbackDefaultContext<>249

@adamsitnik@jozkee@jeffhandley@stephentoub

Also: Don't forget to ignore whitespace changes so it's easier to review the nested type's changes 😃
Author:carlossanlop
Assignees:carlossanlop
Labels:

area-System.IO

Milestone:6.0.0

@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

Tagging subscribers to this area: @carlossanlop

Yes, thank you bot... 😆

@jozkeejozkee 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.

Otherwise; LGTM, will approve if/when comments are resolved, most of them are nits and/or questions.

Comment on lines +17 to +22
internal const long NoResult = 0;
internal const long ResultSuccess = (long)1 << 32;
internal const long ResultError = (long)2 << 32;
internal const long RegisteringCancellation = (long)4 << 32;
internal const long CompletedCallback = (long)8 << 32;
internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32;

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.

Consider prefixing this constants with AsyncCompletionSource_ or something alike since these names are too ambiguous or keep them in their respective classes regardless of them being duplicated.

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.

or put them into a nested static class called ReturnCodes:

internalstaticpartialclassFileStreamHelpers{internalstaticclassReturnCodes{internalconstlongNoResult=0;internalconstlongResultSuccess=(long)1<<32;internalconstlongResultError=(long)2<<32;internalconstlongRegisteringCancellation=(long)4<<32;internalconstlongCompletedCallback=(long)8<<32;internalconstulongResultMask=((ulong)uint.MaxValue)<<32;}

but it's a nit (not a blocker)

Comment on lines +66 to +67
_strategy.CompareExchangeCurrentOverlappedOwner(this, null) == null ?
_strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!) : // allocated when buffer was created, and buffer is non-null

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.

Suggested change
_strategy.CompareExchangeCurrentOverlappedOwner(this,null)==null?
_strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!):// allocated when buffer was created, and buffer is non-null
strategy.CompareExchangeCurrentOverlappedOwner(this,null)==null?
strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!):// allocated when buffer was created, and buffer is non-null

// be directly the AwaitableProvider that's completing (in the case where the preallocated
// overlapped was already in use by another operation).
object? state = ThreadPoolBoundHandle.GetNativeOverlappedState(pOverlapped);
Debug.Assert(state is (AsyncWindowsFileStreamStrategy or FileStreamValueTaskSource));

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.

Suggested change
Debug.Assert(stateis(AsyncWindowsFileStreamStrategy or FileStreamValueTaskSource));
Debug.Assert(stateisAsyncWindowsFileStreamStrategy or FileStreamValueTaskSource);

int errorCode = unchecked((int)(packedResult & uint.MaxValue));
if (errorCode == Interop.Errors.ERROR_OPERATION_ABORTED)
{
_source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(new OperationCanceledException(cancellationToken.IsCancellationRequested ? cancellationToken : new CancellationToken(canceled: true))));

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.

Suggested change
_source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(newOperationCanceledException(cancellationToken.IsCancellationRequested?cancellationToken:newCancellationToken(canceled:true))));
Exceptione=newOperationCanceledException(cancellationToken.IsCancellationRequested?cancellationToken:newCancellationToken(canceled:true));
e.SetCurrentStackTrace();
_source.SetException(e);

}

private void Cancel(CancellationToken token)
{

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.

Missing comment and asserts:

// WARNING: This may potentially be called under a lock (during cancellation registration)
Debug.Assert(stateisFileStreamCompletionSource,"Unknown state passed to cancellation");
FileStreamCompletionSourcecompletionSource=(FileStreamCompletionSource)state;
Debug.Assert(completionSource._overlapped!=null&&!completionSource.Task.IsCompleted,"IO should not have completed yet");

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The first assert is not needed because the method argument is not an object? state.

I'll add the assert that checks for overlapped an completed status.

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.

The state param could be added to this method and allows you to conserve the first Debug.Assert.

long packedResult = Interlocked.CompareExchange(ref _result, FileStreamHelpers.RegisteringCancellation, FileStreamHelpers.NoResult);
if (packedResult == FileStreamHelpers.NoResult)
{
_cancellationRegistration = cancellationToken.UnsafeRegister(static (s, token) => ((FileStreamValueTaskSource)s!).Cancel(token), this);

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.

Could we pass the callback on a similar fashion to FileStreamCompletionSource

_cancellationRegistration=cancellationToken.UnsafeRegister(cancelCallback,this);

Or why did you opted for this way instead?

@carlossanlopcarlossanlopApr 8, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It made more sense to pass this as the second argument of UnsafeRegister, which ensures it's a ValueTaskSource instance, and saves us from the check of the object? state parameter like in the old code. The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

We have a similar example in System.Threading.Channels.

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.

It made more sense to pass this as the second argument of UnsafeRegister

FileStreamCompletionSource was also passing this as 2nd argument.

The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

Yes, seems that it was caching the method group to avoid allocating everytime it was passed to UnsafeRegister.
However I think you can avoid the allocation without caching yourself by doing this:

_cancellationRegistration=cancellationToken.UnsafeRegister((s,token)=>Cancel(s,token),this);

Just a suggestion, not something that you should block on.

@davidfowl

Copy link
Copy Markdown
Member

This is the change I was waiting for 👏🏾👏🏾

@davidfowl

Copy link
Copy Markdown
Member

I haven't looked but I assume we'll reuse the operation objects if there's no concurrency? This is what sockets do today so in the 95% case of no overlapping writes or reads, there'll be a single reusable allocation on the FileStream

@stephentoub

Copy link
Copy Markdown
Member

This is the change I was waiting for

It's not, actually. You want the next one where the IValueTaskSource instance is cached.

}
// else: Some other thread stole the result, so now it is responsible to finish the callback
}
// else: Some other thread is registering a cancellation, so it *must* finish the callback

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.

All this logic looks more complicated than it needs to be. I realize you're just following the same flow as what was already there, so nothing needs to be changed in this PR. But we should revisit all of this synchronization later; I suspect we can do it more succinctly and efficiently.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I opened an issue to follow up on this and on the caching of the IValueTaskSource.
#50972

@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 before we merge I would like to see the following benchmark results:

.NET 6 with new strategy enabled: before and after your change. (not .NET 5 vs 6 as there are too many moving targets)

You should be able to achieve that by running the following command in the perf repo;

git clone https://github.com/dotnet/performance.git
cd performance\src\benchmarks\micro
dotnet run -c Release -f net6.0 --filter *Perf_FileStream* --coreRun $pathToCoreRunWithoutYourChanges $pathToCoreRunWithYourChanges --envVars "DOTNET_SYSTEM_IO_USENET5COMPATFILESTREAM:0"

/// <see cref="MemoryHandle"/> when the operation has completed. This should only be used
/// when memory doesn't wrap a byte[].
/// </summary>
private sealed class MemoryAwaitableProvider : FileStreamValueTaskSource

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.

This should only be used when memory doesn't wrap a byte[].

I just realized that we don't have benchmarks for this scenario as Memory used in our micro-benchmarks always wraps a byte array. Could you please add such benchmarks to https://github.com/dotnet/performance/blob/main/src/benchmarks/micro/libraries/System.IO.FileSystem/Perf.FileStream.cs ? One test case should be enough as this should be rare.

@carlossanlopcarlossanlopApr 7, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Some of the benchmarks do consume this type. If you look at some of the profiling tests I executed, you'll see that MemoryValueTaskSource shows up a few times. The profiling tests reuse the code we had in our benchmarks.

I don't mind adding an explicit one though. Dou you want me to add one anyway?

Comment on lines +17 to +22
internal const long NoResult = 0;
internal const long ResultSuccess = (long)1 << 32;
internal const long ResultError = (long)2 << 32;
internal const long RegisteringCancellation = (long)4 << 32;
internal const long CompletedCallback = (long)8 << 32;
internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32;

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.

or put them into a nested static class called ReturnCodes:

internalstaticpartialclassFileStreamHelpers{internalstaticclassReturnCodes{internalconstlongNoResult=0;internalconstlongResultSuccess=(long)1<<32;internalconstlongResultError=(long)2<<32;internalconstlongRegisteringCancellation=(long)4<<32;internalconstlongCompletedCallback=(long)8<<32;internalconstulongResultMask=((ulong)uint.MaxValue)<<32;}

but it's a nit (not a blocker)

Comment on lines 62 to 63

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.

Did you run the perf. benchmarks after doing this change and did you notice any impact?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Pending.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Confirmed - There is a slight improvement.

I made sure to compare apples to apples: I used my latest commit, and built it setting RunContinuationsAsynchronously to true, then to false. I compared it both times against our code in .NET 5.0.

The improvement is clear when setting it to false (or not setting it at all).

RunContinuationsAsynchronously = true

❯ dotnet run -c release --base "D:\fs50" --diff "D:\fs60_NEW_ON_IVTS_asynctrue" --threshold 0.0001%
summary:
better: 24, geomean: 3.402
worse: 19, geomean: 2.244
total diff: 43

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)5.49882252.574840275.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)5.121003671.885136350.00bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)4.44257218.231141827.01
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)4.08286859.381171600.96
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.251795459.385832233.33
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)3.111844293.365741847.92several?
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.111868294.535807825.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)3.082209384.386796443.75
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)2.46557062.051371566.15
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)2.33478821.501116033.48
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)2.28543512.611239956.25bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.613479778.755612725.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.448197915.6311814575.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.33168291900.00224355000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.335313143.757063218.75
Perf_FileStream.Flush(fileSize: 1024, options: None)1.253448111.254313832.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1250055150.0056309025.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1256033900.0062803900.00
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.12133329050.00148833000.00bimodal
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)117.075489541.6746889.31
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)65.932811281.2542642.74
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.842571736.4644462.45
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.09876923.2641586.04
Perf_FileStream.OpenClose(fileSize: 1024, options: None)5.00152781.6630548.62
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.76153731.8132314.04
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)4.032463834750.00611183400.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.93165696.6842208.34
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.52200423.3656865.52
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.62230670.3887990.73
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.58238920.2292478.58
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501377939.84
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.87156593600.0083681962.50
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.6619598723.0811803641.67
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.6322492768.7513818900.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.577025697.924487121.88
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)1.47686776300.00466936500.00bimodal
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55743825.45
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.24975146.09784156.88
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.195221529.174387085.94
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.181301925.001103803.79
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.04116486775.00112407200.00
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.0321953590.9121241122.73
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0087467100.00
RunContinuationsAsynchronously = false

dotnet run -c release --base "D:\fs50" --diff "D:\fs60_NEW_ON_IVTS_asyncfalse" --threshold 0.0001%
summary:
better: 23, geomean: 3.592
worse: 20, geomean: 2.135
total diff: 43

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)5.36882252.574730875.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)5.051003671.885071912.50bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)4.25257218.231094164.38several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)4.05286859.381161160.27
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.231795459.385791481.25
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)3.101844293.365709820.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)3.092209384.386836504.17
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.071868294.535733883.33
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)2.46557062.051368034.90
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)2.31478821.501103696.25
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)2.24543512.611218234.38bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.603479778.755572682.29
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.428197915.6311634850.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.33168291900.00223141800.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.315313143.756949831.25
Perf_FileStream.Flush(fileSize: 1024, options: None)1.263448111.254332915.63
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.13133329050.00150223800.00bimodal
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055699050.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1056033900.0061710550.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0088348175.0088759625.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)116.975489541.6746929.42
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)65.762811281.2542753.29
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.562571736.4644676.15
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.52876923.2640756.47
Perf_FileStream.OpenClose(fileSize: 1024, options: None)5.06152781.6630216.63
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.81153731.8131975.57
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)4.052463834750.00608864800.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.99165696.6841526.73
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.54200423.3656610.08
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.62230670.3887877.10
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.59238920.2292291.75
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.403222202.501345116.25
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.86156593600.0083969875.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.6419598723.0811925173.68
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.6222492768.7513859764.71
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.567025697.924493881.25
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)1.46686776300.00470158300.00bimodal
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.32987460.55750294.64
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.24975146.09784056.56
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.171301925.001108880.36
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.175221529.174470673.44
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.0721953590.9120580725.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.01116486775.00

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Here is the result comparison if we compare the benchmark results of setting it to true vs false:

base: RunContinuationsAsynchronously=true, diff: RunContinuationsAsynchronously=false

dotnet run -c release --base "D:\fs60_NEW_ON_IVTS_asynctrue" --diff "D:\fs60_NEW_ON_IVTS_asyncfalse" --threshold 0.0001%
summary:
better: 14, geomean: 1.018
worse: 11, geomean: 1.017
total diff: 25

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.WriteAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0567143550.0070259187.50
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.02112407200.00115107550.00
Perf_FileStream.Read_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.02240016.67244921.93
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.024387085.944470673.44
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0187467100.0088759625.00
Perf_FileStream.WriteAsync_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.0121684354.5521982809.09
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: Asynchronous)1.011236709.901253242.19
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 1024, options: None)1.0139289.5439730.73
Perf_FileStream.Write_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.0121445427.2721646650.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.01743825.45750294.64
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)1.0044462.4544676.15
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.WriteByte(fileSize: 1024, options: None)1.041141827.011094164.38several?
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0345688612.5044474575.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.021377939.841345116.25
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)1.024840275.004730875.00
Perf_FileStream.SeekForward(fileSize: 1024, options: None)1.0241586.0440756.47
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: Asynchronous)1.02130849050.00128463775.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)1.0242208.3441526.73
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.0211814575.0011634850.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)1.015807825.005733883.33
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)1.015136350.005071912.50
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0156309025.0055699050.00
Perf_FileStream.Read_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0133747814.293338345.14
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)1.0132314.0431975.57
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 1024, options: Asynchronous)1.0081988.0981641.78

long packedResult = Interlocked.CompareExchange(ref _result, FileStreamHelpers.RegisteringCancellation, FileStreamHelpers.NoResult);
if (packedResult == FileStreamHelpers.NoResult)
{
_cancellationRegistration = cancellationToken.UnsafeRegister(static (s, token) => ((FileStreamValueTaskSource)s!).Cancel(token), this);

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.

It made more sense to pass this as the second argument of UnsafeRegister

FileStreamCompletionSource was also passing this as 2nd argument.

The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

Yes, seems that it was caching the method group to avoid allocating everytime it was passed to UnsafeRegister.
However I think you can avoid the allocation without caching yourself by doing this:

_cancellationRegistration=cancellationToken.UnsafeRegister((s,token)=>Cancel(s,token),this);

Just a suggestion, not something that you should block on.

Comment on lines 120 to 122

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.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I tried, but a ValueTask does not have a Result property, like a ValueTask<T> does, because it returns void. So I'm not sure if it's possible to apply an equivalent pattern.

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.

publicoverridevoidWrite(byte[]buffer,intoffset,intcount){ValueTaskvt=WriteAsyncInternal(newReadOnlyMemory<byte>(buffer,offset,count),CancellationToken.None);if(vt.IsCompleted){vt.GetAwaiter().GetResult();}else{vt.AsTask().GetAwaiter().GetResult();}}

That said, it's not much of an issue for this case. The read side is an issue because AsTask() will almost certainly need to allocate a new Task<int> in order to hand back the result, but for the write side, AsTask() on an already completed ValueTask will just return Task.Completed, assuming the operation completed successfully. So the only time the existing code for the write side will allocate is when the operation was canceled or faulted, in which case we're about to throw an exception anyway, and the extra task allocation won't really matter. Up to you whether you want to change it.

@jozkeejozkee 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.

LGTM; thanks Carlos!

@adamsitnik

Copy link
Copy Markdown
Member

I compared it both times against our code in .NET 5.0.

Comparison against 5.0 is a comparison of all of the changes that were merged to main since we have snapped 5.0 with your change against 5.0. What we care about is how your change (and nothing else) is affecting the perf, so we need to isolate the runs to only your change.

To be able to do that you need to run the benchmarks using two CoreRuns:

  • as a baseline the corerun that was built from main when you have created a new branch, without any of your changes
  • your branch with your changes

My personal workflow:

  1. When I create a new branch, I perform a full build in Release:
build -c Release -subset clr+libs
  1. I copy the folder that contains CoreRun.exe (runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0) to a separate folder. Typically I call it "before".
  2. I apply some changes and perform an incremental Release build. In case of System.Private.CoreLib.dll it's:
build -c Release -subset clr.corelib+clr.nativecorelib+libs.PreTest
  1. I run the benchmarks using the old and new CoreRun.exe:
dotnet run -c Release -f net6.0 --filter *ABC* --corerun C:\Projects\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\before\CoreRun.exe C:\Projects\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe

@carlossanlop

carlossanlop commented Apr 13, 2021

Copy link
Copy Markdown
ContributorAuthor
  • as a baseline the corerun that was built from main when you have created a new branch, without any of your changes
  • your branch with your changes

@adamsitnik@jozkee here's the table comparing the before (original) and after of this PR. Most results had allocation improvements, but worse speed when FileOptions = Asynchronous:

Results

Command:

 dotnet run
-c Release
-f net6.0
--corerun
D:\runtime2\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe
D:\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe
--filter System.IO.Tests.Perf_FileStream.*Async*
--statisticalTest 3ms
--minIterationCount 100
--maxIterationCount 101
--artifacts "D:\finalresults"

Note: The runtime2 corerun contains the baseline commit before my changes, the runtime corerun contains this PR's changes.

MethodToolchainfileSizeuserBufferSizeoptionsMeanErrorStdDevMedianMinMaxRatioAllocated
ReadAsyncBEFORE10241024None78.69 μs0.255 μs0.736 μs78.53 μs77.61 μs80.66 μs1.005 KB
ReadAsyncAFTER10241024None79.10 μs0.237 μs0.673 μs79.08 μs76.96 μs80.71 μs1.015 KB
WriteAsyncBEFORE10241024None316.97 μs2.512 μs6.919 μs315.92 μs305.11 μs339.35 μs1.004 KB
WriteAsyncAFTER10241024None300.13 μs5.643 μs15.730 μs297.59 μs272.46 μs348.15 μs0.954 KB
ReadAsyncBEFORE10241024Asynchronous94.13 μs0.259 μs0.731 μs94.02 μs92.27 μs96.16 μs1.005 KB
ReadAsyncAFTER10241024Asynchronous85.48 μs0.842 μs2.291 μs85.11 μs82.58 μs92.09 μs0.915 KB
WriteAsyncBEFORE10241024Asynchronous292.63 μs5.659 μs15.869 μs290.59 μs264.25 μs338.98 μs1.005 KB
WriteAsyncAFTER10241024Asynchronous324.35 μs13.906 μs39.222 μs311.27 μs274.34 μs428.64 μs1.115 KB
FlushAsyncBEFORE1024?None4,606.81 μs46.736 μs133.341 μs4,633.92 μs4,374.04 μs4,952.63 μs1.00269 KB
FlushAsyncAFTER1024?None4,587.88 μs58.046 μs168.403 μs4,638.98 μs4,322.79 μs5,034.48 μs1.00269 KB
CopyToFileAsyncBEFORE1024?None474.57 μs5.497 μs15.141 μs474.66 μs440.31 μs520.31 μs1.005 KB
CopyToFileAsyncAFTER1024?None488.94 μs3.631 μs10.061 μs488.62 μs469.93 μs518.76 μs1.035 KB
FlushAsyncBEFORE1024?Asynchronous14,680.35 μs150.967 μs437.982 μs14,637.76 μs13,623.50 μs15,645.01 μs1.00301 KB
FlushAsyncAFTER1024?Asynchronous20,333.22 μs116.277 μs339.185 μs20,299.93 μs19,724.25 μs21,148.73 μs1.39261 KB
CopyToFileAsyncBEFORE1024?Asynchronous520.97 μs5.982 μs16.674 μs519.35 μs490.05 μs572.29 μs1.006 KB
CopyToFileAsyncAFTER1024?Asynchronous523.30 μs6.294 μs17.650 μs520.91 μs479.14 μs583.85 μs1.006 KB
ReadAsyncBEFORE1048576512None1,375.28 μs5.930 μs17.015 μs1,376.60 μs1,340.44 μs1,413.73 μs1.0085 KB
ReadAsyncAFTER1048576512None1,347.16 μs5.128 μs14.958 μs1,345.14 μs1,323.57 μs1,382.14 μs0.9885 KB
WriteAsyncBEFORE1048576512None3,530.03 μs38.149 μs108.222 μs3,521.78 μs3,299.91 μs3,823.63 μs1.0076 KB
WriteAsyncAFTER1048576512None3,473.85 μs47.943 μs134.438 μs3,469.00 μs3,222.90 μs3,832.17 μs0.9876 KB
ReadAsyncBEFORE1048576512Asynchronous3,278.98 μs30.616 μs87.844 μs3,257.98 μs3,151.46 μs3,507.69 μs1.0093 KB
ReadAsyncAFTER1048576512Asynchronous4,399.34 μs16.153 μs47.119 μs4,401.64 μs4,294.71 μs4,531.75 μs1.3483 KB
WriteAsyncBEFORE1048576512Asynchronous5,304.10 μs73.935 μs214.500 μs5,309.27 μs4,771.82 μs5,852.43 μs1.0085 KB
WriteAsyncAFTER1048576512Asynchronous6,506.15 μs94.189 μs271.756 μs6,505.61 μs6,042.59 μs7,132.02 μs1.2375 KB
ReadAsyncBEFORE10485764096None1,090.98 μs6.156 μs18.152 μs1,096.14 μs1,039.20 μs1,120.19 μs1.0029 KB
ReadAsyncAFTER10485764096None1,084.03 μs8.601 μs25.225 μs1,086.13 μs1,042.07 μs1,131.85 μs0.9929 KB
WriteAsyncBEFORE10485764096None3,247.60 μs43.259 μs124.117 μs3,243.69 μs2,954.42 μs3,556.50 μs1.0055 KB
WriteAsyncAFTER10485764096None3,250.35 μs36.664 μs100.985 μs3,234.77 μs3,039.74 μs3,505.95 μs1.0055 KB
ReadAsyncBEFORE10485764096Asynchronous3,273.61 μs17.591 μs50.753 μs3,264.72 μs3,190.18 μs3,410.97 μs1.0079 KB
ReadAsyncAFTER10485764096Asynchronous4,547.38 μs73.438 μs216.535 μs4,398.34 μs4,327.10 μs5,035.98 μs1.3969 KB
WriteAsyncBEFORE10485764096Asynchronous5,384.46 μs83.792 μs240.415 μs5,346.69 μs4,897.35 μs5,955.92 μs1.0084 KB
WriteAsyncAFTER10485764096Asynchronous6,845.63 μs88.794 μs256.192 μs6,883.75 μs6,079.92 μs7,286.73 μs1.2774 KB
ReadAsync_NoBufferingBEFORE104857616384None406.34 μs2.413 μs7.113 μs403.28 μs387.16 μs419.08 μs1.007 KB
ReadAsync_NoBufferingAFTER104857616384None380.35 μs3.413 μs10.063 μs378.36 μs363.73 μs402.82 μs0.947 KB
WriteAsync_NoBufferingBEFORE104857616384None2,545.17 μs30.174 μs83.613 μs2,526.42 μs2,397.67 μs2,776.47 μs1.007 KB
WriteAsync_NoBufferingAFTER104857616384None2,537.77 μs27.489 μs76.628 μs2,528.02 μs2,421.65 μs2,750.68 μs1.007 KB
ReadAsync_NoBufferingBEFORE104857616384Asynchronous949.54 μs6.336 μs18.077 μs943.69 μs926.29 μs1,002.20 μs1.0020 KB
ReadAsync_NoBufferingAFTER104857616384Asynchronous1,195.21 μs3.070 μs8.507 μs1,194.94 μs1,179.45 μs1,218.92 μs1.2617 KB
WriteAsync_NoBufferingBEFORE104857616384Asynchronous2,965.91 μs40.616 μs114.558 μs2,952.10 μs2,677.93 μs3,244.55 μs1.0020 KB
WriteAsync_NoBufferingAFTER104857616384Asynchronous3,021.90 μs40.697 μs116.110 μs3,000.74 μs2,803.04 μs3,343.84 μs1.0217 KB
CopyToFileAsyncBEFORE1048576?None2,440.43 μs17.056 μs49.754 μs2,437.81 μs2,343.18 μs2,576.37 μs1.003 KB
CopyToFileAsyncAFTER1048576?None2,421.23 μs15.376 μs43.869 μs2,413.43 μs2,350.26 μs2,521.45 μs0.993 KB
CopyToFileAsyncBEFORE1048576?Asynchronous2,878.14 μs16.682 μs47.863 μs2,876.69 μs2,771.42 μs3,001.49 μs1.004 KB
CopyToFileAsyncAFTER1048576?Asynchronous2,769.09 μs15.649 μs43.882 μs2,760.85 μs2,691.56 μs2,885.67 μs0.964 KB
ReadAsyncBEFORE1048576004096None115,657.20 μs352.576 μs1,039.577 μs115,639.45 μs113,670.15 μs117,832.15 μs1.002,801 KB
ReadAsyncAFTER1048576004096None116,612.88 μs249.577 μs731.966 μs116,751.05 μs114,615.00 μs117,903.05 μs1.012,801 KB
WriteAsyncBEFORE1048576004096None205,378.59 μs7,785.258 μs22,462.248 μs194,584.50 μs184,125.30 μs268,874.60 μs1.005,005 KB
WriteAsyncAFTER1048576004096None209,453.56 μs10,355.532 μs30,043.276 μs194,064.20 μs173,778.40 μs293,593.30 μs1.035,005 KB
ReadAsyncBEFORE1048576004096Asynchronous327,225.97 μs1,532.011 μs4,346.059 μs326,564.00 μs320,187.10 μs339,508.20 μs1.007,801 KB
ReadAsyncAFTER1048576004096Asynchronous457,654.89 μs902.286 μs2,559.634 μs457,207.00 μs451,652.60 μs465,093.00 μs1.406,801 KB
WriteAsyncBEFORE1048576004096Asynchronous454,994.00 μs7,661.181 μs21,857.780 μs449,253.50 μs421,512.80 μs511,199.10 μs1.007,905 KB
WriteAsyncAFTER1048576004096Asynchronous600,076.62 μs10,088.501 μs29,268.570 μs592,035.20 μs559,265.50 μs677,403.80 μs1.326,905 KB
ReadAsync_NoBufferingBEFORE10485760016384None45,086.23 μs156.169 μs453.074 μs45,005.65 μs43,497.93 μs46,041.93 μs1.00701 KB
ReadAsync_NoBufferingAFTER10485760016384None42,133.53 μs174.922 μs457.740 μs42,093.18 μs41,187.75 μs43,731.75 μs0.93701 KB
WriteAsync_NoBufferingBEFORE10485760016384None61,814.53 μs685.277 μs1,887.453 μs61,419.69 μs59,079.38 μs68,035.43 μs1.00701 KB
WriteAsync_NoBufferingAFTER10485760016384None61,435.05 μs720.962 μs1,973.622 μs61,127.18 μs58,506.18 μs67,827.25 μs0.99701 KB
ReadAsync_NoBufferingBEFORE10485760016384Asynchronous97,220.19 μs667.147 μs1,881.701 μs96,464.10 μs94,386.65 μs102,972.80 μs1.001,951 KB
ReadAsync_NoBufferingAFTER10485760016384Asynchronous128,169.40 μs556.512 μs1,587.760 μs127,771.80 μs125,455.30 μs133,061.15 μs1.321,701 KB
WriteAsync_NoBufferingBEFORE10485760016384Asynchronous136,593.81 μs1,249.681 μs3,462.860 μs136,822.00 μs127,534.30 μs146,281.60 μs1.001,951 KB
WriteAsync_NoBufferingAFTER10485760016384Asynchronous169,656.11 μs2,245.877 μs6,515.696 μs168,284.30 μs157,404.20 μs189,342.10 μs1.251,701 KB
CopyToFileAsyncBEFORE104857600?None56,537.38 μs245.101 μs695.312 μs56,440.93 μs55,567.65 μs58,581.80 μs1.00177 KB
CopyToFileAsyncAFTER104857600?None56,184.32 μs275.348 μs785.585 μs56,116.43 μs54,790.00 μs58,574.72 μs0.99177 KB
CopyToFileAsyncBEFORE104857600?Asynchronous87,668.39 μs312.004 μs905.181 μs87,555.38 μs85,324.65 μs89,838.85 μs1.00246 KB
CopyToFileAsyncAFTER104857600?Asynchronous78,490.65 μs501.805 μs1,431.678 μs78,350.84 μs75,379.32 μs82,799.10 μs0.90214 KB

…Source only needed by Net5CompatFileStreamStrategy.
… one allocation and get the actual profiling improvement.
Rename FileStreamAwaitableProvider.
… review. Shorter filename for awaitable provider.
…trategy, and its _fileHandle can now be private.
@adamsitnik

Copy link
Copy Markdown
Member

Most results had allocation improvements, but worse speed when FileOptions = Asynchronous:

As discussed offline, such a regression needs to be solved before merging the PR.

I've even built your fork and run the benchmarks myself to be 100% sure, but unfortunately, I've confirmed the regression:

MethodToolchainfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsync_NoBuffering\after\corerun.exe104857616384Asynchronous838.1 us1.1217 KB
ReadAsync_NoBuffering\before\corerun.exe104857616384Asynchronous748.1 us1.0020 KB
ReadAsync_NoBuffering\after\corerun.exe10485760016384Asynchronous93,214.6 us1.121,700 KB
ReadAsync_NoBuffering\before\corerun.exe10485760016384Asynchronous83,445.4 us1.001,950 KB

If I can suggest something, ReadAsync_NoBuffering with fileSize=1048576 and options=Asynchronous should be the best benchmark for creating a repo app and profiling it. It's the simplest benchmark (buffering is not involved) and ReadAsync has a smaller deviation than WriteAsync. If you fail to find the regression with VS Profiler, you can use Perfview.

@adamsitnikadamsitnik added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Apr 14, 2021
@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

I compared the CPU usage before and after, and discovered we spend a considerable amount of time inside Overlapped.PerformIOCompletionCallback.

The new code (left) consumes 62% of the time calling ExecutionContext.RunInternal, while the old code (right) only took 3.82%.

Screenshot

image

The root cause seems to be this line, where we call _source.SetResult:

Screenshot

image

Which is adding up the time it takes to run the rest of the ReadAsync calls in the loop, including the disposal of the FileStream object in the main method.

There's a comment stating that ExecutionContext.RunInternal (part of the callstack) warning that the code is a very hot path:

Screenshot

image

I wasn't expecting all the continuation code to be counted as part of the asynchronous calls. I wonder if this has anything to do with the removal of RunContinuationsAsynchronously = true flag. I'll bring that flag back and compare the results.

cc @jozkee@adamsitnik@stephentoub

@davidfowl

Copy link
Copy Markdown
Member

We don't need to restore the ExecutionContext in the overlapped, that restore should be avoided in these APIs. The async state machine already does it. This is what you need #42549 😄

@stephentoub

stephentoub commented Apr 14, 2021

Copy link
Copy Markdown
Member

The new code (left) consumes 62% of the time calling ExecutionContext.RunInternal, while the old code (right) only took 3.82%.

That's inclusive (what "Total" means in the VS view). The callback is actually running the continuation now (because of RunContinuationsAsynchronously=false), rather than queueing it to be run on a different thread, so the 62% is inclusive of the actual app code.

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 know you're copying this logic from what was there before, but it seems to be unnecessarily complex, with lots of interlocked operations for transitioning from one state to another to another. If we're looking for places to reduce overheads, revisiting this whole implementation is likely a good place to start.

@adamsitnik

Copy link
Copy Markdown
Member

The callback is actually running the continuation now (because of RunContinuationsAsynchronously=false)

I've run all the async benchmarks for 3 configurations:

  • "before" (@carlossanlop fork main branch)
  • "false" (the current state of this PR as RunContinuationsAsynchronously is not set so it's false)
  • "true" ( _source.RunContinuationsAsynchronously = true; in ctor)

With the following settings:

  • 100 instead of 20 iterations (--minIterationCount 100 --maxIterationCount 101)
  • no outliers removal (--outliers DontRemove)

and confirmed that with _source.RunContinuationsAsynchronously = true; we have no regression in the microbenchmarks

@stephentoub should we:

  • set RunContinuationsAsynchronously = true; and include this PR in preview4
  • get some "real world" numbers from ASP.NET benchmarks (if there are none, write some and contribute to https://github.com/aspnet/benchmarks) and see how RunContinuationsAsynchronously setting affects non-micro benchmarks. This means preview5 for this PR.
Details
MethodToolchainfileSizeuserBufferSizeoptionsMeanStdDevMedianMinMaxRatioAllocated
ReadAsync\before\corerun.exe10241024Asynchronous82.69 us1.946 us82.22 us81.60 us94.90 us1.005 KB
ReadAsync\false\corerun.exe10241024Asynchronous78.76 us0.718 us78.56 us78.04 us82.13 us0.955 KB
ReadAsync\true\corerun.exe10241024Asynchronous83.28 us0.825 us83.12 us82.48 us89.82 us1.015 KB
WriteAsync\before\corerun.exe10241024Asynchronous404.05 us22.574 us400.81 us379.20 us494.53 us1.005 KB
WriteAsync\false\corerun.exe10241024Asynchronous400.76 us17.211 us397.80 us380.07 us472.21 us0.995 KB
WriteAsync\true\corerun.exe10241024Asynchronous405.20 us25.469 us401.47 us383.59 us510.74 us1.015 KB
ReadAsync\before\corerun.exe1048576512Asynchronous2,418.83 us55.684 us2,413.59 us2,361.95 us2,804.46 us1.0093 KB
ReadAsync\false\corerun.exe1048576512Asynchronous2,648.89 us73.366 us2,626.45 us2,550.29 us2,959.92 us1.1083 KB
ReadAsync\true\corerun.exe1048576512Asynchronous2,386.25 us9.985 us2,386.16 us2,356.79 us2,419.58 us0.9983 KB
WriteAsync\before\corerun.exe1048576512Asynchronous4,254.70 us277.778 us4,213.63 us3,967.55 us6,203.71 us1.0085 KB
WriteAsync\false\corerun.exe1048576512Asynchronous4,583.08 us183.949 us4,559.42 us4,317.29 us5,334.19 us1.0875 KB
WriteAsync\true\corerun.exe1048576512Asynchronous4,207.77 us179.340 us4,173.53 us3,896.65 us4,865.02 us0.9975 KB
ReadAsync\before\corerun.exe10485764096Asynchronous2,343.06 us35.339 us2,355.62 us2,225.73 us2,379.72 us1.0079 KB
ReadAsync\false\corerun.exe10485764096Asynchronous2,603.91 us40.916 us2,603.11 us2,513.55 us2,715.90 us1.1169 KB
ReadAsync\true\corerun.exe10485764096Asynchronous2,324.68 us38.225 us2,337.29 us2,226.14 us2,374.04 us0.9969 KB
WriteAsync\before\corerun.exe10485764096Asynchronous4,110.48 us265.902 us4,050.53 us3,844.25 us6,007.74 us1.0084 KB
WriteAsync\false\corerun.exe10485764096Asynchronous4,572.16 us320.658 us4,542.09 us4,256.30 us7,440.25 us1.1274 KB
WriteAsync\true\corerun.exe10485764096Asynchronous4,048.01 us184.553 us4,015.01 us3,739.28 us4,754.61 us0.9974 KB
ReadAsync_NoBuffering\before\corerun.exe104857616384Asynchronous747.53 us5.076 us747.87 us729.80 us755.69 us1.0020 KB
ReadAsync_NoBuffering\false\corerun.exe104857616384Asynchronous822.87 us6.877 us822.90 us809.94 us838.18 us1.1017 KB
ReadAsync_NoBuffering\true\corerun.exe104857616384Asynchronous726.82 us11.262 us726.44 us699.96 us754.36 us0.9717 KB
WriteAsync_NoBuffering\before\corerun.exe104857616384Asynchronous2,726.54 us136.755 us2,712.48 us2,534.40 us3,234.12 us1.0020 KB
WriteAsync_NoBuffering\false\corerun.exe104857616384Asynchronous2,761.05 us146.361 us2,792.11 us2,507.70 us3,188.16 us1.0117 KB
WriteAsync_NoBuffering\true\corerun.exe104857616384Asynchronous2,736.46 us194.495 us2,705.98 us2,415.11 us4,022.13 us1.0117 KB
ReadAsync\before\corerun.exe1048576004096Asynchronous250,490.85 us2,604.707 us250,256.45 us244,814.30 us256,607.90 us1.007,801 KB
ReadAsync\false\corerun.exe1048576004096Asynchronous278,260.89 us4,931.050 us278,009.45 us269,882.60 us297,338.40 us1.116,803 KB
ReadAsync\true\corerun.exe1048576004096Asynchronous252,682.05 us19,763.442 us250,702.50 us243,911.20 us447,534.10 us1.016,801 KB
WriteAsync\before\corerun.exe1048576004096Asynchronous370,893.59 us6,590.799 us369,781.05 us352,834.20 us390,366.40 us1.007,905 KB
WriteAsync\false\corerun.exe1048576004096Asynchronous397,619.03 us8,020.098 us397,052.95 us384,609.00 us446,559.70 us1.076,905 KB
WriteAsync\true\corerun.exe1048576004096Asynchronous358,128.05 us6,536.171 us357,755.45 us346,707.00 us382,735.70 us0.976,905 KB

@adamsitnikadamsitnik removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Apr 15, 2021

@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.

LGTM, thank you @carlossanlop !

@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

With the unit tests passing, the micro benchmarks having no regressions after reverting RunContinuationsAsynchronously to true, and having allocation improvements, I feel confident in merging this change to include it in Preview4, and will continue working on #50972 to add caching and improve this code for Preview5.

@carlossanlop
carlossanlop merged commit a38d0c2 into dotnet:mainApr 15, 2021
@carlossanlop
carlossanlop deleted the IValueTaskSource branch April 15, 2021 22:51
@ghostghost locked as resolved and limited conversation to collaborators May 15, 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.

5 participants

@carlossanlop@davidfowl@stephentoub@adamsitnik@jozkee
, '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: Use IValueTaskSource instead of TaskCompletionSource - #50802

Merged
carlossanlop merged 19 commits into
dotnet:mainfrom
carlossanlop:IValueTaskSource
Apr 15, 2021
Merged

FileStream rewrite: Use IValueTaskSource instead of TaskCompletionSource#50802
carlossanlop merged 19 commits into
dotnet:mainfrom
carlossanlop:IValueTaskSource

Conversation

@carlossanlop

Copy link
Copy Markdown
Contributor

The purpose of this PR is to remove a large allocation found in FileStream.ReadAsync and FileStream.WriteAsync, which we confirmed in our profiling investigation around the new FileStream rewrite code.

The allocation improvement can be achieved by creating a new type, called FileStreamValueTaskSource, that will have the same purpose as FileStreamCompletionSource, but instead of implementing TaskCompletionSource (which returns a Task to represent the asynchronous work) it implements IValueTaskSource, which returns a ValueTask.

The behavior was largely left untouched, except for the following differences:

  • Net5CompatFileStreamStrategy.Windows will be the only strategy that consumes the FileStreamCompletionSource, so it is now a private nested class.
  • One single new type implements both IValueTaskSource (used for write) and IValueTaskSource<int> (used for read).
  • The logic inside the callback and the cancellation token registration is the same, aside for the code that needs to report exceptions or return values.
  • We had interop errors redefined in multiple places.
  • The constants used by FileStreamCompletionSource were moved to FileStreamHelpers so they could be reused by the new type.

Unit tests

Manually ran unit tests from System.IO.FileSystem, System.IO, System.Runtime and System.Runtime.Extensions, they all passed. I made sure to run them with the Net5Compat flag turned off, to ensure my new code was executed.

Benchmarks

  • Ran the FileStream performance benchmarks against .NET 5.0, then against the code of this PR's baseline commit, and then against this PR's new code. I compared the results, and there are subtle improvements in some benchmark tests:
.NET 5.0 vs .NET 6.0 before this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_before_pr' --threshold 0.0001%

summary:
better: 39, geomean: 2.801
worse: 22, geomean: 2.098
total diff: 61

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.93882252.574348913.28
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.621003671.884633945.31bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.13257218.23805205.31several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.13286859.38896524.31
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.061795459.385498006.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.051868294.535689532.29
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)2.99254148.29758897.83
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.97283860.42843052.43
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.901844293.365353701.04several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.892209384.386395554.17bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.65301619.83798302.63
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.86557062.051035575.21bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.81478821.50868866.12
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.693479778.755891160.42several?
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.66543512.61903388.82bimodal
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.40168291900.00235314600.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.248197915.6310200393.75
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.215313143.756444323.96
Perf_FileStream.Flush(fileSize: 1024, options: None)1.183448111.254052132.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055473900.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.0956033900.0061052537.50
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.03133329050.00137489800.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.645489541.6747470.79
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.902811281.2543319.93
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.822571736.4644479.08
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.29876923.2641197.72
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.062463834750.00486578800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.94152781.6630909.09
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.72153731.8132593.67
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437889.31several?
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.98165696.6841632.18
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.97159526.5840152.10
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.50200423.3657204.05
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889181.93
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.55238920.2293543.27
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.50193629.0877357.59
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.443222202.501318286.88
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.39194808.8781591.76
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.36216916.9892096.58
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.24325116900.00145245400.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.157025697.923271982.50
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.10199647.8995019.35
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.04686776300.00336991400.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9519598723.0810074393.75
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)1.85508393800.00274360000.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.74796591200.00457934500.00bimodal
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.73156593600.0090313262.50
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.7022492768.7513248333.33
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64542213800.00331399500.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.615221529.173244615.63
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114593046.88
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742610.27
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09777023.21
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.191301925.001096436.88
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.12142000.45126451.42
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 512, options: None)1.11162435300.00146191400.00
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.10141476.75128484.00
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.10142083.81129048.19
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00113456750.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0288348175.0086894587.50
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 512, options: None)1.0192483275.0091193650.00
.NET 5.0 vs .NET 6.0 after this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_after_pr' --threshold 0.0001%
summary:
better: 36, geomean: 3.006
worse: 22, geomean: 2.080
total diff: 58

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.92882252.574341552.34
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.601003671.884615128.91bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)3.24283860.42919970.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)3.08254148.29783231.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.061868294.535710579.17
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.05286859.38874430.38
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.04257218.23780960.42
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)2.971795459.385329393.75
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.951844293.365432295.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.922209384.386445677.08
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.52301619.83758761.88
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.95557062.051087235.83
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.73543512.61937893.38bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.66478821.50793440.46bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.473479778.755113488.54several?
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.32168291900.00221660000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.235313143.756512252.08
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.228197915.6310012295.65
Perf_FileStream.Flush(fileSize: 1024, options: None)1.213448111.254163660.16
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1356033900.0063237650.00
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0950055150.0054423625.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0088983100.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.505489541.6747526.62
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.942811281.2543292.23
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.062571736.4645070.47
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.05876923.2641661.14
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.362463834750.00459292800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.95152781.6630850.74
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.75153731.8132349.21
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437926.07several?
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.98159526.5840096.31
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.96165696.6841824.65
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889051.28
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.56238920.2293276.60
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.53193629.0876383.11
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.45194808.8779416.04
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501376330.63
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.31216916.9893711.06
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.28325116900.00142880200.00
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)2.22200423.3690245.25
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.167025697.923250746.88
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.11508393800.00241032100.00
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.11199647.8994675.64
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.06686776300.00333108950.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9319598723.0810171781.82
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.74156593600.0090035450.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.65542213800.00329569650.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.645221529.173182177.50
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64796591200.00486630000.00
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.5222492768.7514797453.85
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114625460.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742460.71
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09781337.50
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.201301925.001085495.19
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.11141476.75127158.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.11142083.81128134.40
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.10142000.45129068.15
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00112942550.00

Profiling

I reused the same code we have in our benchmarks, but I executed it inside a console app that forces the consumption of the compiled runtime code.

You'll notice the top Task allocation went away, and the FileStreamCompletionSource allocations were substituted by FileStreamValueTaskSource allocations.

ReadAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,797
- System.String2,153
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,806
- System.String2,153
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]964
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

ReadAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,013
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,840
- System.String2,153
- System.Object[]955
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,836
- System.String2,144
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>64,235
- System.Threading.Tasks.Task<>64,015
- System.IO.FileStreamCompletionSource64,000
- System.SByte[]2,916
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,812
- System.SByte[]2,909
- System.String2,144
- System.Object[]964
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,798
- System.String2,153
- System.Object[]954
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,812
- System.String2,153
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]958
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,249
- System.IO.MemoryFileStreamCompletionSource32,000
- System.IO.FileStreamCompletionSource32,000
- System.SByte[]2,915
- System.String2,155
- System.Object[]952
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,246
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource32,000
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource32,000
- System.SByte[]2,893
- System.String2,144
- System.Object[]954
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.IO.FileStreamCompletionSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,963
- System.SByte[]2,915
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,830
- System.SByte[]2,912
- System.String2,144
- System.Object[]966
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Threading.Tasks.ValueTask.ValueTaskSourceAsTask249
- System.Threading.QueueUserWorkItemCallbackDefaultContext<>249

@adamsitnik@jozkee@jeffhandley@stephentoub

Also: Don't forget to ignore whitespace changes so it's easier to review the nested type's changes 😃

@ghost

ghost commented Apr 6, 2021

Copy link
Copy Markdown

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

Issue Details

The purpose of this PR is to remove a large allocation found in FileStream.ReadAsync and FileStream.WriteAsync, which we confirmed in our profiling investigation around the new FileStream rewrite code.

The allocation improvement can be achieved by creating a new type, called FileStreamValueTaskSource, that will have the same purpose as FileStreamCompletionSource, but instead of implementing TaskCompletionSource (which returns a Task to represent the asynchronous work) it implements IValueTaskSource, which returns a ValueTask.

The behavior was largely left untouched, except for the following differences:

  • Net5CompatFileStreamStrategy.Windows will be the only strategy that consumes the FileStreamCompletionSource, so it is now a private nested class.
  • One single new type implements both IValueTaskSource (used for write) and IValueTaskSource<int> (used for read).
  • The logic inside the callback and the cancellation token registration is the same, aside for the code that needs to report exceptions or return values.
  • We had interop errors redefined in multiple places.
  • The constants used by FileStreamCompletionSource were moved to FileStreamHelpers so they could be reused by the new type.

Unit tests

Manually ran unit tests from System.IO.FileSystem, System.IO, System.Runtime and System.Runtime.Extensions, they all passed. I made sure to run them with the Net5Compat flag turned off, to ensure my new code was executed.

Benchmarks

  • Ran the FileStream performance benchmarks against .NET 5.0, then against the code of this PR's baseline commit, and then against this PR's new code. I compared the results, and there are subtle improvements in some benchmark tests:
.NET 5.0 vs .NET 6.0 before this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_before_pr' --threshold 0.0001%

summary:
better: 39, geomean: 2.801
worse: 22, geomean: 2.098
total diff: 61

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.93882252.574348913.28
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.621003671.884633945.31bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.13257218.23805205.31several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.13286859.38896524.31
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.061795459.385498006.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.051868294.535689532.29
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)2.99254148.29758897.83
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.97283860.42843052.43
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.901844293.365353701.04several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.892209384.386395554.17bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.65301619.83798302.63
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.86557062.051035575.21bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.81478821.50868866.12
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.693479778.755891160.42several?
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.66543512.61903388.82bimodal
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.40168291900.00235314600.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.248197915.6310200393.75
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.215313143.756444323.96
Perf_FileStream.Flush(fileSize: 1024, options: None)1.183448111.254052132.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055473900.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.0956033900.0061052537.50
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.03133329050.00137489800.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.645489541.6747470.79
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.902811281.2543319.93
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.822571736.4644479.08
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.29876923.2641197.72
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.062463834750.00486578800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.94152781.6630909.09
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.72153731.8132593.67
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437889.31several?
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.98165696.6841632.18
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.97159526.5840152.10
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.50200423.3657204.05
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889181.93
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.55238920.2293543.27
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.50193629.0877357.59
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.443222202.501318286.88
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.39194808.8781591.76
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.36216916.9892096.58
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.24325116900.00145245400.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.157025697.923271982.50
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.10199647.8995019.35
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.04686776300.00336991400.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9519598723.0810074393.75
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)1.85508393800.00274360000.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.74796591200.00457934500.00bimodal
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.73156593600.0090313262.50
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.7022492768.7513248333.33
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64542213800.00331399500.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.615221529.173244615.63
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114593046.88
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742610.27
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09777023.21
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.191301925.001096436.88
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.12142000.45126451.42
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 512, options: None)1.11162435300.00146191400.00
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.10141476.75128484.00
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.10142083.81129048.19
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00113456750.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0288348175.0086894587.50
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 512, options: None)1.0192483275.0091193650.00
.NET 5.0 vs .NET 6.0 after this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_after_pr' --threshold 0.0001%
summary:
better: 36, geomean: 3.006
worse: 22, geomean: 2.080
total diff: 58

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.92882252.574341552.34
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.601003671.884615128.91bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)3.24283860.42919970.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)3.08254148.29783231.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.061868294.535710579.17
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.05286859.38874430.38
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.04257218.23780960.42
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)2.971795459.385329393.75
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.951844293.365432295.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.922209384.386445677.08
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.52301619.83758761.88
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.95557062.051087235.83
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.73543512.61937893.38bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.66478821.50793440.46bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.473479778.755113488.54several?
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.32168291900.00221660000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.235313143.756512252.08
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.228197915.6310012295.65
Perf_FileStream.Flush(fileSize: 1024, options: None)1.213448111.254163660.16
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1356033900.0063237650.00
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0950055150.0054423625.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0088983100.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.505489541.6747526.62
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.942811281.2543292.23
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.062571736.4645070.47
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.05876923.2641661.14
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.362463834750.00459292800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.95152781.6630850.74
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.75153731.8132349.21
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437926.07several?
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.98159526.5840096.31
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.96165696.6841824.65
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889051.28
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.56238920.2293276.60
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.53193629.0876383.11
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.45194808.8779416.04
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501376330.63
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.31216916.9893711.06
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.28325116900.00142880200.00
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)2.22200423.3690245.25
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.167025697.923250746.88
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.11508393800.00241032100.00
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.11199647.8994675.64
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.06686776300.00333108950.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9319598723.0810171781.82
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.74156593600.0090035450.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.65542213800.00329569650.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.645221529.173182177.50
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64796591200.00486630000.00
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.5222492768.7514797453.85
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114625460.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742460.71
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09781337.50
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.201301925.001085495.19
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.11141476.75127158.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.11142083.81128134.40
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.10142000.45129068.15
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00112942550.00

Profiling

I reused the same code we have in our benchmarks, but I executed it inside a console app that forces the consumption of the compiled runtime code.

You'll notice the top Task allocation went away, and the FileStreamCompletionSource allocations were substituted by FileStreamValueTaskSource allocations.

ReadAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,797
- System.String2,153
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,806
- System.String2,153
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]964
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

ReadAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,013
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,840
- System.String2,153
- System.Object[]955
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,836
- System.String2,144
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>64,235
- System.Threading.Tasks.Task<>64,015
- System.IO.FileStreamCompletionSource64,000
- System.SByte[]2,916
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,812
- System.SByte[]2,909
- System.String2,144
- System.Object[]964
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,798
- System.String2,153
- System.Object[]954
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,812
- System.String2,153
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]958
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,249
- System.IO.MemoryFileStreamCompletionSource32,000
- System.IO.FileStreamCompletionSource32,000
- System.SByte[]2,915
- System.String2,155
- System.Object[]952
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,246
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource32,000
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource32,000
- System.SByte[]2,893
- System.String2,144
- System.Object[]954
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.IO.FileStreamCompletionSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,963
- System.SByte[]2,915
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,830
- System.SByte[]2,912
- System.String2,144
- System.Object[]966
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Threading.Tasks.ValueTask.ValueTaskSourceAsTask249
- System.Threading.QueueUserWorkItemCallbackDefaultContext<>249

@adamsitnik@jozkee@jeffhandley@stephentoub

Also: Don't forget to ignore whitespace changes so it's easier to review the nested type's changes 😃
Author:carlossanlop
Assignees:carlossanlop
Labels:

area-System.IO

Milestone:6.0.0

@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

Tagging subscribers to this area: @carlossanlop

Yes, thank you bot... 😆

@jozkeejozkee 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.

Otherwise; LGTM, will approve if/when comments are resolved, most of them are nits and/or questions.

Comment on lines +17 to +22
internal const long NoResult = 0;
internal const long ResultSuccess = (long)1 << 32;
internal const long ResultError = (long)2 << 32;
internal const long RegisteringCancellation = (long)4 << 32;
internal const long CompletedCallback = (long)8 << 32;
internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32;

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.

Consider prefixing this constants with AsyncCompletionSource_ or something alike since these names are too ambiguous or keep them in their respective classes regardless of them being duplicated.

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.

or put them into a nested static class called ReturnCodes:

internalstaticpartialclassFileStreamHelpers{internalstaticclassReturnCodes{internalconstlongNoResult=0;internalconstlongResultSuccess=(long)1<<32;internalconstlongResultError=(long)2<<32;internalconstlongRegisteringCancellation=(long)4<<32;internalconstlongCompletedCallback=(long)8<<32;internalconstulongResultMask=((ulong)uint.MaxValue)<<32;}

but it's a nit (not a blocker)

Comment on lines +66 to +67
_strategy.CompareExchangeCurrentOverlappedOwner(this, null) == null ?
_strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!) : // allocated when buffer was created, and buffer is non-null

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.

Suggested change
_strategy.CompareExchangeCurrentOverlappedOwner(this,null)==null?
_strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!):// allocated when buffer was created, and buffer is non-null
strategy.CompareExchangeCurrentOverlappedOwner(this,null)==null?
strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!):// allocated when buffer was created, and buffer is non-null

// be directly the AwaitableProvider that's completing (in the case where the preallocated
// overlapped was already in use by another operation).
object? state = ThreadPoolBoundHandle.GetNativeOverlappedState(pOverlapped);
Debug.Assert(state is (AsyncWindowsFileStreamStrategy or FileStreamValueTaskSource));

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.

Suggested change
Debug.Assert(stateis(AsyncWindowsFileStreamStrategy or FileStreamValueTaskSource));
Debug.Assert(stateisAsyncWindowsFileStreamStrategy or FileStreamValueTaskSource);

int errorCode = unchecked((int)(packedResult & uint.MaxValue));
if (errorCode == Interop.Errors.ERROR_OPERATION_ABORTED)
{
_source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(new OperationCanceledException(cancellationToken.IsCancellationRequested ? cancellationToken : new CancellationToken(canceled: true))));

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.

Suggested change
_source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(newOperationCanceledException(cancellationToken.IsCancellationRequested?cancellationToken:newCancellationToken(canceled:true))));
Exceptione=newOperationCanceledException(cancellationToken.IsCancellationRequested?cancellationToken:newCancellationToken(canceled:true));
e.SetCurrentStackTrace();
_source.SetException(e);

}

private void Cancel(CancellationToken token)
{

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.

Missing comment and asserts:

// WARNING: This may potentially be called under a lock (during cancellation registration)
Debug.Assert(stateisFileStreamCompletionSource,"Unknown state passed to cancellation");
FileStreamCompletionSourcecompletionSource=(FileStreamCompletionSource)state;
Debug.Assert(completionSource._overlapped!=null&&!completionSource.Task.IsCompleted,"IO should not have completed yet");

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The first assert is not needed because the method argument is not an object? state.

I'll add the assert that checks for overlapped an completed status.

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.

The state param could be added to this method and allows you to conserve the first Debug.Assert.

long packedResult = Interlocked.CompareExchange(ref _result, FileStreamHelpers.RegisteringCancellation, FileStreamHelpers.NoResult);
if (packedResult == FileStreamHelpers.NoResult)
{
_cancellationRegistration = cancellationToken.UnsafeRegister(static (s, token) => ((FileStreamValueTaskSource)s!).Cancel(token), this);

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.

Could we pass the callback on a similar fashion to FileStreamCompletionSource

_cancellationRegistration=cancellationToken.UnsafeRegister(cancelCallback,this);

Or why did you opted for this way instead?

@carlossanlopcarlossanlopApr 8, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It made more sense to pass this as the second argument of UnsafeRegister, which ensures it's a ValueTaskSource instance, and saves us from the check of the object? state parameter like in the old code. The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

We have a similar example in System.Threading.Channels.

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.

It made more sense to pass this as the second argument of UnsafeRegister

FileStreamCompletionSource was also passing this as 2nd argument.

The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

Yes, seems that it was caching the method group to avoid allocating everytime it was passed to UnsafeRegister.
However I think you can avoid the allocation without caching yourself by doing this:

_cancellationRegistration=cancellationToken.UnsafeRegister((s,token)=>Cancel(s,token),this);

Just a suggestion, not something that you should block on.

@davidfowl

Copy link
Copy Markdown
Member

This is the change I was waiting for 👏🏾👏🏾

@davidfowl

Copy link
Copy Markdown
Member

I haven't looked but I assume we'll reuse the operation objects if there's no concurrency? This is what sockets do today so in the 95% case of no overlapping writes or reads, there'll be a single reusable allocation on the FileStream

@stephentoub

Copy link
Copy Markdown
Member

This is the change I was waiting for

It's not, actually. You want the next one where the IValueTaskSource instance is cached.

}
// else: Some other thread stole the result, so now it is responsible to finish the callback
}
// else: Some other thread is registering a cancellation, so it *must* finish the callback

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.

All this logic looks more complicated than it needs to be. I realize you're just following the same flow as what was already there, so nothing needs to be changed in this PR. But we should revisit all of this synchronization later; I suspect we can do it more succinctly and efficiently.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I opened an issue to follow up on this and on the caching of the IValueTaskSource.
#50972

@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 before we merge I would like to see the following benchmark results:

.NET 6 with new strategy enabled: before and after your change. (not .NET 5 vs 6 as there are too many moving targets)

You should be able to achieve that by running the following command in the perf repo;

git clone https://github.com/dotnet/performance.git
cd performance\src\benchmarks\micro
dotnet run -c Release -f net6.0 --filter *Perf_FileStream* --coreRun $pathToCoreRunWithoutYourChanges $pathToCoreRunWithYourChanges --envVars "DOTNET_SYSTEM_IO_USENET5COMPATFILESTREAM:0"

/// <see cref="MemoryHandle"/> when the operation has completed. This should only be used
/// when memory doesn't wrap a byte[].
/// </summary>
private sealed class MemoryAwaitableProvider : FileStreamValueTaskSource

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.

This should only be used when memory doesn't wrap a byte[].

I just realized that we don't have benchmarks for this scenario as Memory used in our micro-benchmarks always wraps a byte array. Could you please add such benchmarks to https://github.com/dotnet/performance/blob/main/src/benchmarks/micro/libraries/System.IO.FileSystem/Perf.FileStream.cs ? One test case should be enough as this should be rare.

@carlossanlopcarlossanlopApr 7, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Some of the benchmarks do consume this type. If you look at some of the profiling tests I executed, you'll see that MemoryValueTaskSource shows up a few times. The profiling tests reuse the code we had in our benchmarks.

I don't mind adding an explicit one though. Dou you want me to add one anyway?

Comment on lines +17 to +22
internal const long NoResult = 0;
internal const long ResultSuccess = (long)1 << 32;
internal const long ResultError = (long)2 << 32;
internal const long RegisteringCancellation = (long)4 << 32;
internal const long CompletedCallback = (long)8 << 32;
internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32;

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.

or put them into a nested static class called ReturnCodes:

internalstaticpartialclassFileStreamHelpers{internalstaticclassReturnCodes{internalconstlongNoResult=0;internalconstlongResultSuccess=(long)1<<32;internalconstlongResultError=(long)2<<32;internalconstlongRegisteringCancellation=(long)4<<32;internalconstlongCompletedCallback=(long)8<<32;internalconstulongResultMask=((ulong)uint.MaxValue)<<32;}

but it's a nit (not a blocker)

Comment on lines 62 to 63

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.

Did you run the perf. benchmarks after doing this change and did you notice any impact?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Pending.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Confirmed - There is a slight improvement.

I made sure to compare apples to apples: I used my latest commit, and built it setting RunContinuationsAsynchronously to true, then to false. I compared it both times against our code in .NET 5.0.

The improvement is clear when setting it to false (or not setting it at all).

RunContinuationsAsynchronously = true

❯ dotnet run -c release --base "D:\fs50" --diff "D:\fs60_NEW_ON_IVTS_asynctrue" --threshold 0.0001%
summary:
better: 24, geomean: 3.402
worse: 19, geomean: 2.244
total diff: 43

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)5.49882252.574840275.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)5.121003671.885136350.00bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)4.44257218.231141827.01
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)4.08286859.381171600.96
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.251795459.385832233.33
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)3.111844293.365741847.92several?
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.111868294.535807825.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)3.082209384.386796443.75
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)2.46557062.051371566.15
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)2.33478821.501116033.48
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)2.28543512.611239956.25bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.613479778.755612725.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.448197915.6311814575.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.33168291900.00224355000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.335313143.757063218.75
Perf_FileStream.Flush(fileSize: 1024, options: None)1.253448111.254313832.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1250055150.0056309025.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1256033900.0062803900.00
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.12133329050.00148833000.00bimodal
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)117.075489541.6746889.31
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)65.932811281.2542642.74
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.842571736.4644462.45
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.09876923.2641586.04
Perf_FileStream.OpenClose(fileSize: 1024, options: None)5.00152781.6630548.62
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.76153731.8132314.04
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)4.032463834750.00611183400.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.93165696.6842208.34
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.52200423.3656865.52
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.62230670.3887990.73
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.58238920.2292478.58
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501377939.84
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.87156593600.0083681962.50
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.6619598723.0811803641.67
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.6322492768.7513818900.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.577025697.924487121.88
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)1.47686776300.00466936500.00bimodal
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55743825.45
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.24975146.09784156.88
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.195221529.174387085.94
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.181301925.001103803.79
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.04116486775.00112407200.00
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.0321953590.9121241122.73
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0087467100.00
RunContinuationsAsynchronously = false

dotnet run -c release --base "D:\fs50" --diff "D:\fs60_NEW_ON_IVTS_asyncfalse" --threshold 0.0001%
summary:
better: 23, geomean: 3.592
worse: 20, geomean: 2.135
total diff: 43

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)5.36882252.574730875.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)5.051003671.885071912.50bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)4.25257218.231094164.38several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)4.05286859.381161160.27
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.231795459.385791481.25
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)3.101844293.365709820.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)3.092209384.386836504.17
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.071868294.535733883.33
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)2.46557062.051368034.90
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)2.31478821.501103696.25
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)2.24543512.611218234.38bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.603479778.755572682.29
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.428197915.6311634850.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.33168291900.00223141800.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.315313143.756949831.25
Perf_FileStream.Flush(fileSize: 1024, options: None)1.263448111.254332915.63
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.13133329050.00150223800.00bimodal
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055699050.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1056033900.0061710550.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0088348175.0088759625.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)116.975489541.6746929.42
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)65.762811281.2542753.29
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.562571736.4644676.15
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.52876923.2640756.47
Perf_FileStream.OpenClose(fileSize: 1024, options: None)5.06152781.6630216.63
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.81153731.8131975.57
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)4.052463834750.00608864800.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.99165696.6841526.73
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.54200423.3656610.08
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.62230670.3887877.10
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.59238920.2292291.75
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.403222202.501345116.25
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.86156593600.0083969875.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.6419598723.0811925173.68
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.6222492768.7513859764.71
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.567025697.924493881.25
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)1.46686776300.00470158300.00bimodal
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.32987460.55750294.64
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.24975146.09784056.56
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.171301925.001108880.36
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.175221529.174470673.44
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.0721953590.9120580725.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.01116486775.00

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Here is the result comparison if we compare the benchmark results of setting it to true vs false:

base: RunContinuationsAsynchronously=true, diff: RunContinuationsAsynchronously=false

dotnet run -c release --base "D:\fs60_NEW_ON_IVTS_asynctrue" --diff "D:\fs60_NEW_ON_IVTS_asyncfalse" --threshold 0.0001%
summary:
better: 14, geomean: 1.018
worse: 11, geomean: 1.017
total diff: 25

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.WriteAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0567143550.0070259187.50
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.02112407200.00115107550.00
Perf_FileStream.Read_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.02240016.67244921.93
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.024387085.944470673.44
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0187467100.0088759625.00
Perf_FileStream.WriteAsync_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.0121684354.5521982809.09
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: Asynchronous)1.011236709.901253242.19
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 1024, options: None)1.0139289.5439730.73
Perf_FileStream.Write_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.0121445427.2721646650.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.01743825.45750294.64
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)1.0044462.4544676.15
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.WriteByte(fileSize: 1024, options: None)1.041141827.011094164.38several?
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0345688612.5044474575.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.021377939.841345116.25
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)1.024840275.004730875.00
Perf_FileStream.SeekForward(fileSize: 1024, options: None)1.0241586.0440756.47
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: Asynchronous)1.02130849050.00128463775.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)1.0242208.3441526.73
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.0211814575.0011634850.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)1.015807825.005733883.33
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)1.015136350.005071912.50
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0156309025.0055699050.00
Perf_FileStream.Read_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0133747814.293338345.14
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)1.0132314.0431975.57
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 1024, options: Asynchronous)1.0081988.0981641.78

long packedResult = Interlocked.CompareExchange(ref _result, FileStreamHelpers.RegisteringCancellation, FileStreamHelpers.NoResult);
if (packedResult == FileStreamHelpers.NoResult)
{
_cancellationRegistration = cancellationToken.UnsafeRegister(static (s, token) => ((FileStreamValueTaskSource)s!).Cancel(token), this);

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.

It made more sense to pass this as the second argument of UnsafeRegister

FileStreamCompletionSource was also passing this as 2nd argument.

The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

Yes, seems that it was caching the method group to avoid allocating everytime it was passed to UnsafeRegister.
However I think you can avoid the allocation without caching yourself by doing this:

_cancellationRegistration=cancellationToken.UnsafeRegister((s,token)=>Cancel(s,token),this);

Just a suggestion, not something that you should block on.

Comment on lines 120 to 122

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.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I tried, but a ValueTask does not have a Result property, like a ValueTask<T> does, because it returns void. So I'm not sure if it's possible to apply an equivalent pattern.

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.

publicoverridevoidWrite(byte[]buffer,intoffset,intcount){ValueTaskvt=WriteAsyncInternal(newReadOnlyMemory<byte>(buffer,offset,count),CancellationToken.None);if(vt.IsCompleted){vt.GetAwaiter().GetResult();}else{vt.AsTask().GetAwaiter().GetResult();}}

That said, it's not much of an issue for this case. The read side is an issue because AsTask() will almost certainly need to allocate a new Task<int> in order to hand back the result, but for the write side, AsTask() on an already completed ValueTask will just return Task.Completed, assuming the operation completed successfully. So the only time the existing code for the write side will allocate is when the operation was canceled or faulted, in which case we're about to throw an exception anyway, and the extra task allocation won't really matter. Up to you whether you want to change it.

@jozkeejozkee 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.

LGTM; thanks Carlos!

@adamsitnik

Copy link
Copy Markdown
Member

I compared it both times against our code in .NET 5.0.

Comparison against 5.0 is a comparison of all of the changes that were merged to main since we have snapped 5.0 with your change against 5.0. What we care about is how your change (and nothing else) is affecting the perf, so we need to isolate the runs to only your change.

To be able to do that you need to run the benchmarks using two CoreRuns:

  • as a baseline the corerun that was built from main when you have created a new branch, without any of your changes
  • your branch with your changes

My personal workflow:

  1. When I create a new branch, I perform a full build in Release:
build -c Release -subset clr+libs
  1. I copy the folder that contains CoreRun.exe (runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0) to a separate folder. Typically I call it "before".
  2. I apply some changes and perform an incremental Release build. In case of System.Private.CoreLib.dll it's:
build -c Release -subset clr.corelib+clr.nativecorelib+libs.PreTest
  1. I run the benchmarks using the old and new CoreRun.exe:
dotnet run -c Release -f net6.0 --filter *ABC* --corerun C:\Projects\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\before\CoreRun.exe C:\Projects\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe

@carlossanlop

carlossanlop commented Apr 13, 2021

Copy link
Copy Markdown
ContributorAuthor
  • as a baseline the corerun that was built from main when you have created a new branch, without any of your changes
  • your branch with your changes

@adamsitnik@jozkee here's the table comparing the before (original) and after of this PR. Most results had allocation improvements, but worse speed when FileOptions = Asynchronous:

Results

Command:

 dotnet run
-c Release
-f net6.0
--corerun
D:\runtime2\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe
D:\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe
--filter System.IO.Tests.Perf_FileStream.*Async*
--statisticalTest 3ms
--minIterationCount 100
--maxIterationCount 101
--artifacts "D:\finalresults"

Note: The runtime2 corerun contains the baseline commit before my changes, the runtime corerun contains this PR's changes.

MethodToolchainfileSizeuserBufferSizeoptionsMeanErrorStdDevMedianMinMaxRatioAllocated
ReadAsyncBEFORE10241024None78.69 μs0.255 μs0.736 μs78.53 μs77.61 μs80.66 μs1.005 KB
ReadAsyncAFTER10241024None79.10 μs0.237 μs0.673 μs79.08 μs76.96 μs80.71 μs1.015 KB
WriteAsyncBEFORE10241024None316.97 μs2.512 μs6.919 μs315.92 μs305.11 μs339.35 μs1.004 KB
WriteAsyncAFTER10241024None300.13 μs5.643 μs15.730 μs297.59 μs272.46 μs348.15 μs0.954 KB
ReadAsyncBEFORE10241024Asynchronous94.13 μs0.259 μs0.731 μs94.02 μs92.27 μs96.16 μs1.005 KB
ReadAsyncAFTER10241024Asynchronous85.48 μs0.842 μs2.291 μs85.11 μs82.58 μs92.09 μs0.915 KB
WriteAsyncBEFORE10241024Asynchronous292.63 μs5.659 μs15.869 μs290.59 μs264.25 μs338.98 μs1.005 KB
WriteAsyncAFTER10241024Asynchronous324.35 μs13.906 μs39.222 μs311.27 μs274.34 μs428.64 μs1.115 KB
FlushAsyncBEFORE1024?None4,606.81 μs46.736 μs133.341 μs4,633.92 μs4,374.04 μs4,952.63 μs1.00269 KB
FlushAsyncAFTER1024?None4,587.88 μs58.046 μs168.403 μs4,638.98 μs4,322.79 μs5,034.48 μs1.00269 KB
CopyToFileAsyncBEFORE1024?None474.57 μs5.497 μs15.141 μs474.66 μs440.31 μs520.31 μs1.005 KB
CopyToFileAsyncAFTER1024?None488.94 μs3.631 μs10.061 μs488.62 μs469.93 μs518.76 μs1.035 KB
FlushAsyncBEFORE1024?Asynchronous14,680.35 μs150.967 μs437.982 μs14,637.76 μs13,623.50 μs15,645.01 μs1.00301 KB
FlushAsyncAFTER1024?Asynchronous20,333.22 μs116.277 μs339.185 μs20,299.93 μs19,724.25 μs21,148.73 μs1.39261 KB
CopyToFileAsyncBEFORE1024?Asynchronous520.97 μs5.982 μs16.674 μs519.35 μs490.05 μs572.29 μs1.006 KB
CopyToFileAsyncAFTER1024?Asynchronous523.30 μs6.294 μs17.650 μs520.91 μs479.14 μs583.85 μs1.006 KB
ReadAsyncBEFORE1048576512None1,375.28 μs5.930 μs17.015 μs1,376.60 μs1,340.44 μs1,413.73 μs1.0085 KB
ReadAsyncAFTER1048576512None1,347.16 μs5.128 μs14.958 μs1,345.14 μs1,323.57 μs1,382.14 μs0.9885 KB
WriteAsyncBEFORE1048576512None3,530.03 μs38.149 μs108.222 μs3,521.78 μs3,299.91 μs3,823.63 μs1.0076 KB
WriteAsyncAFTER1048576512None3,473.85 μs47.943 μs134.438 μs3,469.00 μs3,222.90 μs3,832.17 μs0.9876 KB
ReadAsyncBEFORE1048576512Asynchronous3,278.98 μs30.616 μs87.844 μs3,257.98 μs3,151.46 μs3,507.69 μs1.0093 KB
ReadAsyncAFTER1048576512Asynchronous4,399.34 μs16.153 μs47.119 μs4,401.64 μs4,294.71 μs4,531.75 μs1.3483 KB
WriteAsyncBEFORE1048576512Asynchronous5,304.10 μs73.935 μs214.500 μs5,309.27 μs4,771.82 μs5,852.43 μs1.0085 KB
WriteAsyncAFTER1048576512Asynchronous6,506.15 μs94.189 μs271.756 μs6,505.61 μs6,042.59 μs7,132.02 μs1.2375 KB
ReadAsyncBEFORE10485764096None1,090.98 μs6.156 μs18.152 μs1,096.14 μs1,039.20 μs1,120.19 μs1.0029 KB
ReadAsyncAFTER10485764096None1,084.03 μs8.601 μs25.225 μs1,086.13 μs1,042.07 μs1,131.85 μs0.9929 KB
WriteAsyncBEFORE10485764096None3,247.60 μs43.259 μs124.117 μs3,243.69 μs2,954.42 μs3,556.50 μs1.0055 KB
WriteAsyncAFTER10485764096None3,250.35 μs36.664 μs100.985 μs3,234.77 μs3,039.74 μs3,505.95 μs1.0055 KB
ReadAsyncBEFORE10485764096Asynchronous3,273.61 μs17.591 μs50.753 μs3,264.72 μs3,190.18 μs3,410.97 μs1.0079 KB
ReadAsyncAFTER10485764096Asynchronous4,547.38 μs73.438 μs216.535 μs4,398.34 μs4,327.10 μs5,035.98 μs1.3969 KB
WriteAsyncBEFORE10485764096Asynchronous5,384.46 μs83.792 μs240.415 μs5,346.69 μs4,897.35 μs5,955.92 μs1.0084 KB
WriteAsyncAFTER10485764096Asynchronous6,845.63 μs88.794 μs256.192 μs6,883.75 μs6,079.92 μs7,286.73 μs1.2774 KB
ReadAsync_NoBufferingBEFORE104857616384None406.34 μs2.413 μs7.113 μs403.28 μs387.16 μs419.08 μs1.007 KB
ReadAsync_NoBufferingAFTER104857616384None380.35 μs3.413 μs10.063 μs378.36 μs363.73 μs402.82 μs0.947 KB
WriteAsync_NoBufferingBEFORE104857616384None2,545.17 μs30.174 μs83.613 μs2,526.42 μs2,397.67 μs2,776.47 μs1.007 KB
WriteAsync_NoBufferingAFTER104857616384None2,537.77 μs27.489 μs76.628 μs2,528.02 μs2,421.65 μs2,750.68 μs1.007 KB
ReadAsync_NoBufferingBEFORE104857616384Asynchronous949.54 μs6.336 μs18.077 μs943.69 μs926.29 μs1,002.20 μs1.0020 KB
ReadAsync_NoBufferingAFTER104857616384Asynchronous1,195.21 μs3.070 μs8.507 μs1,194.94 μs1,179.45 μs1,218.92 μs1.2617 KB
WriteAsync_NoBufferingBEFORE104857616384Asynchronous2,965.91 μs40.616 μs114.558 μs2,952.10 μs2,677.93 μs3,244.55 μs1.0020 KB
WriteAsync_NoBufferingAFTER104857616384Asynchronous3,021.90 μs40.697 μs116.110 μs3,000.74 μs2,803.04 μs3,343.84 μs1.0217 KB
CopyToFileAsyncBEFORE1048576?None2,440.43 μs17.056 μs49.754 μs2,437.81 μs2,343.18 μs2,576.37 μs1.003 KB
CopyToFileAsyncAFTER1048576?None2,421.23 μs15.376 μs43.869 μs2,413.43 μs2,350.26 μs2,521.45 μs0.993 KB
CopyToFileAsyncBEFORE1048576?Asynchronous2,878.14 μs16.682 μs47.863 μs2,876.69 μs2,771.42 μs3,001.49 μs1.004 KB
CopyToFileAsyncAFTER1048576?Asynchronous2,769.09 μs15.649 μs43.882 μs2,760.85 μs2,691.56 μs2,885.67 μs0.964 KB
ReadAsyncBEFORE1048576004096None115,657.20 μs352.576 μs1,039.577 μs115,639.45 μs113,670.15 μs117,832.15 μs1.002,801 KB
ReadAsyncAFTER1048576004096None116,612.88 μs249.577 μs731.966 μs116,751.05 μs114,615.00 μs117,903.05 μs1.012,801 KB
WriteAsyncBEFORE1048576004096None205,378.59 μs7,785.258 μs22,462.248 μs194,584.50 μs184,125.30 μs268,874.60 μs1.005,005 KB
WriteAsyncAFTER1048576004096None209,453.56 μs10,355.532 μs30,043.276 μs194,064.20 μs173,778.40 μs293,593.30 μs1.035,005 KB
ReadAsyncBEFORE1048576004096Asynchronous327,225.97 μs1,532.011 μs4,346.059 μs326,564.00 μs320,187.10 μs339,508.20 μs1.007,801 KB
ReadAsyncAFTER1048576004096Asynchronous457,654.89 μs902.286 μs2,559.634 μs457,207.00 μs451,652.60 μs465,093.00 μs1.406,801 KB
WriteAsyncBEFORE1048576004096Asynchronous454,994.00 μs7,661.181 μs21,857.780 μs449,253.50 μs421,512.80 μs511,199.10 μs1.007,905 KB
WriteAsyncAFTER1048576004096Asynchronous600,076.62 μs10,088.501 μs29,268.570 μs592,035.20 μs559,265.50 μs677,403.80 μs1.326,905 KB
ReadAsync_NoBufferingBEFORE10485760016384None45,086.23 μs156.169 μs453.074 μs45,005.65 μs43,497.93 μs46,041.93 μs1.00701 KB
ReadAsync_NoBufferingAFTER10485760016384None42,133.53 μs174.922 μs457.740 μs42,093.18 μs41,187.75 μs43,731.75 μs0.93701 KB
WriteAsync_NoBufferingBEFORE10485760016384None61,814.53 μs685.277 μs1,887.453 μs61,419.69 μs59,079.38 μs68,035.43 μs1.00701 KB
WriteAsync_NoBufferingAFTER10485760016384None61,435.05 μs720.962 μs1,973.622 μs61,127.18 μs58,506.18 μs67,827.25 μs0.99701 KB
ReadAsync_NoBufferingBEFORE10485760016384Asynchronous97,220.19 μs667.147 μs1,881.701 μs96,464.10 μs94,386.65 μs102,972.80 μs1.001,951 KB
ReadAsync_NoBufferingAFTER10485760016384Asynchronous128,169.40 μs556.512 μs1,587.760 μs127,771.80 μs125,455.30 μs133,061.15 μs1.321,701 KB
WriteAsync_NoBufferingBEFORE10485760016384Asynchronous136,593.81 μs1,249.681 μs3,462.860 μs136,822.00 μs127,534.30 μs146,281.60 μs1.001,951 KB
WriteAsync_NoBufferingAFTER10485760016384Asynchronous169,656.11 μs2,245.877 μs6,515.696 μs168,284.30 μs157,404.20 μs189,342.10 μs1.251,701 KB
CopyToFileAsyncBEFORE104857600?None56,537.38 μs245.101 μs695.312 μs56,440.93 μs55,567.65 μs58,581.80 μs1.00177 KB
CopyToFileAsyncAFTER104857600?None56,184.32 μs275.348 μs785.585 μs56,116.43 μs54,790.00 μs58,574.72 μs0.99177 KB
CopyToFileAsyncBEFORE104857600?Asynchronous87,668.39 μs312.004 μs905.181 μs87,555.38 μs85,324.65 μs89,838.85 μs1.00246 KB
CopyToFileAsyncAFTER104857600?Asynchronous78,490.65 μs501.805 μs1,431.678 μs78,350.84 μs75,379.32 μs82,799.10 μs0.90214 KB

…Source only needed by Net5CompatFileStreamStrategy.
… one allocation and get the actual profiling improvement.
Rename FileStreamAwaitableProvider.
… review. Shorter filename for awaitable provider.
…trategy, and its _fileHandle can now be private.
@adamsitnik

Copy link
Copy Markdown
Member

Most results had allocation improvements, but worse speed when FileOptions = Asynchronous:

As discussed offline, such a regression needs to be solved before merging the PR.

I've even built your fork and run the benchmarks myself to be 100% sure, but unfortunately, I've confirmed the regression:

MethodToolchainfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsync_NoBuffering\after\corerun.exe104857616384Asynchronous838.1 us1.1217 KB
ReadAsync_NoBuffering\before\corerun.exe104857616384Asynchronous748.1 us1.0020 KB
ReadAsync_NoBuffering\after\corerun.exe10485760016384Asynchronous93,214.6 us1.121,700 KB
ReadAsync_NoBuffering\before\corerun.exe10485760016384Asynchronous83,445.4 us1.001,950 KB

If I can suggest something, ReadAsync_NoBuffering with fileSize=1048576 and options=Asynchronous should be the best benchmark for creating a repo app and profiling it. It's the simplest benchmark (buffering is not involved) and ReadAsync has a smaller deviation than WriteAsync. If you fail to find the regression with VS Profiler, you can use Perfview.

@adamsitnikadamsitnik added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Apr 14, 2021
@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

I compared the CPU usage before and after, and discovered we spend a considerable amount of time inside Overlapped.PerformIOCompletionCallback.

The new code (left) consumes 62% of the time calling ExecutionContext.RunInternal, while the old code (right) only took 3.82%.

Screenshot

image

The root cause seems to be this line, where we call _source.SetResult:

Screenshot

image

Which is adding up the time it takes to run the rest of the ReadAsync calls in the loop, including the disposal of the FileStream object in the main method.

There's a comment stating that ExecutionContext.RunInternal (part of the callstack) warning that the code is a very hot path:

Screenshot

image

I wasn't expecting all the continuation code to be counted as part of the asynchronous calls. I wonder if this has anything to do with the removal of RunContinuationsAsynchronously = true flag. I'll bring that flag back and compare the results.

cc @jozkee@adamsitnik@stephentoub

@davidfowl

Copy link
Copy Markdown
Member

We don't need to restore the ExecutionContext in the overlapped, that restore should be avoided in these APIs. The async state machine already does it. This is what you need #42549 😄

@stephentoub

stephentoub commented Apr 14, 2021

Copy link
Copy Markdown
Member

The new code (left) consumes 62% of the time calling ExecutionContext.RunInternal, while the old code (right) only took 3.82%.

That's inclusive (what "Total" means in the VS view). The callback is actually running the continuation now (because of RunContinuationsAsynchronously=false), rather than queueing it to be run on a different thread, so the 62% is inclusive of the actual app code.

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 know you're copying this logic from what was there before, but it seems to be unnecessarily complex, with lots of interlocked operations for transitioning from one state to another to another. If we're looking for places to reduce overheads, revisiting this whole implementation is likely a good place to start.

@adamsitnik

Copy link
Copy Markdown
Member

The callback is actually running the continuation now (because of RunContinuationsAsynchronously=false)

I've run all the async benchmarks for 3 configurations:

  • "before" (@carlossanlop fork main branch)
  • "false" (the current state of this PR as RunContinuationsAsynchronously is not set so it's false)
  • "true" ( _source.RunContinuationsAsynchronously = true; in ctor)

With the following settings:

  • 100 instead of 20 iterations (--minIterationCount 100 --maxIterationCount 101)
  • no outliers removal (--outliers DontRemove)

and confirmed that with _source.RunContinuationsAsynchronously = true; we have no regression in the microbenchmarks

@stephentoub should we:

  • set RunContinuationsAsynchronously = true; and include this PR in preview4
  • get some "real world" numbers from ASP.NET benchmarks (if there are none, write some and contribute to https://github.com/aspnet/benchmarks) and see how RunContinuationsAsynchronously setting affects non-micro benchmarks. This means preview5 for this PR.
Details
MethodToolchainfileSizeuserBufferSizeoptionsMeanStdDevMedianMinMaxRatioAllocated
ReadAsync\before\corerun.exe10241024Asynchronous82.69 us1.946 us82.22 us81.60 us94.90 us1.005 KB
ReadAsync\false\corerun.exe10241024Asynchronous78.76 us0.718 us78.56 us78.04 us82.13 us0.955 KB
ReadAsync\true\corerun.exe10241024Asynchronous83.28 us0.825 us83.12 us82.48 us89.82 us1.015 KB
WriteAsync\before\corerun.exe10241024Asynchronous404.05 us22.574 us400.81 us379.20 us494.53 us1.005 KB
WriteAsync\false\corerun.exe10241024Asynchronous400.76 us17.211 us397.80 us380.07 us472.21 us0.995 KB
WriteAsync\true\corerun.exe10241024Asynchronous405.20 us25.469 us401.47 us383.59 us510.74 us1.015 KB
ReadAsync\before\corerun.exe1048576512Asynchronous2,418.83 us55.684 us2,413.59 us2,361.95 us2,804.46 us1.0093 KB
ReadAsync\false\corerun.exe1048576512Asynchronous2,648.89 us73.366 us2,626.45 us2,550.29 us2,959.92 us1.1083 KB
ReadAsync\true\corerun.exe1048576512Asynchronous2,386.25 us9.985 us2,386.16 us2,356.79 us2,419.58 us0.9983 KB
WriteAsync\before\corerun.exe1048576512Asynchronous4,254.70 us277.778 us4,213.63 us3,967.55 us6,203.71 us1.0085 KB
WriteAsync\false\corerun.exe1048576512Asynchronous4,583.08 us183.949 us4,559.42 us4,317.29 us5,334.19 us1.0875 KB
WriteAsync\true\corerun.exe1048576512Asynchronous4,207.77 us179.340 us4,173.53 us3,896.65 us4,865.02 us0.9975 KB
ReadAsync\before\corerun.exe10485764096Asynchronous2,343.06 us35.339 us2,355.62 us2,225.73 us2,379.72 us1.0079 KB
ReadAsync\false\corerun.exe10485764096Asynchronous2,603.91 us40.916 us2,603.11 us2,513.55 us2,715.90 us1.1169 KB
ReadAsync\true\corerun.exe10485764096Asynchronous2,324.68 us38.225 us2,337.29 us2,226.14 us2,374.04 us0.9969 KB
WriteAsync\before\corerun.exe10485764096Asynchronous4,110.48 us265.902 us4,050.53 us3,844.25 us6,007.74 us1.0084 KB
WriteAsync\false\corerun.exe10485764096Asynchronous4,572.16 us320.658 us4,542.09 us4,256.30 us7,440.25 us1.1274 KB
WriteAsync\true\corerun.exe10485764096Asynchronous4,048.01 us184.553 us4,015.01 us3,739.28 us4,754.61 us0.9974 KB
ReadAsync_NoBuffering\before\corerun.exe104857616384Asynchronous747.53 us5.076 us747.87 us729.80 us755.69 us1.0020 KB
ReadAsync_NoBuffering\false\corerun.exe104857616384Asynchronous822.87 us6.877 us822.90 us809.94 us838.18 us1.1017 KB
ReadAsync_NoBuffering\true\corerun.exe104857616384Asynchronous726.82 us11.262 us726.44 us699.96 us754.36 us0.9717 KB
WriteAsync_NoBuffering\before\corerun.exe104857616384Asynchronous2,726.54 us136.755 us2,712.48 us2,534.40 us3,234.12 us1.0020 KB
WriteAsync_NoBuffering\false\corerun.exe104857616384Asynchronous2,761.05 us146.361 us2,792.11 us2,507.70 us3,188.16 us1.0117 KB
WriteAsync_NoBuffering\true\corerun.exe104857616384Asynchronous2,736.46 us194.495 us2,705.98 us2,415.11 us4,022.13 us1.0117 KB
ReadAsync\before\corerun.exe1048576004096Asynchronous250,490.85 us2,604.707 us250,256.45 us244,814.30 us256,607.90 us1.007,801 KB
ReadAsync\false\corerun.exe1048576004096Asynchronous278,260.89 us4,931.050 us278,009.45 us269,882.60 us297,338.40 us1.116,803 KB
ReadAsync\true\corerun.exe1048576004096Asynchronous252,682.05 us19,763.442 us250,702.50 us243,911.20 us447,534.10 us1.016,801 KB
WriteAsync\before\corerun.exe1048576004096Asynchronous370,893.59 us6,590.799 us369,781.05 us352,834.20 us390,366.40 us1.007,905 KB
WriteAsync\false\corerun.exe1048576004096Asynchronous397,619.03 us8,020.098 us397,052.95 us384,609.00 us446,559.70 us1.076,905 KB
WriteAsync\true\corerun.exe1048576004096Asynchronous358,128.05 us6,536.171 us357,755.45 us346,707.00 us382,735.70 us0.976,905 KB

@adamsitnikadamsitnik removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Apr 15, 2021

@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.

LGTM, thank you @carlossanlop !

@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

With the unit tests passing, the micro benchmarks having no regressions after reverting RunContinuationsAsynchronously to true, and having allocation improvements, I feel confident in merging this change to include it in Preview4, and will continue working on #50972 to add caching and improve this code for Preview5.

@carlossanlop
carlossanlop merged commit a38d0c2 into dotnet:mainApr 15, 2021
@carlossanlop
carlossanlop deleted the IValueTaskSource branch April 15, 2021 22:51
@ghostghost locked as resolved and limited conversation to collaborators May 15, 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.

5 participants

@carlossanlop@davidfowl@stephentoub@adamsitnik@jozkee
, '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: Use IValueTaskSource instead of TaskCompletionSource - #50802

Merged
carlossanlop merged 19 commits into
dotnet:mainfrom
carlossanlop:IValueTaskSource
Apr 15, 2021
Merged

FileStream rewrite: Use IValueTaskSource instead of TaskCompletionSource#50802
carlossanlop merged 19 commits into
dotnet:mainfrom
carlossanlop:IValueTaskSource

Conversation

@carlossanlop

Copy link
Copy Markdown
Contributor

The purpose of this PR is to remove a large allocation found in FileStream.ReadAsync and FileStream.WriteAsync, which we confirmed in our profiling investigation around the new FileStream rewrite code.

The allocation improvement can be achieved by creating a new type, called FileStreamValueTaskSource, that will have the same purpose as FileStreamCompletionSource, but instead of implementing TaskCompletionSource (which returns a Task to represent the asynchronous work) it implements IValueTaskSource, which returns a ValueTask.

The behavior was largely left untouched, except for the following differences:

  • Net5CompatFileStreamStrategy.Windows will be the only strategy that consumes the FileStreamCompletionSource, so it is now a private nested class.
  • One single new type implements both IValueTaskSource (used for write) and IValueTaskSource<int> (used for read).
  • The logic inside the callback and the cancellation token registration is the same, aside for the code that needs to report exceptions or return values.
  • We had interop errors redefined in multiple places.
  • The constants used by FileStreamCompletionSource were moved to FileStreamHelpers so they could be reused by the new type.

Unit tests

Manually ran unit tests from System.IO.FileSystem, System.IO, System.Runtime and System.Runtime.Extensions, they all passed. I made sure to run them with the Net5Compat flag turned off, to ensure my new code was executed.

Benchmarks

  • Ran the FileStream performance benchmarks against .NET 5.0, then against the code of this PR's baseline commit, and then against this PR's new code. I compared the results, and there are subtle improvements in some benchmark tests:
.NET 5.0 vs .NET 6.0 before this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_before_pr' --threshold 0.0001%

summary:
better: 39, geomean: 2.801
worse: 22, geomean: 2.098
total diff: 61

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.93882252.574348913.28
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.621003671.884633945.31bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.13257218.23805205.31several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.13286859.38896524.31
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.061795459.385498006.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.051868294.535689532.29
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)2.99254148.29758897.83
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.97283860.42843052.43
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.901844293.365353701.04several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.892209384.386395554.17bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.65301619.83798302.63
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.86557062.051035575.21bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.81478821.50868866.12
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.693479778.755891160.42several?
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.66543512.61903388.82bimodal
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.40168291900.00235314600.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.248197915.6310200393.75
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.215313143.756444323.96
Perf_FileStream.Flush(fileSize: 1024, options: None)1.183448111.254052132.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055473900.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.0956033900.0061052537.50
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.03133329050.00137489800.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.645489541.6747470.79
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.902811281.2543319.93
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.822571736.4644479.08
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.29876923.2641197.72
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.062463834750.00486578800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.94152781.6630909.09
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.72153731.8132593.67
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437889.31several?
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.98165696.6841632.18
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.97159526.5840152.10
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.50200423.3657204.05
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889181.93
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.55238920.2293543.27
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.50193629.0877357.59
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.443222202.501318286.88
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.39194808.8781591.76
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.36216916.9892096.58
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.24325116900.00145245400.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.157025697.923271982.50
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.10199647.8995019.35
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.04686776300.00336991400.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9519598723.0810074393.75
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)1.85508393800.00274360000.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.74796591200.00457934500.00bimodal
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.73156593600.0090313262.50
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.7022492768.7513248333.33
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64542213800.00331399500.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.615221529.173244615.63
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114593046.88
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742610.27
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09777023.21
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.191301925.001096436.88
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.12142000.45126451.42
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 512, options: None)1.11162435300.00146191400.00
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.10141476.75128484.00
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.10142083.81129048.19
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00113456750.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0288348175.0086894587.50
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 512, options: None)1.0192483275.0091193650.00
.NET 5.0 vs .NET 6.0 after this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_after_pr' --threshold 0.0001%
summary:
better: 36, geomean: 3.006
worse: 22, geomean: 2.080
total diff: 58

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.92882252.574341552.34
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.601003671.884615128.91bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)3.24283860.42919970.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)3.08254148.29783231.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.061868294.535710579.17
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.05286859.38874430.38
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.04257218.23780960.42
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)2.971795459.385329393.75
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.951844293.365432295.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.922209384.386445677.08
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.52301619.83758761.88
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.95557062.051087235.83
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.73543512.61937893.38bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.66478821.50793440.46bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.473479778.755113488.54several?
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.32168291900.00221660000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.235313143.756512252.08
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.228197915.6310012295.65
Perf_FileStream.Flush(fileSize: 1024, options: None)1.213448111.254163660.16
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1356033900.0063237650.00
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0950055150.0054423625.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0088983100.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.505489541.6747526.62
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.942811281.2543292.23
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.062571736.4645070.47
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.05876923.2641661.14
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.362463834750.00459292800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.95152781.6630850.74
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.75153731.8132349.21
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437926.07several?
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.98159526.5840096.31
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.96165696.6841824.65
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889051.28
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.56238920.2293276.60
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.53193629.0876383.11
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.45194808.8779416.04
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501376330.63
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.31216916.9893711.06
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.28325116900.00142880200.00
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)2.22200423.3690245.25
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.167025697.923250746.88
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.11508393800.00241032100.00
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.11199647.8994675.64
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.06686776300.00333108950.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9319598723.0810171781.82
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.74156593600.0090035450.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.65542213800.00329569650.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.645221529.173182177.50
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64796591200.00486630000.00
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.5222492768.7514797453.85
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114625460.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742460.71
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09781337.50
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.201301925.001085495.19
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.11141476.75127158.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.11142083.81128134.40
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.10142000.45129068.15
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00112942550.00

Profiling

I reused the same code we have in our benchmarks, but I executed it inside a console app that forces the consumption of the compiled runtime code.

You'll notice the top Task allocation went away, and the FileStreamCompletionSource allocations were substituted by FileStreamValueTaskSource allocations.

ReadAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,797
- System.String2,153
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,806
- System.String2,153
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]964
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

ReadAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,013
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,840
- System.String2,153
- System.Object[]955
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,836
- System.String2,144
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>64,235
- System.Threading.Tasks.Task<>64,015
- System.IO.FileStreamCompletionSource64,000
- System.SByte[]2,916
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,812
- System.SByte[]2,909
- System.String2,144
- System.Object[]964
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,798
- System.String2,153
- System.Object[]954
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,812
- System.String2,153
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]958
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,249
- System.IO.MemoryFileStreamCompletionSource32,000
- System.IO.FileStreamCompletionSource32,000
- System.SByte[]2,915
- System.String2,155
- System.Object[]952
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,246
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource32,000
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource32,000
- System.SByte[]2,893
- System.String2,144
- System.Object[]954
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.IO.FileStreamCompletionSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,963
- System.SByte[]2,915
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,830
- System.SByte[]2,912
- System.String2,144
- System.Object[]966
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Threading.Tasks.ValueTask.ValueTaskSourceAsTask249
- System.Threading.QueueUserWorkItemCallbackDefaultContext<>249

@adamsitnik@jozkee@jeffhandley@stephentoub

Also: Don't forget to ignore whitespace changes so it's easier to review the nested type's changes 😃

@ghost

ghost commented Apr 6, 2021

Copy link
Copy Markdown

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

Issue Details

The purpose of this PR is to remove a large allocation found in FileStream.ReadAsync and FileStream.WriteAsync, which we confirmed in our profiling investigation around the new FileStream rewrite code.

The allocation improvement can be achieved by creating a new type, called FileStreamValueTaskSource, that will have the same purpose as FileStreamCompletionSource, but instead of implementing TaskCompletionSource (which returns a Task to represent the asynchronous work) it implements IValueTaskSource, which returns a ValueTask.

The behavior was largely left untouched, except for the following differences:

  • Net5CompatFileStreamStrategy.Windows will be the only strategy that consumes the FileStreamCompletionSource, so it is now a private nested class.
  • One single new type implements both IValueTaskSource (used for write) and IValueTaskSource<int> (used for read).
  • The logic inside the callback and the cancellation token registration is the same, aside for the code that needs to report exceptions or return values.
  • We had interop errors redefined in multiple places.
  • The constants used by FileStreamCompletionSource were moved to FileStreamHelpers so they could be reused by the new type.

Unit tests

Manually ran unit tests from System.IO.FileSystem, System.IO, System.Runtime and System.Runtime.Extensions, they all passed. I made sure to run them with the Net5Compat flag turned off, to ensure my new code was executed.

Benchmarks

  • Ran the FileStream performance benchmarks against .NET 5.0, then against the code of this PR's baseline commit, and then against this PR's new code. I compared the results, and there are subtle improvements in some benchmark tests:
.NET 5.0 vs .NET 6.0 before this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_before_pr' --threshold 0.0001%

summary:
better: 39, geomean: 2.801
worse: 22, geomean: 2.098
total diff: 61

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.93882252.574348913.28
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.621003671.884633945.31bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.13257218.23805205.31several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.13286859.38896524.31
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.061795459.385498006.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.051868294.535689532.29
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)2.99254148.29758897.83
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.97283860.42843052.43
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.901844293.365353701.04several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.892209384.386395554.17bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.65301619.83798302.63
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.86557062.051035575.21bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.81478821.50868866.12
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.693479778.755891160.42several?
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.66543512.61903388.82bimodal
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.40168291900.00235314600.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.248197915.6310200393.75
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.215313143.756444323.96
Perf_FileStream.Flush(fileSize: 1024, options: None)1.183448111.254052132.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055473900.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.0956033900.0061052537.50
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.03133329050.00137489800.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.645489541.6747470.79
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.902811281.2543319.93
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.822571736.4644479.08
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.29876923.2641197.72
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.062463834750.00486578800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.94152781.6630909.09
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.72153731.8132593.67
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437889.31several?
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.98165696.6841632.18
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.97159526.5840152.10
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.50200423.3657204.05
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889181.93
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.55238920.2293543.27
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.50193629.0877357.59
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.443222202.501318286.88
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.39194808.8781591.76
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.36216916.9892096.58
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.24325116900.00145245400.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.157025697.923271982.50
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.10199647.8995019.35
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.04686776300.00336991400.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9519598723.0810074393.75
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)1.85508393800.00274360000.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.74796591200.00457934500.00bimodal
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.73156593600.0090313262.50
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.7022492768.7513248333.33
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64542213800.00331399500.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.615221529.173244615.63
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114593046.88
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742610.27
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09777023.21
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.191301925.001096436.88
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.12142000.45126451.42
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 512, options: None)1.11162435300.00146191400.00
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.10141476.75128484.00
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.10142083.81129048.19
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00113456750.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0288348175.0086894587.50
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 512, options: None)1.0192483275.0091193650.00
.NET 5.0 vs .NET 6.0 after this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_after_pr' --threshold 0.0001%
summary:
better: 36, geomean: 3.006
worse: 22, geomean: 2.080
total diff: 58

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.92882252.574341552.34
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.601003671.884615128.91bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)3.24283860.42919970.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)3.08254148.29783231.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.061868294.535710579.17
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.05286859.38874430.38
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.04257218.23780960.42
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)2.971795459.385329393.75
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.951844293.365432295.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.922209384.386445677.08
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.52301619.83758761.88
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.95557062.051087235.83
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.73543512.61937893.38bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.66478821.50793440.46bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.473479778.755113488.54several?
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.32168291900.00221660000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.235313143.756512252.08
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.228197915.6310012295.65
Perf_FileStream.Flush(fileSize: 1024, options: None)1.213448111.254163660.16
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1356033900.0063237650.00
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0950055150.0054423625.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0088983100.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.505489541.6747526.62
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.942811281.2543292.23
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.062571736.4645070.47
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.05876923.2641661.14
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.362463834750.00459292800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.95152781.6630850.74
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.75153731.8132349.21
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437926.07several?
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.98159526.5840096.31
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.96165696.6841824.65
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889051.28
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.56238920.2293276.60
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.53193629.0876383.11
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.45194808.8779416.04
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501376330.63
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.31216916.9893711.06
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.28325116900.00142880200.00
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)2.22200423.3690245.25
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.167025697.923250746.88
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.11508393800.00241032100.00
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.11199647.8994675.64
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.06686776300.00333108950.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9319598723.0810171781.82
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.74156593600.0090035450.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.65542213800.00329569650.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.645221529.173182177.50
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64796591200.00486630000.00
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.5222492768.7514797453.85
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114625460.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742460.71
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09781337.50
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.201301925.001085495.19
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.11141476.75127158.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.11142083.81128134.40
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.10142000.45129068.15
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00112942550.00

Profiling

I reused the same code we have in our benchmarks, but I executed it inside a console app that forces the consumption of the compiled runtime code.

You'll notice the top Task allocation went away, and the FileStreamCompletionSource allocations were substituted by FileStreamValueTaskSource allocations.

ReadAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,797
- System.String2,153
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,806
- System.String2,153
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]964
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

ReadAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,013
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,840
- System.String2,153
- System.Object[]955
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,836
- System.String2,144
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>64,235
- System.Threading.Tasks.Task<>64,015
- System.IO.FileStreamCompletionSource64,000
- System.SByte[]2,916
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,812
- System.SByte[]2,909
- System.String2,144
- System.Object[]964
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,798
- System.String2,153
- System.Object[]954
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,812
- System.String2,153
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]958
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,249
- System.IO.MemoryFileStreamCompletionSource32,000
- System.IO.FileStreamCompletionSource32,000
- System.SByte[]2,915
- System.String2,155
- System.Object[]952
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,246
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource32,000
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource32,000
- System.SByte[]2,893
- System.String2,144
- System.Object[]954
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.IO.FileStreamCompletionSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,963
- System.SByte[]2,915
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,830
- System.SByte[]2,912
- System.String2,144
- System.Object[]966
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Threading.Tasks.ValueTask.ValueTaskSourceAsTask249
- System.Threading.QueueUserWorkItemCallbackDefaultContext<>249

@adamsitnik@jozkee@jeffhandley@stephentoub

Also: Don't forget to ignore whitespace changes so it's easier to review the nested type's changes 😃
Author:carlossanlop
Assignees:carlossanlop
Labels:

area-System.IO

Milestone:6.0.0

@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

Tagging subscribers to this area: @carlossanlop

Yes, thank you bot... 😆

@jozkeejozkee 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.

Otherwise; LGTM, will approve if/when comments are resolved, most of them are nits and/or questions.

Comment on lines +17 to +22
internal const long NoResult = 0;
internal const long ResultSuccess = (long)1 << 32;
internal const long ResultError = (long)2 << 32;
internal const long RegisteringCancellation = (long)4 << 32;
internal const long CompletedCallback = (long)8 << 32;
internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32;

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.

Consider prefixing this constants with AsyncCompletionSource_ or something alike since these names are too ambiguous or keep them in their respective classes regardless of them being duplicated.

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.

or put them into a nested static class called ReturnCodes:

internalstaticpartialclassFileStreamHelpers{internalstaticclassReturnCodes{internalconstlongNoResult=0;internalconstlongResultSuccess=(long)1<<32;internalconstlongResultError=(long)2<<32;internalconstlongRegisteringCancellation=(long)4<<32;internalconstlongCompletedCallback=(long)8<<32;internalconstulongResultMask=((ulong)uint.MaxValue)<<32;}

but it's a nit (not a blocker)

Comment on lines +66 to +67
_strategy.CompareExchangeCurrentOverlappedOwner(this, null) == null ?
_strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!) : // allocated when buffer was created, and buffer is non-null

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.

Suggested change
_strategy.CompareExchangeCurrentOverlappedOwner(this,null)==null?
_strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!):// allocated when buffer was created, and buffer is non-null
strategy.CompareExchangeCurrentOverlappedOwner(this,null)==null?
strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!):// allocated when buffer was created, and buffer is non-null

// be directly the AwaitableProvider that's completing (in the case where the preallocated
// overlapped was already in use by another operation).
object? state = ThreadPoolBoundHandle.GetNativeOverlappedState(pOverlapped);
Debug.Assert(state is (AsyncWindowsFileStreamStrategy or FileStreamValueTaskSource));

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.

Suggested change
Debug.Assert(stateis(AsyncWindowsFileStreamStrategy or FileStreamValueTaskSource));
Debug.Assert(stateisAsyncWindowsFileStreamStrategy or FileStreamValueTaskSource);

int errorCode = unchecked((int)(packedResult & uint.MaxValue));
if (errorCode == Interop.Errors.ERROR_OPERATION_ABORTED)
{
_source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(new OperationCanceledException(cancellationToken.IsCancellationRequested ? cancellationToken : new CancellationToken(canceled: true))));

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.

Suggested change
_source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(newOperationCanceledException(cancellationToken.IsCancellationRequested?cancellationToken:newCancellationToken(canceled:true))));
Exceptione=newOperationCanceledException(cancellationToken.IsCancellationRequested?cancellationToken:newCancellationToken(canceled:true));
e.SetCurrentStackTrace();
_source.SetException(e);

}

private void Cancel(CancellationToken token)
{

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.

Missing comment and asserts:

// WARNING: This may potentially be called under a lock (during cancellation registration)
Debug.Assert(stateisFileStreamCompletionSource,"Unknown state passed to cancellation");
FileStreamCompletionSourcecompletionSource=(FileStreamCompletionSource)state;
Debug.Assert(completionSource._overlapped!=null&&!completionSource.Task.IsCompleted,"IO should not have completed yet");

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The first assert is not needed because the method argument is not an object? state.

I'll add the assert that checks for overlapped an completed status.

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.

The state param could be added to this method and allows you to conserve the first Debug.Assert.

long packedResult = Interlocked.CompareExchange(ref _result, FileStreamHelpers.RegisteringCancellation, FileStreamHelpers.NoResult);
if (packedResult == FileStreamHelpers.NoResult)
{
_cancellationRegistration = cancellationToken.UnsafeRegister(static (s, token) => ((FileStreamValueTaskSource)s!).Cancel(token), this);

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.

Could we pass the callback on a similar fashion to FileStreamCompletionSource

_cancellationRegistration=cancellationToken.UnsafeRegister(cancelCallback,this);

Or why did you opted for this way instead?

@carlossanlopcarlossanlopApr 8, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It made more sense to pass this as the second argument of UnsafeRegister, which ensures it's a ValueTaskSource instance, and saves us from the check of the object? state parameter like in the old code. The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

We have a similar example in System.Threading.Channels.

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.

It made more sense to pass this as the second argument of UnsafeRegister

FileStreamCompletionSource was also passing this as 2nd argument.

The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

Yes, seems that it was caching the method group to avoid allocating everytime it was passed to UnsafeRegister.
However I think you can avoid the allocation without caching yourself by doing this:

_cancellationRegistration=cancellationToken.UnsafeRegister((s,token)=>Cancel(s,token),this);

Just a suggestion, not something that you should block on.

@davidfowl

Copy link
Copy Markdown
Member

This is the change I was waiting for 👏🏾👏🏾

@davidfowl

Copy link
Copy Markdown
Member

I haven't looked but I assume we'll reuse the operation objects if there's no concurrency? This is what sockets do today so in the 95% case of no overlapping writes or reads, there'll be a single reusable allocation on the FileStream

@stephentoub

Copy link
Copy Markdown
Member

This is the change I was waiting for

It's not, actually. You want the next one where the IValueTaskSource instance is cached.

}
// else: Some other thread stole the result, so now it is responsible to finish the callback
}
// else: Some other thread is registering a cancellation, so it *must* finish the callback

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.

All this logic looks more complicated than it needs to be. I realize you're just following the same flow as what was already there, so nothing needs to be changed in this PR. But we should revisit all of this synchronization later; I suspect we can do it more succinctly and efficiently.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I opened an issue to follow up on this and on the caching of the IValueTaskSource.
#50972

@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 before we merge I would like to see the following benchmark results:

.NET 6 with new strategy enabled: before and after your change. (not .NET 5 vs 6 as there are too many moving targets)

You should be able to achieve that by running the following command in the perf repo;

git clone https://github.com/dotnet/performance.git
cd performance\src\benchmarks\micro
dotnet run -c Release -f net6.0 --filter *Perf_FileStream* --coreRun $pathToCoreRunWithoutYourChanges $pathToCoreRunWithYourChanges --envVars "DOTNET_SYSTEM_IO_USENET5COMPATFILESTREAM:0"

/// <see cref="MemoryHandle"/> when the operation has completed. This should only be used
/// when memory doesn't wrap a byte[].
/// </summary>
private sealed class MemoryAwaitableProvider : FileStreamValueTaskSource

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.

This should only be used when memory doesn't wrap a byte[].

I just realized that we don't have benchmarks for this scenario as Memory used in our micro-benchmarks always wraps a byte array. Could you please add such benchmarks to https://github.com/dotnet/performance/blob/main/src/benchmarks/micro/libraries/System.IO.FileSystem/Perf.FileStream.cs ? One test case should be enough as this should be rare.

@carlossanlopcarlossanlopApr 7, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Some of the benchmarks do consume this type. If you look at some of the profiling tests I executed, you'll see that MemoryValueTaskSource shows up a few times. The profiling tests reuse the code we had in our benchmarks.

I don't mind adding an explicit one though. Dou you want me to add one anyway?

Comment on lines +17 to +22
internal const long NoResult = 0;
internal const long ResultSuccess = (long)1 << 32;
internal const long ResultError = (long)2 << 32;
internal const long RegisteringCancellation = (long)4 << 32;
internal const long CompletedCallback = (long)8 << 32;
internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32;

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.

or put them into a nested static class called ReturnCodes:

internalstaticpartialclassFileStreamHelpers{internalstaticclassReturnCodes{internalconstlongNoResult=0;internalconstlongResultSuccess=(long)1<<32;internalconstlongResultError=(long)2<<32;internalconstlongRegisteringCancellation=(long)4<<32;internalconstlongCompletedCallback=(long)8<<32;internalconstulongResultMask=((ulong)uint.MaxValue)<<32;}

but it's a nit (not a blocker)

Comment on lines 62 to 63

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.

Did you run the perf. benchmarks after doing this change and did you notice any impact?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Pending.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Confirmed - There is a slight improvement.

I made sure to compare apples to apples: I used my latest commit, and built it setting RunContinuationsAsynchronously to true, then to false. I compared it both times against our code in .NET 5.0.

The improvement is clear when setting it to false (or not setting it at all).

RunContinuationsAsynchronously = true

❯ dotnet run -c release --base "D:\fs50" --diff "D:\fs60_NEW_ON_IVTS_asynctrue" --threshold 0.0001%
summary:
better: 24, geomean: 3.402
worse: 19, geomean: 2.244
total diff: 43

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)5.49882252.574840275.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)5.121003671.885136350.00bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)4.44257218.231141827.01
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)4.08286859.381171600.96
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.251795459.385832233.33
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)3.111844293.365741847.92several?
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.111868294.535807825.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)3.082209384.386796443.75
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)2.46557062.051371566.15
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)2.33478821.501116033.48
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)2.28543512.611239956.25bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.613479778.755612725.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.448197915.6311814575.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.33168291900.00224355000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.335313143.757063218.75
Perf_FileStream.Flush(fileSize: 1024, options: None)1.253448111.254313832.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1250055150.0056309025.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1256033900.0062803900.00
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.12133329050.00148833000.00bimodal
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)117.075489541.6746889.31
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)65.932811281.2542642.74
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.842571736.4644462.45
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.09876923.2641586.04
Perf_FileStream.OpenClose(fileSize: 1024, options: None)5.00152781.6630548.62
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.76153731.8132314.04
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)4.032463834750.00611183400.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.93165696.6842208.34
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.52200423.3656865.52
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.62230670.3887990.73
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.58238920.2292478.58
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501377939.84
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.87156593600.0083681962.50
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.6619598723.0811803641.67
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.6322492768.7513818900.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.577025697.924487121.88
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)1.47686776300.00466936500.00bimodal
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55743825.45
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.24975146.09784156.88
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.195221529.174387085.94
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.181301925.001103803.79
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.04116486775.00112407200.00
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.0321953590.9121241122.73
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0087467100.00
RunContinuationsAsynchronously = false

dotnet run -c release --base "D:\fs50" --diff "D:\fs60_NEW_ON_IVTS_asyncfalse" --threshold 0.0001%
summary:
better: 23, geomean: 3.592
worse: 20, geomean: 2.135
total diff: 43

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)5.36882252.574730875.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)5.051003671.885071912.50bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)4.25257218.231094164.38several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)4.05286859.381161160.27
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.231795459.385791481.25
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)3.101844293.365709820.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)3.092209384.386836504.17
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.071868294.535733883.33
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)2.46557062.051368034.90
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)2.31478821.501103696.25
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)2.24543512.611218234.38bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.603479778.755572682.29
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.428197915.6311634850.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.33168291900.00223141800.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.315313143.756949831.25
Perf_FileStream.Flush(fileSize: 1024, options: None)1.263448111.254332915.63
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.13133329050.00150223800.00bimodal
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055699050.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1056033900.0061710550.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0088348175.0088759625.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)116.975489541.6746929.42
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)65.762811281.2542753.29
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.562571736.4644676.15
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.52876923.2640756.47
Perf_FileStream.OpenClose(fileSize: 1024, options: None)5.06152781.6630216.63
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.81153731.8131975.57
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)4.052463834750.00608864800.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.99165696.6841526.73
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.54200423.3656610.08
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.62230670.3887877.10
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.59238920.2292291.75
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.403222202.501345116.25
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.86156593600.0083969875.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.6419598723.0811925173.68
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.6222492768.7513859764.71
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.567025697.924493881.25
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)1.46686776300.00470158300.00bimodal
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.32987460.55750294.64
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.24975146.09784056.56
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.171301925.001108880.36
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.175221529.174470673.44
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.0721953590.9120580725.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.01116486775.00

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Here is the result comparison if we compare the benchmark results of setting it to true vs false:

base: RunContinuationsAsynchronously=true, diff: RunContinuationsAsynchronously=false

dotnet run -c release --base "D:\fs60_NEW_ON_IVTS_asynctrue" --diff "D:\fs60_NEW_ON_IVTS_asyncfalse" --threshold 0.0001%
summary:
better: 14, geomean: 1.018
worse: 11, geomean: 1.017
total diff: 25

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.WriteAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0567143550.0070259187.50
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.02112407200.00115107550.00
Perf_FileStream.Read_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.02240016.67244921.93
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.024387085.944470673.44
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0187467100.0088759625.00
Perf_FileStream.WriteAsync_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.0121684354.5521982809.09
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: Asynchronous)1.011236709.901253242.19
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 1024, options: None)1.0139289.5439730.73
Perf_FileStream.Write_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.0121445427.2721646650.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.01743825.45750294.64
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)1.0044462.4544676.15
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.WriteByte(fileSize: 1024, options: None)1.041141827.011094164.38several?
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0345688612.5044474575.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.021377939.841345116.25
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)1.024840275.004730875.00
Perf_FileStream.SeekForward(fileSize: 1024, options: None)1.0241586.0440756.47
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: Asynchronous)1.02130849050.00128463775.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)1.0242208.3441526.73
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.0211814575.0011634850.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)1.015807825.005733883.33
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)1.015136350.005071912.50
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0156309025.0055699050.00
Perf_FileStream.Read_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0133747814.293338345.14
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)1.0132314.0431975.57
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 1024, options: Asynchronous)1.0081988.0981641.78

long packedResult = Interlocked.CompareExchange(ref _result, FileStreamHelpers.RegisteringCancellation, FileStreamHelpers.NoResult);
if (packedResult == FileStreamHelpers.NoResult)
{
_cancellationRegistration = cancellationToken.UnsafeRegister(static (s, token) => ((FileStreamValueTaskSource)s!).Cancel(token), this);

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.

It made more sense to pass this as the second argument of UnsafeRegister

FileStreamCompletionSource was also passing this as 2nd argument.

The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

Yes, seems that it was caching the method group to avoid allocating everytime it was passed to UnsafeRegister.
However I think you can avoid the allocation without caching yourself by doing this:

_cancellationRegistration=cancellationToken.UnsafeRegister((s,token)=>Cancel(s,token),this);

Just a suggestion, not something that you should block on.

Comment on lines 120 to 122

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.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I tried, but a ValueTask does not have a Result property, like a ValueTask<T> does, because it returns void. So I'm not sure if it's possible to apply an equivalent pattern.

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.

publicoverridevoidWrite(byte[]buffer,intoffset,intcount){ValueTaskvt=WriteAsyncInternal(newReadOnlyMemory<byte>(buffer,offset,count),CancellationToken.None);if(vt.IsCompleted){vt.GetAwaiter().GetResult();}else{vt.AsTask().GetAwaiter().GetResult();}}

That said, it's not much of an issue for this case. The read side is an issue because AsTask() will almost certainly need to allocate a new Task<int> in order to hand back the result, but for the write side, AsTask() on an already completed ValueTask will just return Task.Completed, assuming the operation completed successfully. So the only time the existing code for the write side will allocate is when the operation was canceled or faulted, in which case we're about to throw an exception anyway, and the extra task allocation won't really matter. Up to you whether you want to change it.

@jozkeejozkee 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.

LGTM; thanks Carlos!

@adamsitnik

Copy link
Copy Markdown
Member

I compared it both times against our code in .NET 5.0.

Comparison against 5.0 is a comparison of all of the changes that were merged to main since we have snapped 5.0 with your change against 5.0. What we care about is how your change (and nothing else) is affecting the perf, so we need to isolate the runs to only your change.

To be able to do that you need to run the benchmarks using two CoreRuns:

  • as a baseline the corerun that was built from main when you have created a new branch, without any of your changes
  • your branch with your changes

My personal workflow:

  1. When I create a new branch, I perform a full build in Release:
build -c Release -subset clr+libs
  1. I copy the folder that contains CoreRun.exe (runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0) to a separate folder. Typically I call it "before".
  2. I apply some changes and perform an incremental Release build. In case of System.Private.CoreLib.dll it's:
build -c Release -subset clr.corelib+clr.nativecorelib+libs.PreTest
  1. I run the benchmarks using the old and new CoreRun.exe:
dotnet run -c Release -f net6.0 --filter *ABC* --corerun C:\Projects\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\before\CoreRun.exe C:\Projects\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe

@carlossanlop

carlossanlop commented Apr 13, 2021

Copy link
Copy Markdown
ContributorAuthor
  • as a baseline the corerun that was built from main when you have created a new branch, without any of your changes
  • your branch with your changes

@adamsitnik@jozkee here's the table comparing the before (original) and after of this PR. Most results had allocation improvements, but worse speed when FileOptions = Asynchronous:

Results

Command:

 dotnet run
-c Release
-f net6.0
--corerun
D:\runtime2\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe
D:\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe
--filter System.IO.Tests.Perf_FileStream.*Async*
--statisticalTest 3ms
--minIterationCount 100
--maxIterationCount 101
--artifacts "D:\finalresults"

Note: The runtime2 corerun contains the baseline commit before my changes, the runtime corerun contains this PR's changes.

MethodToolchainfileSizeuserBufferSizeoptionsMeanErrorStdDevMedianMinMaxRatioAllocated
ReadAsyncBEFORE10241024None78.69 μs0.255 μs0.736 μs78.53 μs77.61 μs80.66 μs1.005 KB
ReadAsyncAFTER10241024None79.10 μs0.237 μs0.673 μs79.08 μs76.96 μs80.71 μs1.015 KB
WriteAsyncBEFORE10241024None316.97 μs2.512 μs6.919 μs315.92 μs305.11 μs339.35 μs1.004 KB
WriteAsyncAFTER10241024None300.13 μs5.643 μs15.730 μs297.59 μs272.46 μs348.15 μs0.954 KB
ReadAsyncBEFORE10241024Asynchronous94.13 μs0.259 μs0.731 μs94.02 μs92.27 μs96.16 μs1.005 KB
ReadAsyncAFTER10241024Asynchronous85.48 μs0.842 μs2.291 μs85.11 μs82.58 μs92.09 μs0.915 KB
WriteAsyncBEFORE10241024Asynchronous292.63 μs5.659 μs15.869 μs290.59 μs264.25 μs338.98 μs1.005 KB
WriteAsyncAFTER10241024Asynchronous324.35 μs13.906 μs39.222 μs311.27 μs274.34 μs428.64 μs1.115 KB
FlushAsyncBEFORE1024?None4,606.81 μs46.736 μs133.341 μs4,633.92 μs4,374.04 μs4,952.63 μs1.00269 KB
FlushAsyncAFTER1024?None4,587.88 μs58.046 μs168.403 μs4,638.98 μs4,322.79 μs5,034.48 μs1.00269 KB
CopyToFileAsyncBEFORE1024?None474.57 μs5.497 μs15.141 μs474.66 μs440.31 μs520.31 μs1.005 KB
CopyToFileAsyncAFTER1024?None488.94 μs3.631 μs10.061 μs488.62 μs469.93 μs518.76 μs1.035 KB
FlushAsyncBEFORE1024?Asynchronous14,680.35 μs150.967 μs437.982 μs14,637.76 μs13,623.50 μs15,645.01 μs1.00301 KB
FlushAsyncAFTER1024?Asynchronous20,333.22 μs116.277 μs339.185 μs20,299.93 μs19,724.25 μs21,148.73 μs1.39261 KB
CopyToFileAsyncBEFORE1024?Asynchronous520.97 μs5.982 μs16.674 μs519.35 μs490.05 μs572.29 μs1.006 KB
CopyToFileAsyncAFTER1024?Asynchronous523.30 μs6.294 μs17.650 μs520.91 μs479.14 μs583.85 μs1.006 KB
ReadAsyncBEFORE1048576512None1,375.28 μs5.930 μs17.015 μs1,376.60 μs1,340.44 μs1,413.73 μs1.0085 KB
ReadAsyncAFTER1048576512None1,347.16 μs5.128 μs14.958 μs1,345.14 μs1,323.57 μs1,382.14 μs0.9885 KB
WriteAsyncBEFORE1048576512None3,530.03 μs38.149 μs108.222 μs3,521.78 μs3,299.91 μs3,823.63 μs1.0076 KB
WriteAsyncAFTER1048576512None3,473.85 μs47.943 μs134.438 μs3,469.00 μs3,222.90 μs3,832.17 μs0.9876 KB
ReadAsyncBEFORE1048576512Asynchronous3,278.98 μs30.616 μs87.844 μs3,257.98 μs3,151.46 μs3,507.69 μs1.0093 KB
ReadAsyncAFTER1048576512Asynchronous4,399.34 μs16.153 μs47.119 μs4,401.64 μs4,294.71 μs4,531.75 μs1.3483 KB
WriteAsyncBEFORE1048576512Asynchronous5,304.10 μs73.935 μs214.500 μs5,309.27 μs4,771.82 μs5,852.43 μs1.0085 KB
WriteAsyncAFTER1048576512Asynchronous6,506.15 μs94.189 μs271.756 μs6,505.61 μs6,042.59 μs7,132.02 μs1.2375 KB
ReadAsyncBEFORE10485764096None1,090.98 μs6.156 μs18.152 μs1,096.14 μs1,039.20 μs1,120.19 μs1.0029 KB
ReadAsyncAFTER10485764096None1,084.03 μs8.601 μs25.225 μs1,086.13 μs1,042.07 μs1,131.85 μs0.9929 KB
WriteAsyncBEFORE10485764096None3,247.60 μs43.259 μs124.117 μs3,243.69 μs2,954.42 μs3,556.50 μs1.0055 KB
WriteAsyncAFTER10485764096None3,250.35 μs36.664 μs100.985 μs3,234.77 μs3,039.74 μs3,505.95 μs1.0055 KB
ReadAsyncBEFORE10485764096Asynchronous3,273.61 μs17.591 μs50.753 μs3,264.72 μs3,190.18 μs3,410.97 μs1.0079 KB
ReadAsyncAFTER10485764096Asynchronous4,547.38 μs73.438 μs216.535 μs4,398.34 μs4,327.10 μs5,035.98 μs1.3969 KB
WriteAsyncBEFORE10485764096Asynchronous5,384.46 μs83.792 μs240.415 μs5,346.69 μs4,897.35 μs5,955.92 μs1.0084 KB
WriteAsyncAFTER10485764096Asynchronous6,845.63 μs88.794 μs256.192 μs6,883.75 μs6,079.92 μs7,286.73 μs1.2774 KB
ReadAsync_NoBufferingBEFORE104857616384None406.34 μs2.413 μs7.113 μs403.28 μs387.16 μs419.08 μs1.007 KB
ReadAsync_NoBufferingAFTER104857616384None380.35 μs3.413 μs10.063 μs378.36 μs363.73 μs402.82 μs0.947 KB
WriteAsync_NoBufferingBEFORE104857616384None2,545.17 μs30.174 μs83.613 μs2,526.42 μs2,397.67 μs2,776.47 μs1.007 KB
WriteAsync_NoBufferingAFTER104857616384None2,537.77 μs27.489 μs76.628 μs2,528.02 μs2,421.65 μs2,750.68 μs1.007 KB
ReadAsync_NoBufferingBEFORE104857616384Asynchronous949.54 μs6.336 μs18.077 μs943.69 μs926.29 μs1,002.20 μs1.0020 KB
ReadAsync_NoBufferingAFTER104857616384Asynchronous1,195.21 μs3.070 μs8.507 μs1,194.94 μs1,179.45 μs1,218.92 μs1.2617 KB
WriteAsync_NoBufferingBEFORE104857616384Asynchronous2,965.91 μs40.616 μs114.558 μs2,952.10 μs2,677.93 μs3,244.55 μs1.0020 KB
WriteAsync_NoBufferingAFTER104857616384Asynchronous3,021.90 μs40.697 μs116.110 μs3,000.74 μs2,803.04 μs3,343.84 μs1.0217 KB
CopyToFileAsyncBEFORE1048576?None2,440.43 μs17.056 μs49.754 μs2,437.81 μs2,343.18 μs2,576.37 μs1.003 KB
CopyToFileAsyncAFTER1048576?None2,421.23 μs15.376 μs43.869 μs2,413.43 μs2,350.26 μs2,521.45 μs0.993 KB
CopyToFileAsyncBEFORE1048576?Asynchronous2,878.14 μs16.682 μs47.863 μs2,876.69 μs2,771.42 μs3,001.49 μs1.004 KB
CopyToFileAsyncAFTER1048576?Asynchronous2,769.09 μs15.649 μs43.882 μs2,760.85 μs2,691.56 μs2,885.67 μs0.964 KB
ReadAsyncBEFORE1048576004096None115,657.20 μs352.576 μs1,039.577 μs115,639.45 μs113,670.15 μs117,832.15 μs1.002,801 KB
ReadAsyncAFTER1048576004096None116,612.88 μs249.577 μs731.966 μs116,751.05 μs114,615.00 μs117,903.05 μs1.012,801 KB
WriteAsyncBEFORE1048576004096None205,378.59 μs7,785.258 μs22,462.248 μs194,584.50 μs184,125.30 μs268,874.60 μs1.005,005 KB
WriteAsyncAFTER1048576004096None209,453.56 μs10,355.532 μs30,043.276 μs194,064.20 μs173,778.40 μs293,593.30 μs1.035,005 KB
ReadAsyncBEFORE1048576004096Asynchronous327,225.97 μs1,532.011 μs4,346.059 μs326,564.00 μs320,187.10 μs339,508.20 μs1.007,801 KB
ReadAsyncAFTER1048576004096Asynchronous457,654.89 μs902.286 μs2,559.634 μs457,207.00 μs451,652.60 μs465,093.00 μs1.406,801 KB
WriteAsyncBEFORE1048576004096Asynchronous454,994.00 μs7,661.181 μs21,857.780 μs449,253.50 μs421,512.80 μs511,199.10 μs1.007,905 KB
WriteAsyncAFTER1048576004096Asynchronous600,076.62 μs10,088.501 μs29,268.570 μs592,035.20 μs559,265.50 μs677,403.80 μs1.326,905 KB
ReadAsync_NoBufferingBEFORE10485760016384None45,086.23 μs156.169 μs453.074 μs45,005.65 μs43,497.93 μs46,041.93 μs1.00701 KB
ReadAsync_NoBufferingAFTER10485760016384None42,133.53 μs174.922 μs457.740 μs42,093.18 μs41,187.75 μs43,731.75 μs0.93701 KB
WriteAsync_NoBufferingBEFORE10485760016384None61,814.53 μs685.277 μs1,887.453 μs61,419.69 μs59,079.38 μs68,035.43 μs1.00701 KB
WriteAsync_NoBufferingAFTER10485760016384None61,435.05 μs720.962 μs1,973.622 μs61,127.18 μs58,506.18 μs67,827.25 μs0.99701 KB
ReadAsync_NoBufferingBEFORE10485760016384Asynchronous97,220.19 μs667.147 μs1,881.701 μs96,464.10 μs94,386.65 μs102,972.80 μs1.001,951 KB
ReadAsync_NoBufferingAFTER10485760016384Asynchronous128,169.40 μs556.512 μs1,587.760 μs127,771.80 μs125,455.30 μs133,061.15 μs1.321,701 KB
WriteAsync_NoBufferingBEFORE10485760016384Asynchronous136,593.81 μs1,249.681 μs3,462.860 μs136,822.00 μs127,534.30 μs146,281.60 μs1.001,951 KB
WriteAsync_NoBufferingAFTER10485760016384Asynchronous169,656.11 μs2,245.877 μs6,515.696 μs168,284.30 μs157,404.20 μs189,342.10 μs1.251,701 KB
CopyToFileAsyncBEFORE104857600?None56,537.38 μs245.101 μs695.312 μs56,440.93 μs55,567.65 μs58,581.80 μs1.00177 KB
CopyToFileAsyncAFTER104857600?None56,184.32 μs275.348 μs785.585 μs56,116.43 μs54,790.00 μs58,574.72 μs0.99177 KB
CopyToFileAsyncBEFORE104857600?Asynchronous87,668.39 μs312.004 μs905.181 μs87,555.38 μs85,324.65 μs89,838.85 μs1.00246 KB
CopyToFileAsyncAFTER104857600?Asynchronous78,490.65 μs501.805 μs1,431.678 μs78,350.84 μs75,379.32 μs82,799.10 μs0.90214 KB

…Source only needed by Net5CompatFileStreamStrategy.
… one allocation and get the actual profiling improvement.
Rename FileStreamAwaitableProvider.
… review. Shorter filename for awaitable provider.
…trategy, and its _fileHandle can now be private.
@adamsitnik

Copy link
Copy Markdown
Member

Most results had allocation improvements, but worse speed when FileOptions = Asynchronous:

As discussed offline, such a regression needs to be solved before merging the PR.

I've even built your fork and run the benchmarks myself to be 100% sure, but unfortunately, I've confirmed the regression:

MethodToolchainfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsync_NoBuffering\after\corerun.exe104857616384Asynchronous838.1 us1.1217 KB
ReadAsync_NoBuffering\before\corerun.exe104857616384Asynchronous748.1 us1.0020 KB
ReadAsync_NoBuffering\after\corerun.exe10485760016384Asynchronous93,214.6 us1.121,700 KB
ReadAsync_NoBuffering\before\corerun.exe10485760016384Asynchronous83,445.4 us1.001,950 KB

If I can suggest something, ReadAsync_NoBuffering with fileSize=1048576 and options=Asynchronous should be the best benchmark for creating a repo app and profiling it. It's the simplest benchmark (buffering is not involved) and ReadAsync has a smaller deviation than WriteAsync. If you fail to find the regression with VS Profiler, you can use Perfview.

@adamsitnikadamsitnik added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Apr 14, 2021
@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

I compared the CPU usage before and after, and discovered we spend a considerable amount of time inside Overlapped.PerformIOCompletionCallback.

The new code (left) consumes 62% of the time calling ExecutionContext.RunInternal, while the old code (right) only took 3.82%.

Screenshot

image

The root cause seems to be this line, where we call _source.SetResult:

Screenshot

image

Which is adding up the time it takes to run the rest of the ReadAsync calls in the loop, including the disposal of the FileStream object in the main method.

There's a comment stating that ExecutionContext.RunInternal (part of the callstack) warning that the code is a very hot path:

Screenshot

image

I wasn't expecting all the continuation code to be counted as part of the asynchronous calls. I wonder if this has anything to do with the removal of RunContinuationsAsynchronously = true flag. I'll bring that flag back and compare the results.

cc @jozkee@adamsitnik@stephentoub

@davidfowl

Copy link
Copy Markdown
Member

We don't need to restore the ExecutionContext in the overlapped, that restore should be avoided in these APIs. The async state machine already does it. This is what you need #42549 😄

@stephentoub

stephentoub commented Apr 14, 2021

Copy link
Copy Markdown
Member

The new code (left) consumes 62% of the time calling ExecutionContext.RunInternal, while the old code (right) only took 3.82%.

That's inclusive (what "Total" means in the VS view). The callback is actually running the continuation now (because of RunContinuationsAsynchronously=false), rather than queueing it to be run on a different thread, so the 62% is inclusive of the actual app code.

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 know you're copying this logic from what was there before, but it seems to be unnecessarily complex, with lots of interlocked operations for transitioning from one state to another to another. If we're looking for places to reduce overheads, revisiting this whole implementation is likely a good place to start.

@adamsitnik

Copy link
Copy Markdown
Member

The callback is actually running the continuation now (because of RunContinuationsAsynchronously=false)

I've run all the async benchmarks for 3 configurations:

  • "before" (@carlossanlop fork main branch)
  • "false" (the current state of this PR as RunContinuationsAsynchronously is not set so it's false)
  • "true" ( _source.RunContinuationsAsynchronously = true; in ctor)

With the following settings:

  • 100 instead of 20 iterations (--minIterationCount 100 --maxIterationCount 101)
  • no outliers removal (--outliers DontRemove)

and confirmed that with _source.RunContinuationsAsynchronously = true; we have no regression in the microbenchmarks

@stephentoub should we:

  • set RunContinuationsAsynchronously = true; and include this PR in preview4
  • get some "real world" numbers from ASP.NET benchmarks (if there are none, write some and contribute to https://github.com/aspnet/benchmarks) and see how RunContinuationsAsynchronously setting affects non-micro benchmarks. This means preview5 for this PR.
Details
MethodToolchainfileSizeuserBufferSizeoptionsMeanStdDevMedianMinMaxRatioAllocated
ReadAsync\before\corerun.exe10241024Asynchronous82.69 us1.946 us82.22 us81.60 us94.90 us1.005 KB
ReadAsync\false\corerun.exe10241024Asynchronous78.76 us0.718 us78.56 us78.04 us82.13 us0.955 KB
ReadAsync\true\corerun.exe10241024Asynchronous83.28 us0.825 us83.12 us82.48 us89.82 us1.015 KB
WriteAsync\before\corerun.exe10241024Asynchronous404.05 us22.574 us400.81 us379.20 us494.53 us1.005 KB
WriteAsync\false\corerun.exe10241024Asynchronous400.76 us17.211 us397.80 us380.07 us472.21 us0.995 KB
WriteAsync\true\corerun.exe10241024Asynchronous405.20 us25.469 us401.47 us383.59 us510.74 us1.015 KB
ReadAsync\before\corerun.exe1048576512Asynchronous2,418.83 us55.684 us2,413.59 us2,361.95 us2,804.46 us1.0093 KB
ReadAsync\false\corerun.exe1048576512Asynchronous2,648.89 us73.366 us2,626.45 us2,550.29 us2,959.92 us1.1083 KB
ReadAsync\true\corerun.exe1048576512Asynchronous2,386.25 us9.985 us2,386.16 us2,356.79 us2,419.58 us0.9983 KB
WriteAsync\before\corerun.exe1048576512Asynchronous4,254.70 us277.778 us4,213.63 us3,967.55 us6,203.71 us1.0085 KB
WriteAsync\false\corerun.exe1048576512Asynchronous4,583.08 us183.949 us4,559.42 us4,317.29 us5,334.19 us1.0875 KB
WriteAsync\true\corerun.exe1048576512Asynchronous4,207.77 us179.340 us4,173.53 us3,896.65 us4,865.02 us0.9975 KB
ReadAsync\before\corerun.exe10485764096Asynchronous2,343.06 us35.339 us2,355.62 us2,225.73 us2,379.72 us1.0079 KB
ReadAsync\false\corerun.exe10485764096Asynchronous2,603.91 us40.916 us2,603.11 us2,513.55 us2,715.90 us1.1169 KB
ReadAsync\true\corerun.exe10485764096Asynchronous2,324.68 us38.225 us2,337.29 us2,226.14 us2,374.04 us0.9969 KB
WriteAsync\before\corerun.exe10485764096Asynchronous4,110.48 us265.902 us4,050.53 us3,844.25 us6,007.74 us1.0084 KB
WriteAsync\false\corerun.exe10485764096Asynchronous4,572.16 us320.658 us4,542.09 us4,256.30 us7,440.25 us1.1274 KB
WriteAsync\true\corerun.exe10485764096Asynchronous4,048.01 us184.553 us4,015.01 us3,739.28 us4,754.61 us0.9974 KB
ReadAsync_NoBuffering\before\corerun.exe104857616384Asynchronous747.53 us5.076 us747.87 us729.80 us755.69 us1.0020 KB
ReadAsync_NoBuffering\false\corerun.exe104857616384Asynchronous822.87 us6.877 us822.90 us809.94 us838.18 us1.1017 KB
ReadAsync_NoBuffering\true\corerun.exe104857616384Asynchronous726.82 us11.262 us726.44 us699.96 us754.36 us0.9717 KB
WriteAsync_NoBuffering\before\corerun.exe104857616384Asynchronous2,726.54 us136.755 us2,712.48 us2,534.40 us3,234.12 us1.0020 KB
WriteAsync_NoBuffering\false\corerun.exe104857616384Asynchronous2,761.05 us146.361 us2,792.11 us2,507.70 us3,188.16 us1.0117 KB
WriteAsync_NoBuffering\true\corerun.exe104857616384Asynchronous2,736.46 us194.495 us2,705.98 us2,415.11 us4,022.13 us1.0117 KB
ReadAsync\before\corerun.exe1048576004096Asynchronous250,490.85 us2,604.707 us250,256.45 us244,814.30 us256,607.90 us1.007,801 KB
ReadAsync\false\corerun.exe1048576004096Asynchronous278,260.89 us4,931.050 us278,009.45 us269,882.60 us297,338.40 us1.116,803 KB
ReadAsync\true\corerun.exe1048576004096Asynchronous252,682.05 us19,763.442 us250,702.50 us243,911.20 us447,534.10 us1.016,801 KB
WriteAsync\before\corerun.exe1048576004096Asynchronous370,893.59 us6,590.799 us369,781.05 us352,834.20 us390,366.40 us1.007,905 KB
WriteAsync\false\corerun.exe1048576004096Asynchronous397,619.03 us8,020.098 us397,052.95 us384,609.00 us446,559.70 us1.076,905 KB
WriteAsync\true\corerun.exe1048576004096Asynchronous358,128.05 us6,536.171 us357,755.45 us346,707.00 us382,735.70 us0.976,905 KB

@adamsitnikadamsitnik removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Apr 15, 2021

@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.

LGTM, thank you @carlossanlop !

@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

With the unit tests passing, the micro benchmarks having no regressions after reverting RunContinuationsAsynchronously to true, and having allocation improvements, I feel confident in merging this change to include it in Preview4, and will continue working on #50972 to add caching and improve this code for Preview5.

@carlossanlop
carlossanlop merged commit a38d0c2 into dotnet:mainApr 15, 2021
@carlossanlop
carlossanlop deleted the IValueTaskSource branch April 15, 2021 22:51
@ghostghost locked as resolved and limited conversation to collaborators May 15, 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.

5 participants

@carlossanlop@davidfowl@stephentoub@adamsitnik@jozkee
, '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: Use IValueTaskSource instead of TaskCompletionSource - #50802

Merged
carlossanlop merged 19 commits into
dotnet:mainfrom
carlossanlop:IValueTaskSource
Apr 15, 2021
Merged

FileStream rewrite: Use IValueTaskSource instead of TaskCompletionSource#50802
carlossanlop merged 19 commits into
dotnet:mainfrom
carlossanlop:IValueTaskSource

Conversation

@carlossanlop

Copy link
Copy Markdown
Contributor

The purpose of this PR is to remove a large allocation found in FileStream.ReadAsync and FileStream.WriteAsync, which we confirmed in our profiling investigation around the new FileStream rewrite code.

The allocation improvement can be achieved by creating a new type, called FileStreamValueTaskSource, that will have the same purpose as FileStreamCompletionSource, but instead of implementing TaskCompletionSource (which returns a Task to represent the asynchronous work) it implements IValueTaskSource, which returns a ValueTask.

The behavior was largely left untouched, except for the following differences:

  • Net5CompatFileStreamStrategy.Windows will be the only strategy that consumes the FileStreamCompletionSource, so it is now a private nested class.
  • One single new type implements both IValueTaskSource (used for write) and IValueTaskSource<int> (used for read).
  • The logic inside the callback and the cancellation token registration is the same, aside for the code that needs to report exceptions or return values.
  • We had interop errors redefined in multiple places.
  • The constants used by FileStreamCompletionSource were moved to FileStreamHelpers so they could be reused by the new type.

Unit tests

Manually ran unit tests from System.IO.FileSystem, System.IO, System.Runtime and System.Runtime.Extensions, they all passed. I made sure to run them with the Net5Compat flag turned off, to ensure my new code was executed.

Benchmarks

  • Ran the FileStream performance benchmarks against .NET 5.0, then against the code of this PR's baseline commit, and then against this PR's new code. I compared the results, and there are subtle improvements in some benchmark tests:
.NET 5.0 vs .NET 6.0 before this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_before_pr' --threshold 0.0001%

summary:
better: 39, geomean: 2.801
worse: 22, geomean: 2.098
total diff: 61

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.93882252.574348913.28
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.621003671.884633945.31bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.13257218.23805205.31several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.13286859.38896524.31
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.061795459.385498006.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.051868294.535689532.29
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)2.99254148.29758897.83
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.97283860.42843052.43
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.901844293.365353701.04several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.892209384.386395554.17bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.65301619.83798302.63
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.86557062.051035575.21bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.81478821.50868866.12
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.693479778.755891160.42several?
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.66543512.61903388.82bimodal
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.40168291900.00235314600.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.248197915.6310200393.75
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.215313143.756444323.96
Perf_FileStream.Flush(fileSize: 1024, options: None)1.183448111.254052132.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055473900.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.0956033900.0061052537.50
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.03133329050.00137489800.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.645489541.6747470.79
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.902811281.2543319.93
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.822571736.4644479.08
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.29876923.2641197.72
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.062463834750.00486578800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.94152781.6630909.09
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.72153731.8132593.67
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437889.31several?
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.98165696.6841632.18
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.97159526.5840152.10
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.50200423.3657204.05
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889181.93
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.55238920.2293543.27
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.50193629.0877357.59
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.443222202.501318286.88
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.39194808.8781591.76
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.36216916.9892096.58
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.24325116900.00145245400.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.157025697.923271982.50
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.10199647.8995019.35
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.04686776300.00336991400.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9519598723.0810074393.75
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)1.85508393800.00274360000.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.74796591200.00457934500.00bimodal
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.73156593600.0090313262.50
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.7022492768.7513248333.33
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64542213800.00331399500.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.615221529.173244615.63
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114593046.88
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742610.27
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09777023.21
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.191301925.001096436.88
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.12142000.45126451.42
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 512, options: None)1.11162435300.00146191400.00
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.10141476.75128484.00
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.10142083.81129048.19
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00113456750.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0288348175.0086894587.50
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 512, options: None)1.0192483275.0091193650.00
.NET 5.0 vs .NET 6.0 after this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_after_pr' --threshold 0.0001%
summary:
better: 36, geomean: 3.006
worse: 22, geomean: 2.080
total diff: 58

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.92882252.574341552.34
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.601003671.884615128.91bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)3.24283860.42919970.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)3.08254148.29783231.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.061868294.535710579.17
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.05286859.38874430.38
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.04257218.23780960.42
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)2.971795459.385329393.75
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.951844293.365432295.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.922209384.386445677.08
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.52301619.83758761.88
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.95557062.051087235.83
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.73543512.61937893.38bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.66478821.50793440.46bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.473479778.755113488.54several?
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.32168291900.00221660000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.235313143.756512252.08
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.228197915.6310012295.65
Perf_FileStream.Flush(fileSize: 1024, options: None)1.213448111.254163660.16
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1356033900.0063237650.00
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0950055150.0054423625.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0088983100.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.505489541.6747526.62
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.942811281.2543292.23
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.062571736.4645070.47
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.05876923.2641661.14
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.362463834750.00459292800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.95152781.6630850.74
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.75153731.8132349.21
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437926.07several?
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.98159526.5840096.31
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.96165696.6841824.65
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889051.28
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.56238920.2293276.60
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.53193629.0876383.11
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.45194808.8779416.04
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501376330.63
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.31216916.9893711.06
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.28325116900.00142880200.00
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)2.22200423.3690245.25
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.167025697.923250746.88
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.11508393800.00241032100.00
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.11199647.8994675.64
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.06686776300.00333108950.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9319598723.0810171781.82
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.74156593600.0090035450.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.65542213800.00329569650.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.645221529.173182177.50
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64796591200.00486630000.00
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.5222492768.7514797453.85
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114625460.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742460.71
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09781337.50
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.201301925.001085495.19
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.11141476.75127158.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.11142083.81128134.40
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.10142000.45129068.15
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00112942550.00

Profiling

I reused the same code we have in our benchmarks, but I executed it inside a console app that forces the consumption of the compiled runtime code.

You'll notice the top Task allocation went away, and the FileStreamCompletionSource allocations were substituted by FileStreamValueTaskSource allocations.

ReadAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,797
- System.String2,153
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,806
- System.String2,153
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]964
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

ReadAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,013
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,840
- System.String2,153
- System.Object[]955
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,836
- System.String2,144
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>64,235
- System.Threading.Tasks.Task<>64,015
- System.IO.FileStreamCompletionSource64,000
- System.SByte[]2,916
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,812
- System.SByte[]2,909
- System.String2,144
- System.Object[]964
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,798
- System.String2,153
- System.Object[]954
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,812
- System.String2,153
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]958
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,249
- System.IO.MemoryFileStreamCompletionSource32,000
- System.IO.FileStreamCompletionSource32,000
- System.SByte[]2,915
- System.String2,155
- System.Object[]952
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,246
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource32,000
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource32,000
- System.SByte[]2,893
- System.String2,144
- System.Object[]954
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.IO.FileStreamCompletionSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,963
- System.SByte[]2,915
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,830
- System.SByte[]2,912
- System.String2,144
- System.Object[]966
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Threading.Tasks.ValueTask.ValueTaskSourceAsTask249
- System.Threading.QueueUserWorkItemCallbackDefaultContext<>249

@adamsitnik@jozkee@jeffhandley@stephentoub

Also: Don't forget to ignore whitespace changes so it's easier to review the nested type's changes 😃

@ghost

ghost commented Apr 6, 2021

Copy link
Copy Markdown

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

Issue Details

The purpose of this PR is to remove a large allocation found in FileStream.ReadAsync and FileStream.WriteAsync, which we confirmed in our profiling investigation around the new FileStream rewrite code.

The allocation improvement can be achieved by creating a new type, called FileStreamValueTaskSource, that will have the same purpose as FileStreamCompletionSource, but instead of implementing TaskCompletionSource (which returns a Task to represent the asynchronous work) it implements IValueTaskSource, which returns a ValueTask.

The behavior was largely left untouched, except for the following differences:

  • Net5CompatFileStreamStrategy.Windows will be the only strategy that consumes the FileStreamCompletionSource, so it is now a private nested class.
  • One single new type implements both IValueTaskSource (used for write) and IValueTaskSource<int> (used for read).
  • The logic inside the callback and the cancellation token registration is the same, aside for the code that needs to report exceptions or return values.
  • We had interop errors redefined in multiple places.
  • The constants used by FileStreamCompletionSource were moved to FileStreamHelpers so they could be reused by the new type.

Unit tests

Manually ran unit tests from System.IO.FileSystem, System.IO, System.Runtime and System.Runtime.Extensions, they all passed. I made sure to run them with the Net5Compat flag turned off, to ensure my new code was executed.

Benchmarks

  • Ran the FileStream performance benchmarks against .NET 5.0, then against the code of this PR's baseline commit, and then against this PR's new code. I compared the results, and there are subtle improvements in some benchmark tests:
.NET 5.0 vs .NET 6.0 before this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_before_pr' --threshold 0.0001%

summary:
better: 39, geomean: 2.801
worse: 22, geomean: 2.098
total diff: 61

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.93882252.574348913.28
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.621003671.884633945.31bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.13257218.23805205.31several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.13286859.38896524.31
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.061795459.385498006.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.051868294.535689532.29
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)2.99254148.29758897.83
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.97283860.42843052.43
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.901844293.365353701.04several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.892209384.386395554.17bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.65301619.83798302.63
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.86557062.051035575.21bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.81478821.50868866.12
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.693479778.755891160.42several?
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.66543512.61903388.82bimodal
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.40168291900.00235314600.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.248197915.6310200393.75
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.215313143.756444323.96
Perf_FileStream.Flush(fileSize: 1024, options: None)1.183448111.254052132.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055473900.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.0956033900.0061052537.50
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.03133329050.00137489800.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.645489541.6747470.79
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.902811281.2543319.93
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.822571736.4644479.08
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.29876923.2641197.72
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.062463834750.00486578800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.94152781.6630909.09
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.72153731.8132593.67
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437889.31several?
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.98165696.6841632.18
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.97159526.5840152.10
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.50200423.3657204.05
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889181.93
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.55238920.2293543.27
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.50193629.0877357.59
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.443222202.501318286.88
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.39194808.8781591.76
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.36216916.9892096.58
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.24325116900.00145245400.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.157025697.923271982.50
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.10199647.8995019.35
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.04686776300.00336991400.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9519598723.0810074393.75
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)1.85508393800.00274360000.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.74796591200.00457934500.00bimodal
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.73156593600.0090313262.50
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.7022492768.7513248333.33
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64542213800.00331399500.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.615221529.173244615.63
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114593046.88
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742610.27
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09777023.21
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.191301925.001096436.88
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.12142000.45126451.42
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 512, options: None)1.11162435300.00146191400.00
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.10141476.75128484.00
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.10142083.81129048.19
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00113456750.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0288348175.0086894587.50
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 512, options: None)1.0192483275.0091193650.00
.NET 5.0 vs .NET 6.0 after this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_after_pr' --threshold 0.0001%
summary:
better: 36, geomean: 3.006
worse: 22, geomean: 2.080
total diff: 58

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.92882252.574341552.34
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.601003671.884615128.91bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)3.24283860.42919970.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)3.08254148.29783231.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.061868294.535710579.17
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.05286859.38874430.38
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.04257218.23780960.42
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)2.971795459.385329393.75
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.951844293.365432295.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.922209384.386445677.08
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.52301619.83758761.88
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.95557062.051087235.83
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.73543512.61937893.38bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.66478821.50793440.46bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.473479778.755113488.54several?
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.32168291900.00221660000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.235313143.756512252.08
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.228197915.6310012295.65
Perf_FileStream.Flush(fileSize: 1024, options: None)1.213448111.254163660.16
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1356033900.0063237650.00
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0950055150.0054423625.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0088983100.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.505489541.6747526.62
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.942811281.2543292.23
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.062571736.4645070.47
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.05876923.2641661.14
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.362463834750.00459292800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.95152781.6630850.74
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.75153731.8132349.21
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437926.07several?
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.98159526.5840096.31
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.96165696.6841824.65
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889051.28
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.56238920.2293276.60
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.53193629.0876383.11
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.45194808.8779416.04
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501376330.63
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.31216916.9893711.06
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.28325116900.00142880200.00
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)2.22200423.3690245.25
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.167025697.923250746.88
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.11508393800.00241032100.00
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.11199647.8994675.64
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.06686776300.00333108950.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9319598723.0810171781.82
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.74156593600.0090035450.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.65542213800.00329569650.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.645221529.173182177.50
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64796591200.00486630000.00
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.5222492768.7514797453.85
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114625460.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742460.71
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09781337.50
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.201301925.001085495.19
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.11141476.75127158.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.11142083.81128134.40
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.10142000.45129068.15
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00112942550.00

Profiling

I reused the same code we have in our benchmarks, but I executed it inside a console app that forces the consumption of the compiled runtime code.

You'll notice the top Task allocation went away, and the FileStreamCompletionSource allocations were substituted by FileStreamValueTaskSource allocations.

ReadAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,797
- System.String2,153
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,806
- System.String2,153
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]964
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

ReadAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,013
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,840
- System.String2,153
- System.Object[]955
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,836
- System.String2,144
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>64,235
- System.Threading.Tasks.Task<>64,015
- System.IO.FileStreamCompletionSource64,000
- System.SByte[]2,916
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,812
- System.SByte[]2,909
- System.String2,144
- System.Object[]964
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,798
- System.String2,153
- System.Object[]954
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,812
- System.String2,153
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]958
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,249
- System.IO.MemoryFileStreamCompletionSource32,000
- System.IO.FileStreamCompletionSource32,000
- System.SByte[]2,915
- System.String2,155
- System.Object[]952
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,246
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource32,000
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource32,000
- System.SByte[]2,893
- System.String2,144
- System.Object[]954
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.IO.FileStreamCompletionSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,963
- System.SByte[]2,915
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,830
- System.SByte[]2,912
- System.String2,144
- System.Object[]966
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Threading.Tasks.ValueTask.ValueTaskSourceAsTask249
- System.Threading.QueueUserWorkItemCallbackDefaultContext<>249

@adamsitnik@jozkee@jeffhandley@stephentoub

Also: Don't forget to ignore whitespace changes so it's easier to review the nested type's changes 😃
Author:carlossanlop
Assignees:carlossanlop
Labels:

area-System.IO

Milestone:6.0.0

@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

Tagging subscribers to this area: @carlossanlop

Yes, thank you bot... 😆

@jozkeejozkee 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.

Otherwise; LGTM, will approve if/when comments are resolved, most of them are nits and/or questions.

Comment on lines +17 to +22
internal const long NoResult = 0;
internal const long ResultSuccess = (long)1 << 32;
internal const long ResultError = (long)2 << 32;
internal const long RegisteringCancellation = (long)4 << 32;
internal const long CompletedCallback = (long)8 << 32;
internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32;

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.

Consider prefixing this constants with AsyncCompletionSource_ or something alike since these names are too ambiguous or keep them in their respective classes regardless of them being duplicated.

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.

or put them into a nested static class called ReturnCodes:

internalstaticpartialclassFileStreamHelpers{internalstaticclassReturnCodes{internalconstlongNoResult=0;internalconstlongResultSuccess=(long)1<<32;internalconstlongResultError=(long)2<<32;internalconstlongRegisteringCancellation=(long)4<<32;internalconstlongCompletedCallback=(long)8<<32;internalconstulongResultMask=((ulong)uint.MaxValue)<<32;}

but it's a nit (not a blocker)

Comment on lines +66 to +67
_strategy.CompareExchangeCurrentOverlappedOwner(this, null) == null ?
_strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!) : // allocated when buffer was created, and buffer is non-null

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.

Suggested change
_strategy.CompareExchangeCurrentOverlappedOwner(this,null)==null?
_strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!):// allocated when buffer was created, and buffer is non-null
strategy.CompareExchangeCurrentOverlappedOwner(this,null)==null?
strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!):// allocated when buffer was created, and buffer is non-null

// be directly the AwaitableProvider that's completing (in the case where the preallocated
// overlapped was already in use by another operation).
object? state = ThreadPoolBoundHandle.GetNativeOverlappedState(pOverlapped);
Debug.Assert(state is (AsyncWindowsFileStreamStrategy or FileStreamValueTaskSource));

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.

Suggested change
Debug.Assert(stateis(AsyncWindowsFileStreamStrategy or FileStreamValueTaskSource));
Debug.Assert(stateisAsyncWindowsFileStreamStrategy or FileStreamValueTaskSource);

int errorCode = unchecked((int)(packedResult & uint.MaxValue));
if (errorCode == Interop.Errors.ERROR_OPERATION_ABORTED)
{
_source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(new OperationCanceledException(cancellationToken.IsCancellationRequested ? cancellationToken : new CancellationToken(canceled: true))));

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.

Suggested change
_source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(newOperationCanceledException(cancellationToken.IsCancellationRequested?cancellationToken:newCancellationToken(canceled:true))));
Exceptione=newOperationCanceledException(cancellationToken.IsCancellationRequested?cancellationToken:newCancellationToken(canceled:true));
e.SetCurrentStackTrace();
_source.SetException(e);

}

private void Cancel(CancellationToken token)
{

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.

Missing comment and asserts:

// WARNING: This may potentially be called under a lock (during cancellation registration)
Debug.Assert(stateisFileStreamCompletionSource,"Unknown state passed to cancellation");
FileStreamCompletionSourcecompletionSource=(FileStreamCompletionSource)state;
Debug.Assert(completionSource._overlapped!=null&&!completionSource.Task.IsCompleted,"IO should not have completed yet");

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The first assert is not needed because the method argument is not an object? state.

I'll add the assert that checks for overlapped an completed status.

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.

The state param could be added to this method and allows you to conserve the first Debug.Assert.

long packedResult = Interlocked.CompareExchange(ref _result, FileStreamHelpers.RegisteringCancellation, FileStreamHelpers.NoResult);
if (packedResult == FileStreamHelpers.NoResult)
{
_cancellationRegistration = cancellationToken.UnsafeRegister(static (s, token) => ((FileStreamValueTaskSource)s!).Cancel(token), this);

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.

Could we pass the callback on a similar fashion to FileStreamCompletionSource

_cancellationRegistration=cancellationToken.UnsafeRegister(cancelCallback,this);

Or why did you opted for this way instead?

@carlossanlopcarlossanlopApr 8, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It made more sense to pass this as the second argument of UnsafeRegister, which ensures it's a ValueTaskSource instance, and saves us from the check of the object? state parameter like in the old code. The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

We have a similar example in System.Threading.Channels.

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.

It made more sense to pass this as the second argument of UnsafeRegister

FileStreamCompletionSource was also passing this as 2nd argument.

The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

Yes, seems that it was caching the method group to avoid allocating everytime it was passed to UnsafeRegister.
However I think you can avoid the allocation without caching yourself by doing this:

_cancellationRegistration=cancellationToken.UnsafeRegister((s,token)=>Cancel(s,token),this);

Just a suggestion, not something that you should block on.

@davidfowl

Copy link
Copy Markdown
Member

This is the change I was waiting for 👏🏾👏🏾

@davidfowl

Copy link
Copy Markdown
Member

I haven't looked but I assume we'll reuse the operation objects if there's no concurrency? This is what sockets do today so in the 95% case of no overlapping writes or reads, there'll be a single reusable allocation on the FileStream

@stephentoub

Copy link
Copy Markdown
Member

This is the change I was waiting for

It's not, actually. You want the next one where the IValueTaskSource instance is cached.

}
// else: Some other thread stole the result, so now it is responsible to finish the callback
}
// else: Some other thread is registering a cancellation, so it *must* finish the callback

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.

All this logic looks more complicated than it needs to be. I realize you're just following the same flow as what was already there, so nothing needs to be changed in this PR. But we should revisit all of this synchronization later; I suspect we can do it more succinctly and efficiently.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I opened an issue to follow up on this and on the caching of the IValueTaskSource.
#50972

@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 before we merge I would like to see the following benchmark results:

.NET 6 with new strategy enabled: before and after your change. (not .NET 5 vs 6 as there are too many moving targets)

You should be able to achieve that by running the following command in the perf repo;

git clone https://github.com/dotnet/performance.git
cd performance\src\benchmarks\micro
dotnet run -c Release -f net6.0 --filter *Perf_FileStream* --coreRun $pathToCoreRunWithoutYourChanges $pathToCoreRunWithYourChanges --envVars "DOTNET_SYSTEM_IO_USENET5COMPATFILESTREAM:0"

/// <see cref="MemoryHandle"/> when the operation has completed. This should only be used
/// when memory doesn't wrap a byte[].
/// </summary>
private sealed class MemoryAwaitableProvider : FileStreamValueTaskSource

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.

This should only be used when memory doesn't wrap a byte[].

I just realized that we don't have benchmarks for this scenario as Memory used in our micro-benchmarks always wraps a byte array. Could you please add such benchmarks to https://github.com/dotnet/performance/blob/main/src/benchmarks/micro/libraries/System.IO.FileSystem/Perf.FileStream.cs ? One test case should be enough as this should be rare.

@carlossanlopcarlossanlopApr 7, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Some of the benchmarks do consume this type. If you look at some of the profiling tests I executed, you'll see that MemoryValueTaskSource shows up a few times. The profiling tests reuse the code we had in our benchmarks.

I don't mind adding an explicit one though. Dou you want me to add one anyway?

Comment on lines +17 to +22
internal const long NoResult = 0;
internal const long ResultSuccess = (long)1 << 32;
internal const long ResultError = (long)2 << 32;
internal const long RegisteringCancellation = (long)4 << 32;
internal const long CompletedCallback = (long)8 << 32;
internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32;

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.

or put them into a nested static class called ReturnCodes:

internalstaticpartialclassFileStreamHelpers{internalstaticclassReturnCodes{internalconstlongNoResult=0;internalconstlongResultSuccess=(long)1<<32;internalconstlongResultError=(long)2<<32;internalconstlongRegisteringCancellation=(long)4<<32;internalconstlongCompletedCallback=(long)8<<32;internalconstulongResultMask=((ulong)uint.MaxValue)<<32;}

but it's a nit (not a blocker)

Comment on lines 62 to 63

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.

Did you run the perf. benchmarks after doing this change and did you notice any impact?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Pending.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Confirmed - There is a slight improvement.

I made sure to compare apples to apples: I used my latest commit, and built it setting RunContinuationsAsynchronously to true, then to false. I compared it both times against our code in .NET 5.0.

The improvement is clear when setting it to false (or not setting it at all).

RunContinuationsAsynchronously = true

❯ dotnet run -c release --base "D:\fs50" --diff "D:\fs60_NEW_ON_IVTS_asynctrue" --threshold 0.0001%
summary:
better: 24, geomean: 3.402
worse: 19, geomean: 2.244
total diff: 43

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)5.49882252.574840275.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)5.121003671.885136350.00bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)4.44257218.231141827.01
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)4.08286859.381171600.96
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.251795459.385832233.33
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)3.111844293.365741847.92several?
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.111868294.535807825.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)3.082209384.386796443.75
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)2.46557062.051371566.15
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)2.33478821.501116033.48
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)2.28543512.611239956.25bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.613479778.755612725.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.448197915.6311814575.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.33168291900.00224355000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.335313143.757063218.75
Perf_FileStream.Flush(fileSize: 1024, options: None)1.253448111.254313832.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1250055150.0056309025.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1256033900.0062803900.00
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.12133329050.00148833000.00bimodal
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)117.075489541.6746889.31
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)65.932811281.2542642.74
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.842571736.4644462.45
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.09876923.2641586.04
Perf_FileStream.OpenClose(fileSize: 1024, options: None)5.00152781.6630548.62
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.76153731.8132314.04
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)4.032463834750.00611183400.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.93165696.6842208.34
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.52200423.3656865.52
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.62230670.3887990.73
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.58238920.2292478.58
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501377939.84
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.87156593600.0083681962.50
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.6619598723.0811803641.67
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.6322492768.7513818900.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.577025697.924487121.88
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)1.47686776300.00466936500.00bimodal
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55743825.45
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.24975146.09784156.88
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.195221529.174387085.94
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.181301925.001103803.79
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.04116486775.00112407200.00
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.0321953590.9121241122.73
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0087467100.00
RunContinuationsAsynchronously = false

dotnet run -c release --base "D:\fs50" --diff "D:\fs60_NEW_ON_IVTS_asyncfalse" --threshold 0.0001%
summary:
better: 23, geomean: 3.592
worse: 20, geomean: 2.135
total diff: 43

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)5.36882252.574730875.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)5.051003671.885071912.50bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)4.25257218.231094164.38several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)4.05286859.381161160.27
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.231795459.385791481.25
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)3.101844293.365709820.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)3.092209384.386836504.17
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.071868294.535733883.33
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)2.46557062.051368034.90
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)2.31478821.501103696.25
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)2.24543512.611218234.38bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.603479778.755572682.29
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.428197915.6311634850.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.33168291900.00223141800.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.315313143.756949831.25
Perf_FileStream.Flush(fileSize: 1024, options: None)1.263448111.254332915.63
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.13133329050.00150223800.00bimodal
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055699050.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1056033900.0061710550.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0088348175.0088759625.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)116.975489541.6746929.42
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)65.762811281.2542753.29
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.562571736.4644676.15
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.52876923.2640756.47
Perf_FileStream.OpenClose(fileSize: 1024, options: None)5.06152781.6630216.63
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.81153731.8131975.57
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)4.052463834750.00608864800.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.99165696.6841526.73
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.54200423.3656610.08
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.62230670.3887877.10
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.59238920.2292291.75
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.403222202.501345116.25
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.86156593600.0083969875.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.6419598723.0811925173.68
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.6222492768.7513859764.71
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.567025697.924493881.25
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)1.46686776300.00470158300.00bimodal
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.32987460.55750294.64
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.24975146.09784056.56
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.171301925.001108880.36
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.175221529.174470673.44
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.0721953590.9120580725.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.01116486775.00

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Here is the result comparison if we compare the benchmark results of setting it to true vs false:

base: RunContinuationsAsynchronously=true, diff: RunContinuationsAsynchronously=false

dotnet run -c release --base "D:\fs60_NEW_ON_IVTS_asynctrue" --diff "D:\fs60_NEW_ON_IVTS_asyncfalse" --threshold 0.0001%
summary:
better: 14, geomean: 1.018
worse: 11, geomean: 1.017
total diff: 25

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.WriteAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0567143550.0070259187.50
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.02112407200.00115107550.00
Perf_FileStream.Read_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.02240016.67244921.93
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.024387085.944470673.44
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0187467100.0088759625.00
Perf_FileStream.WriteAsync_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.0121684354.5521982809.09
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: Asynchronous)1.011236709.901253242.19
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 1024, options: None)1.0139289.5439730.73
Perf_FileStream.Write_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.0121445427.2721646650.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.01743825.45750294.64
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)1.0044462.4544676.15
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.WriteByte(fileSize: 1024, options: None)1.041141827.011094164.38several?
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0345688612.5044474575.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.021377939.841345116.25
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)1.024840275.004730875.00
Perf_FileStream.SeekForward(fileSize: 1024, options: None)1.0241586.0440756.47
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: Asynchronous)1.02130849050.00128463775.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)1.0242208.3441526.73
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.0211814575.0011634850.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)1.015807825.005733883.33
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)1.015136350.005071912.50
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0156309025.0055699050.00
Perf_FileStream.Read_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0133747814.293338345.14
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)1.0132314.0431975.57
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 1024, options: Asynchronous)1.0081988.0981641.78

long packedResult = Interlocked.CompareExchange(ref _result, FileStreamHelpers.RegisteringCancellation, FileStreamHelpers.NoResult);
if (packedResult == FileStreamHelpers.NoResult)
{
_cancellationRegistration = cancellationToken.UnsafeRegister(static (s, token) => ((FileStreamValueTaskSource)s!).Cancel(token), this);

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.

It made more sense to pass this as the second argument of UnsafeRegister

FileStreamCompletionSource was also passing this as 2nd argument.

The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

Yes, seems that it was caching the method group to avoid allocating everytime it was passed to UnsafeRegister.
However I think you can avoid the allocation without caching yourself by doing this:

_cancellationRegistration=cancellationToken.UnsafeRegister((s,token)=>Cancel(s,token),this);

Just a suggestion, not something that you should block on.

Comment on lines 120 to 122

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.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I tried, but a ValueTask does not have a Result property, like a ValueTask<T> does, because it returns void. So I'm not sure if it's possible to apply an equivalent pattern.

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.

publicoverridevoidWrite(byte[]buffer,intoffset,intcount){ValueTaskvt=WriteAsyncInternal(newReadOnlyMemory<byte>(buffer,offset,count),CancellationToken.None);if(vt.IsCompleted){vt.GetAwaiter().GetResult();}else{vt.AsTask().GetAwaiter().GetResult();}}

That said, it's not much of an issue for this case. The read side is an issue because AsTask() will almost certainly need to allocate a new Task<int> in order to hand back the result, but for the write side, AsTask() on an already completed ValueTask will just return Task.Completed, assuming the operation completed successfully. So the only time the existing code for the write side will allocate is when the operation was canceled or faulted, in which case we're about to throw an exception anyway, and the extra task allocation won't really matter. Up to you whether you want to change it.

@jozkeejozkee 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.

LGTM; thanks Carlos!

@adamsitnik

Copy link
Copy Markdown
Member

I compared it both times against our code in .NET 5.0.

Comparison against 5.0 is a comparison of all of the changes that were merged to main since we have snapped 5.0 with your change against 5.0. What we care about is how your change (and nothing else) is affecting the perf, so we need to isolate the runs to only your change.

To be able to do that you need to run the benchmarks using two CoreRuns:

  • as a baseline the corerun that was built from main when you have created a new branch, without any of your changes
  • your branch with your changes

My personal workflow:

  1. When I create a new branch, I perform a full build in Release:
build -c Release -subset clr+libs
  1. I copy the folder that contains CoreRun.exe (runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0) to a separate folder. Typically I call it "before".
  2. I apply some changes and perform an incremental Release build. In case of System.Private.CoreLib.dll it's:
build -c Release -subset clr.corelib+clr.nativecorelib+libs.PreTest
  1. I run the benchmarks using the old and new CoreRun.exe:
dotnet run -c Release -f net6.0 --filter *ABC* --corerun C:\Projects\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\before\CoreRun.exe C:\Projects\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe

@carlossanlop

carlossanlop commented Apr 13, 2021

Copy link
Copy Markdown
ContributorAuthor
  • as a baseline the corerun that was built from main when you have created a new branch, without any of your changes
  • your branch with your changes

@adamsitnik@jozkee here's the table comparing the before (original) and after of this PR. Most results had allocation improvements, but worse speed when FileOptions = Asynchronous:

Results

Command:

 dotnet run
-c Release
-f net6.0
--corerun
D:\runtime2\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe
D:\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe
--filter System.IO.Tests.Perf_FileStream.*Async*
--statisticalTest 3ms
--minIterationCount 100
--maxIterationCount 101
--artifacts "D:\finalresults"

Note: The runtime2 corerun contains the baseline commit before my changes, the runtime corerun contains this PR's changes.

MethodToolchainfileSizeuserBufferSizeoptionsMeanErrorStdDevMedianMinMaxRatioAllocated
ReadAsyncBEFORE10241024None78.69 μs0.255 μs0.736 μs78.53 μs77.61 μs80.66 μs1.005 KB
ReadAsyncAFTER10241024None79.10 μs0.237 μs0.673 μs79.08 μs76.96 μs80.71 μs1.015 KB
WriteAsyncBEFORE10241024None316.97 μs2.512 μs6.919 μs315.92 μs305.11 μs339.35 μs1.004 KB
WriteAsyncAFTER10241024None300.13 μs5.643 μs15.730 μs297.59 μs272.46 μs348.15 μs0.954 KB
ReadAsyncBEFORE10241024Asynchronous94.13 μs0.259 μs0.731 μs94.02 μs92.27 μs96.16 μs1.005 KB
ReadAsyncAFTER10241024Asynchronous85.48 μs0.842 μs2.291 μs85.11 μs82.58 μs92.09 μs0.915 KB
WriteAsyncBEFORE10241024Asynchronous292.63 μs5.659 μs15.869 μs290.59 μs264.25 μs338.98 μs1.005 KB
WriteAsyncAFTER10241024Asynchronous324.35 μs13.906 μs39.222 μs311.27 μs274.34 μs428.64 μs1.115 KB
FlushAsyncBEFORE1024?None4,606.81 μs46.736 μs133.341 μs4,633.92 μs4,374.04 μs4,952.63 μs1.00269 KB
FlushAsyncAFTER1024?None4,587.88 μs58.046 μs168.403 μs4,638.98 μs4,322.79 μs5,034.48 μs1.00269 KB
CopyToFileAsyncBEFORE1024?None474.57 μs5.497 μs15.141 μs474.66 μs440.31 μs520.31 μs1.005 KB
CopyToFileAsyncAFTER1024?None488.94 μs3.631 μs10.061 μs488.62 μs469.93 μs518.76 μs1.035 KB
FlushAsyncBEFORE1024?Asynchronous14,680.35 μs150.967 μs437.982 μs14,637.76 μs13,623.50 μs15,645.01 μs1.00301 KB
FlushAsyncAFTER1024?Asynchronous20,333.22 μs116.277 μs339.185 μs20,299.93 μs19,724.25 μs21,148.73 μs1.39261 KB
CopyToFileAsyncBEFORE1024?Asynchronous520.97 μs5.982 μs16.674 μs519.35 μs490.05 μs572.29 μs1.006 KB
CopyToFileAsyncAFTER1024?Asynchronous523.30 μs6.294 μs17.650 μs520.91 μs479.14 μs583.85 μs1.006 KB
ReadAsyncBEFORE1048576512None1,375.28 μs5.930 μs17.015 μs1,376.60 μs1,340.44 μs1,413.73 μs1.0085 KB
ReadAsyncAFTER1048576512None1,347.16 μs5.128 μs14.958 μs1,345.14 μs1,323.57 μs1,382.14 μs0.9885 KB
WriteAsyncBEFORE1048576512None3,530.03 μs38.149 μs108.222 μs3,521.78 μs3,299.91 μs3,823.63 μs1.0076 KB
WriteAsyncAFTER1048576512None3,473.85 μs47.943 μs134.438 μs3,469.00 μs3,222.90 μs3,832.17 μs0.9876 KB
ReadAsyncBEFORE1048576512Asynchronous3,278.98 μs30.616 μs87.844 μs3,257.98 μs3,151.46 μs3,507.69 μs1.0093 KB
ReadAsyncAFTER1048576512Asynchronous4,399.34 μs16.153 μs47.119 μs4,401.64 μs4,294.71 μs4,531.75 μs1.3483 KB
WriteAsyncBEFORE1048576512Asynchronous5,304.10 μs73.935 μs214.500 μs5,309.27 μs4,771.82 μs5,852.43 μs1.0085 KB
WriteAsyncAFTER1048576512Asynchronous6,506.15 μs94.189 μs271.756 μs6,505.61 μs6,042.59 μs7,132.02 μs1.2375 KB
ReadAsyncBEFORE10485764096None1,090.98 μs6.156 μs18.152 μs1,096.14 μs1,039.20 μs1,120.19 μs1.0029 KB
ReadAsyncAFTER10485764096None1,084.03 μs8.601 μs25.225 μs1,086.13 μs1,042.07 μs1,131.85 μs0.9929 KB
WriteAsyncBEFORE10485764096None3,247.60 μs43.259 μs124.117 μs3,243.69 μs2,954.42 μs3,556.50 μs1.0055 KB
WriteAsyncAFTER10485764096None3,250.35 μs36.664 μs100.985 μs3,234.77 μs3,039.74 μs3,505.95 μs1.0055 KB
ReadAsyncBEFORE10485764096Asynchronous3,273.61 μs17.591 μs50.753 μs3,264.72 μs3,190.18 μs3,410.97 μs1.0079 KB
ReadAsyncAFTER10485764096Asynchronous4,547.38 μs73.438 μs216.535 μs4,398.34 μs4,327.10 μs5,035.98 μs1.3969 KB
WriteAsyncBEFORE10485764096Asynchronous5,384.46 μs83.792 μs240.415 μs5,346.69 μs4,897.35 μs5,955.92 μs1.0084 KB
WriteAsyncAFTER10485764096Asynchronous6,845.63 μs88.794 μs256.192 μs6,883.75 μs6,079.92 μs7,286.73 μs1.2774 KB
ReadAsync_NoBufferingBEFORE104857616384None406.34 μs2.413 μs7.113 μs403.28 μs387.16 μs419.08 μs1.007 KB
ReadAsync_NoBufferingAFTER104857616384None380.35 μs3.413 μs10.063 μs378.36 μs363.73 μs402.82 μs0.947 KB
WriteAsync_NoBufferingBEFORE104857616384None2,545.17 μs30.174 μs83.613 μs2,526.42 μs2,397.67 μs2,776.47 μs1.007 KB
WriteAsync_NoBufferingAFTER104857616384None2,537.77 μs27.489 μs76.628 μs2,528.02 μs2,421.65 μs2,750.68 μs1.007 KB
ReadAsync_NoBufferingBEFORE104857616384Asynchronous949.54 μs6.336 μs18.077 μs943.69 μs926.29 μs1,002.20 μs1.0020 KB
ReadAsync_NoBufferingAFTER104857616384Asynchronous1,195.21 μs3.070 μs8.507 μs1,194.94 μs1,179.45 μs1,218.92 μs1.2617 KB
WriteAsync_NoBufferingBEFORE104857616384Asynchronous2,965.91 μs40.616 μs114.558 μs2,952.10 μs2,677.93 μs3,244.55 μs1.0020 KB
WriteAsync_NoBufferingAFTER104857616384Asynchronous3,021.90 μs40.697 μs116.110 μs3,000.74 μs2,803.04 μs3,343.84 μs1.0217 KB
CopyToFileAsyncBEFORE1048576?None2,440.43 μs17.056 μs49.754 μs2,437.81 μs2,343.18 μs2,576.37 μs1.003 KB
CopyToFileAsyncAFTER1048576?None2,421.23 μs15.376 μs43.869 μs2,413.43 μs2,350.26 μs2,521.45 μs0.993 KB
CopyToFileAsyncBEFORE1048576?Asynchronous2,878.14 μs16.682 μs47.863 μs2,876.69 μs2,771.42 μs3,001.49 μs1.004 KB
CopyToFileAsyncAFTER1048576?Asynchronous2,769.09 μs15.649 μs43.882 μs2,760.85 μs2,691.56 μs2,885.67 μs0.964 KB
ReadAsyncBEFORE1048576004096None115,657.20 μs352.576 μs1,039.577 μs115,639.45 μs113,670.15 μs117,832.15 μs1.002,801 KB
ReadAsyncAFTER1048576004096None116,612.88 μs249.577 μs731.966 μs116,751.05 μs114,615.00 μs117,903.05 μs1.012,801 KB
WriteAsyncBEFORE1048576004096None205,378.59 μs7,785.258 μs22,462.248 μs194,584.50 μs184,125.30 μs268,874.60 μs1.005,005 KB
WriteAsyncAFTER1048576004096None209,453.56 μs10,355.532 μs30,043.276 μs194,064.20 μs173,778.40 μs293,593.30 μs1.035,005 KB
ReadAsyncBEFORE1048576004096Asynchronous327,225.97 μs1,532.011 μs4,346.059 μs326,564.00 μs320,187.10 μs339,508.20 μs1.007,801 KB
ReadAsyncAFTER1048576004096Asynchronous457,654.89 μs902.286 μs2,559.634 μs457,207.00 μs451,652.60 μs465,093.00 μs1.406,801 KB
WriteAsyncBEFORE1048576004096Asynchronous454,994.00 μs7,661.181 μs21,857.780 μs449,253.50 μs421,512.80 μs511,199.10 μs1.007,905 KB
WriteAsyncAFTER1048576004096Asynchronous600,076.62 μs10,088.501 μs29,268.570 μs592,035.20 μs559,265.50 μs677,403.80 μs1.326,905 KB
ReadAsync_NoBufferingBEFORE10485760016384None45,086.23 μs156.169 μs453.074 μs45,005.65 μs43,497.93 μs46,041.93 μs1.00701 KB
ReadAsync_NoBufferingAFTER10485760016384None42,133.53 μs174.922 μs457.740 μs42,093.18 μs41,187.75 μs43,731.75 μs0.93701 KB
WriteAsync_NoBufferingBEFORE10485760016384None61,814.53 μs685.277 μs1,887.453 μs61,419.69 μs59,079.38 μs68,035.43 μs1.00701 KB
WriteAsync_NoBufferingAFTER10485760016384None61,435.05 μs720.962 μs1,973.622 μs61,127.18 μs58,506.18 μs67,827.25 μs0.99701 KB
ReadAsync_NoBufferingBEFORE10485760016384Asynchronous97,220.19 μs667.147 μs1,881.701 μs96,464.10 μs94,386.65 μs102,972.80 μs1.001,951 KB
ReadAsync_NoBufferingAFTER10485760016384Asynchronous128,169.40 μs556.512 μs1,587.760 μs127,771.80 μs125,455.30 μs133,061.15 μs1.321,701 KB
WriteAsync_NoBufferingBEFORE10485760016384Asynchronous136,593.81 μs1,249.681 μs3,462.860 μs136,822.00 μs127,534.30 μs146,281.60 μs1.001,951 KB
WriteAsync_NoBufferingAFTER10485760016384Asynchronous169,656.11 μs2,245.877 μs6,515.696 μs168,284.30 μs157,404.20 μs189,342.10 μs1.251,701 KB
CopyToFileAsyncBEFORE104857600?None56,537.38 μs245.101 μs695.312 μs56,440.93 μs55,567.65 μs58,581.80 μs1.00177 KB
CopyToFileAsyncAFTER104857600?None56,184.32 μs275.348 μs785.585 μs56,116.43 μs54,790.00 μs58,574.72 μs0.99177 KB
CopyToFileAsyncBEFORE104857600?Asynchronous87,668.39 μs312.004 μs905.181 μs87,555.38 μs85,324.65 μs89,838.85 μs1.00246 KB
CopyToFileAsyncAFTER104857600?Asynchronous78,490.65 μs501.805 μs1,431.678 μs78,350.84 μs75,379.32 μs82,799.10 μs0.90214 KB

…Source only needed by Net5CompatFileStreamStrategy.
… one allocation and get the actual profiling improvement.
Rename FileStreamAwaitableProvider.
… review. Shorter filename for awaitable provider.
…trategy, and its _fileHandle can now be private.
@adamsitnik

Copy link
Copy Markdown
Member

Most results had allocation improvements, but worse speed when FileOptions = Asynchronous:

As discussed offline, such a regression needs to be solved before merging the PR.

I've even built your fork and run the benchmarks myself to be 100% sure, but unfortunately, I've confirmed the regression:

MethodToolchainfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsync_NoBuffering\after\corerun.exe104857616384Asynchronous838.1 us1.1217 KB
ReadAsync_NoBuffering\before\corerun.exe104857616384Asynchronous748.1 us1.0020 KB
ReadAsync_NoBuffering\after\corerun.exe10485760016384Asynchronous93,214.6 us1.121,700 KB
ReadAsync_NoBuffering\before\corerun.exe10485760016384Asynchronous83,445.4 us1.001,950 KB

If I can suggest something, ReadAsync_NoBuffering with fileSize=1048576 and options=Asynchronous should be the best benchmark for creating a repo app and profiling it. It's the simplest benchmark (buffering is not involved) and ReadAsync has a smaller deviation than WriteAsync. If you fail to find the regression with VS Profiler, you can use Perfview.

@adamsitnikadamsitnik added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Apr 14, 2021
@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

I compared the CPU usage before and after, and discovered we spend a considerable amount of time inside Overlapped.PerformIOCompletionCallback.

The new code (left) consumes 62% of the time calling ExecutionContext.RunInternal, while the old code (right) only took 3.82%.

Screenshot

image

The root cause seems to be this line, where we call _source.SetResult:

Screenshot

image

Which is adding up the time it takes to run the rest of the ReadAsync calls in the loop, including the disposal of the FileStream object in the main method.

There's a comment stating that ExecutionContext.RunInternal (part of the callstack) warning that the code is a very hot path:

Screenshot

image

I wasn't expecting all the continuation code to be counted as part of the asynchronous calls. I wonder if this has anything to do with the removal of RunContinuationsAsynchronously = true flag. I'll bring that flag back and compare the results.

cc @jozkee@adamsitnik@stephentoub

@davidfowl

Copy link
Copy Markdown
Member

We don't need to restore the ExecutionContext in the overlapped, that restore should be avoided in these APIs. The async state machine already does it. This is what you need #42549 😄

@stephentoub

stephentoub commented Apr 14, 2021

Copy link
Copy Markdown
Member

The new code (left) consumes 62% of the time calling ExecutionContext.RunInternal, while the old code (right) only took 3.82%.

That's inclusive (what "Total" means in the VS view). The callback is actually running the continuation now (because of RunContinuationsAsynchronously=false), rather than queueing it to be run on a different thread, so the 62% is inclusive of the actual app code.

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 know you're copying this logic from what was there before, but it seems to be unnecessarily complex, with lots of interlocked operations for transitioning from one state to another to another. If we're looking for places to reduce overheads, revisiting this whole implementation is likely a good place to start.

@adamsitnik

Copy link
Copy Markdown
Member

The callback is actually running the continuation now (because of RunContinuationsAsynchronously=false)

I've run all the async benchmarks for 3 configurations:

  • "before" (@carlossanlop fork main branch)
  • "false" (the current state of this PR as RunContinuationsAsynchronously is not set so it's false)
  • "true" ( _source.RunContinuationsAsynchronously = true; in ctor)

With the following settings:

  • 100 instead of 20 iterations (--minIterationCount 100 --maxIterationCount 101)
  • no outliers removal (--outliers DontRemove)

and confirmed that with _source.RunContinuationsAsynchronously = true; we have no regression in the microbenchmarks

@stephentoub should we:

  • set RunContinuationsAsynchronously = true; and include this PR in preview4
  • get some "real world" numbers from ASP.NET benchmarks (if there are none, write some and contribute to https://github.com/aspnet/benchmarks) and see how RunContinuationsAsynchronously setting affects non-micro benchmarks. This means preview5 for this PR.
Details
MethodToolchainfileSizeuserBufferSizeoptionsMeanStdDevMedianMinMaxRatioAllocated
ReadAsync\before\corerun.exe10241024Asynchronous82.69 us1.946 us82.22 us81.60 us94.90 us1.005 KB
ReadAsync\false\corerun.exe10241024Asynchronous78.76 us0.718 us78.56 us78.04 us82.13 us0.955 KB
ReadAsync\true\corerun.exe10241024Asynchronous83.28 us0.825 us83.12 us82.48 us89.82 us1.015 KB
WriteAsync\before\corerun.exe10241024Asynchronous404.05 us22.574 us400.81 us379.20 us494.53 us1.005 KB
WriteAsync\false\corerun.exe10241024Asynchronous400.76 us17.211 us397.80 us380.07 us472.21 us0.995 KB
WriteAsync\true\corerun.exe10241024Asynchronous405.20 us25.469 us401.47 us383.59 us510.74 us1.015 KB
ReadAsync\before\corerun.exe1048576512Asynchronous2,418.83 us55.684 us2,413.59 us2,361.95 us2,804.46 us1.0093 KB
ReadAsync\false\corerun.exe1048576512Asynchronous2,648.89 us73.366 us2,626.45 us2,550.29 us2,959.92 us1.1083 KB
ReadAsync\true\corerun.exe1048576512Asynchronous2,386.25 us9.985 us2,386.16 us2,356.79 us2,419.58 us0.9983 KB
WriteAsync\before\corerun.exe1048576512Asynchronous4,254.70 us277.778 us4,213.63 us3,967.55 us6,203.71 us1.0085 KB
WriteAsync\false\corerun.exe1048576512Asynchronous4,583.08 us183.949 us4,559.42 us4,317.29 us5,334.19 us1.0875 KB
WriteAsync\true\corerun.exe1048576512Asynchronous4,207.77 us179.340 us4,173.53 us3,896.65 us4,865.02 us0.9975 KB
ReadAsync\before\corerun.exe10485764096Asynchronous2,343.06 us35.339 us2,355.62 us2,225.73 us2,379.72 us1.0079 KB
ReadAsync\false\corerun.exe10485764096Asynchronous2,603.91 us40.916 us2,603.11 us2,513.55 us2,715.90 us1.1169 KB
ReadAsync\true\corerun.exe10485764096Asynchronous2,324.68 us38.225 us2,337.29 us2,226.14 us2,374.04 us0.9969 KB
WriteAsync\before\corerun.exe10485764096Asynchronous4,110.48 us265.902 us4,050.53 us3,844.25 us6,007.74 us1.0084 KB
WriteAsync\false\corerun.exe10485764096Asynchronous4,572.16 us320.658 us4,542.09 us4,256.30 us7,440.25 us1.1274 KB
WriteAsync\true\corerun.exe10485764096Asynchronous4,048.01 us184.553 us4,015.01 us3,739.28 us4,754.61 us0.9974 KB
ReadAsync_NoBuffering\before\corerun.exe104857616384Asynchronous747.53 us5.076 us747.87 us729.80 us755.69 us1.0020 KB
ReadAsync_NoBuffering\false\corerun.exe104857616384Asynchronous822.87 us6.877 us822.90 us809.94 us838.18 us1.1017 KB
ReadAsync_NoBuffering\true\corerun.exe104857616384Asynchronous726.82 us11.262 us726.44 us699.96 us754.36 us0.9717 KB
WriteAsync_NoBuffering\before\corerun.exe104857616384Asynchronous2,726.54 us136.755 us2,712.48 us2,534.40 us3,234.12 us1.0020 KB
WriteAsync_NoBuffering\false\corerun.exe104857616384Asynchronous2,761.05 us146.361 us2,792.11 us2,507.70 us3,188.16 us1.0117 KB
WriteAsync_NoBuffering\true\corerun.exe104857616384Asynchronous2,736.46 us194.495 us2,705.98 us2,415.11 us4,022.13 us1.0117 KB
ReadAsync\before\corerun.exe1048576004096Asynchronous250,490.85 us2,604.707 us250,256.45 us244,814.30 us256,607.90 us1.007,801 KB
ReadAsync\false\corerun.exe1048576004096Asynchronous278,260.89 us4,931.050 us278,009.45 us269,882.60 us297,338.40 us1.116,803 KB
ReadAsync\true\corerun.exe1048576004096Asynchronous252,682.05 us19,763.442 us250,702.50 us243,911.20 us447,534.10 us1.016,801 KB
WriteAsync\before\corerun.exe1048576004096Asynchronous370,893.59 us6,590.799 us369,781.05 us352,834.20 us390,366.40 us1.007,905 KB
WriteAsync\false\corerun.exe1048576004096Asynchronous397,619.03 us8,020.098 us397,052.95 us384,609.00 us446,559.70 us1.076,905 KB
WriteAsync\true\corerun.exe1048576004096Asynchronous358,128.05 us6,536.171 us357,755.45 us346,707.00 us382,735.70 us0.976,905 KB

@adamsitnikadamsitnik removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Apr 15, 2021

@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.

LGTM, thank you @carlossanlop !

@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

With the unit tests passing, the micro benchmarks having no regressions after reverting RunContinuationsAsynchronously to true, and having allocation improvements, I feel confident in merging this change to include it in Preview4, and will continue working on #50972 to add caching and improve this code for Preview5.

@carlossanlop
carlossanlop merged commit a38d0c2 into dotnet:mainApr 15, 2021
@carlossanlop
carlossanlop deleted the IValueTaskSource branch April 15, 2021 22:51
@ghostghost locked as resolved and limited conversation to collaborators May 15, 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.

5 participants

@carlossanlop@davidfowl@stephentoub@adamsitnik@jozkee
, '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: Use IValueTaskSource instead of TaskCompletionSource - #50802

Merged
carlossanlop merged 19 commits into
dotnet:mainfrom
carlossanlop:IValueTaskSource
Apr 15, 2021
Merged

FileStream rewrite: Use IValueTaskSource instead of TaskCompletionSource#50802
carlossanlop merged 19 commits into
dotnet:mainfrom
carlossanlop:IValueTaskSource

Conversation

@carlossanlop

Copy link
Copy Markdown
Contributor

The purpose of this PR is to remove a large allocation found in FileStream.ReadAsync and FileStream.WriteAsync, which we confirmed in our profiling investigation around the new FileStream rewrite code.

The allocation improvement can be achieved by creating a new type, called FileStreamValueTaskSource, that will have the same purpose as FileStreamCompletionSource, but instead of implementing TaskCompletionSource (which returns a Task to represent the asynchronous work) it implements IValueTaskSource, which returns a ValueTask.

The behavior was largely left untouched, except for the following differences:

  • Net5CompatFileStreamStrategy.Windows will be the only strategy that consumes the FileStreamCompletionSource, so it is now a private nested class.
  • One single new type implements both IValueTaskSource (used for write) and IValueTaskSource<int> (used for read).
  • The logic inside the callback and the cancellation token registration is the same, aside for the code that needs to report exceptions or return values.
  • We had interop errors redefined in multiple places.
  • The constants used by FileStreamCompletionSource were moved to FileStreamHelpers so they could be reused by the new type.

Unit tests

Manually ran unit tests from System.IO.FileSystem, System.IO, System.Runtime and System.Runtime.Extensions, they all passed. I made sure to run them with the Net5Compat flag turned off, to ensure my new code was executed.

Benchmarks

  • Ran the FileStream performance benchmarks against .NET 5.0, then against the code of this PR's baseline commit, and then against this PR's new code. I compared the results, and there are subtle improvements in some benchmark tests:
.NET 5.0 vs .NET 6.0 before this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_before_pr' --threshold 0.0001%

summary:
better: 39, geomean: 2.801
worse: 22, geomean: 2.098
total diff: 61

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.93882252.574348913.28
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.621003671.884633945.31bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.13257218.23805205.31several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.13286859.38896524.31
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.061795459.385498006.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.051868294.535689532.29
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)2.99254148.29758897.83
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.97283860.42843052.43
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.901844293.365353701.04several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.892209384.386395554.17bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.65301619.83798302.63
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.86557062.051035575.21bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.81478821.50868866.12
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.693479778.755891160.42several?
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.66543512.61903388.82bimodal
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.40168291900.00235314600.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.248197915.6310200393.75
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.215313143.756444323.96
Perf_FileStream.Flush(fileSize: 1024, options: None)1.183448111.254052132.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055473900.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.0956033900.0061052537.50
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.03133329050.00137489800.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.645489541.6747470.79
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.902811281.2543319.93
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.822571736.4644479.08
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.29876923.2641197.72
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.062463834750.00486578800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.94152781.6630909.09
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.72153731.8132593.67
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437889.31several?
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.98165696.6841632.18
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.97159526.5840152.10
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.50200423.3657204.05
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889181.93
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.55238920.2293543.27
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.50193629.0877357.59
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.443222202.501318286.88
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.39194808.8781591.76
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.36216916.9892096.58
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.24325116900.00145245400.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.157025697.923271982.50
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.10199647.8995019.35
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.04686776300.00336991400.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9519598723.0810074393.75
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)1.85508393800.00274360000.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.74796591200.00457934500.00bimodal
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.73156593600.0090313262.50
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.7022492768.7513248333.33
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64542213800.00331399500.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.615221529.173244615.63
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114593046.88
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742610.27
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09777023.21
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.191301925.001096436.88
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.12142000.45126451.42
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 512, options: None)1.11162435300.00146191400.00
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.10141476.75128484.00
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.10142083.81129048.19
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00113456750.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0288348175.0086894587.50
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 512, options: None)1.0192483275.0091193650.00
.NET 5.0 vs .NET 6.0 after this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_after_pr' --threshold 0.0001%
summary:
better: 36, geomean: 3.006
worse: 22, geomean: 2.080
total diff: 58

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.92882252.574341552.34
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.601003671.884615128.91bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)3.24283860.42919970.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)3.08254148.29783231.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.061868294.535710579.17
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.05286859.38874430.38
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.04257218.23780960.42
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)2.971795459.385329393.75
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.951844293.365432295.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.922209384.386445677.08
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.52301619.83758761.88
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.95557062.051087235.83
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.73543512.61937893.38bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.66478821.50793440.46bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.473479778.755113488.54several?
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.32168291900.00221660000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.235313143.756512252.08
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.228197915.6310012295.65
Perf_FileStream.Flush(fileSize: 1024, options: None)1.213448111.254163660.16
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1356033900.0063237650.00
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0950055150.0054423625.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0088983100.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.505489541.6747526.62
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.942811281.2543292.23
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.062571736.4645070.47
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.05876923.2641661.14
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.362463834750.00459292800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.95152781.6630850.74
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.75153731.8132349.21
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437926.07several?
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.98159526.5840096.31
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.96165696.6841824.65
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889051.28
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.56238920.2293276.60
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.53193629.0876383.11
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.45194808.8779416.04
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501376330.63
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.31216916.9893711.06
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.28325116900.00142880200.00
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)2.22200423.3690245.25
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.167025697.923250746.88
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.11508393800.00241032100.00
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.11199647.8994675.64
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.06686776300.00333108950.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9319598723.0810171781.82
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.74156593600.0090035450.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.65542213800.00329569650.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.645221529.173182177.50
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64796591200.00486630000.00
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.5222492768.7514797453.85
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114625460.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742460.71
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09781337.50
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.201301925.001085495.19
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.11141476.75127158.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.11142083.81128134.40
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.10142000.45129068.15
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00112942550.00

Profiling

I reused the same code we have in our benchmarks, but I executed it inside a console app that forces the consumption of the compiled runtime code.

You'll notice the top Task allocation went away, and the FileStreamCompletionSource allocations were substituted by FileStreamValueTaskSource allocations.

ReadAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,797
- System.String2,153
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,806
- System.String2,153
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]964
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

ReadAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,013
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,840
- System.String2,153
- System.Object[]955
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,836
- System.String2,144
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>64,235
- System.Threading.Tasks.Task<>64,015
- System.IO.FileStreamCompletionSource64,000
- System.SByte[]2,916
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,812
- System.SByte[]2,909
- System.String2,144
- System.Object[]964
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,798
- System.String2,153
- System.Object[]954
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,812
- System.String2,153
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]958
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,249
- System.IO.MemoryFileStreamCompletionSource32,000
- System.IO.FileStreamCompletionSource32,000
- System.SByte[]2,915
- System.String2,155
- System.Object[]952
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,246
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource32,000
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource32,000
- System.SByte[]2,893
- System.String2,144
- System.Object[]954
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.IO.FileStreamCompletionSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,963
- System.SByte[]2,915
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,830
- System.SByte[]2,912
- System.String2,144
- System.Object[]966
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Threading.Tasks.ValueTask.ValueTaskSourceAsTask249
- System.Threading.QueueUserWorkItemCallbackDefaultContext<>249

@adamsitnik@jozkee@jeffhandley@stephentoub

Also: Don't forget to ignore whitespace changes so it's easier to review the nested type's changes 😃

@ghost

ghost commented Apr 6, 2021

Copy link
Copy Markdown

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

Issue Details

The purpose of this PR is to remove a large allocation found in FileStream.ReadAsync and FileStream.WriteAsync, which we confirmed in our profiling investigation around the new FileStream rewrite code.

The allocation improvement can be achieved by creating a new type, called FileStreamValueTaskSource, that will have the same purpose as FileStreamCompletionSource, but instead of implementing TaskCompletionSource (which returns a Task to represent the asynchronous work) it implements IValueTaskSource, which returns a ValueTask.

The behavior was largely left untouched, except for the following differences:

  • Net5CompatFileStreamStrategy.Windows will be the only strategy that consumes the FileStreamCompletionSource, so it is now a private nested class.
  • One single new type implements both IValueTaskSource (used for write) and IValueTaskSource<int> (used for read).
  • The logic inside the callback and the cancellation token registration is the same, aside for the code that needs to report exceptions or return values.
  • We had interop errors redefined in multiple places.
  • The constants used by FileStreamCompletionSource were moved to FileStreamHelpers so they could be reused by the new type.

Unit tests

Manually ran unit tests from System.IO.FileSystem, System.IO, System.Runtime and System.Runtime.Extensions, they all passed. I made sure to run them with the Net5Compat flag turned off, to ensure my new code was executed.

Benchmarks

  • Ran the FileStream performance benchmarks against .NET 5.0, then against the code of this PR's baseline commit, and then against this PR's new code. I compared the results, and there are subtle improvements in some benchmark tests:
.NET 5.0 vs .NET 6.0 before this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_before_pr' --threshold 0.0001%

summary:
better: 39, geomean: 2.801
worse: 22, geomean: 2.098
total diff: 61

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.93882252.574348913.28
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.621003671.884633945.31bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.13257218.23805205.31several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.13286859.38896524.31
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.061795459.385498006.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.051868294.535689532.29
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)2.99254148.29758897.83
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.97283860.42843052.43
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.901844293.365353701.04several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.892209384.386395554.17bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.65301619.83798302.63
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.86557062.051035575.21bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.81478821.50868866.12
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.693479778.755891160.42several?
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.66543512.61903388.82bimodal
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.40168291900.00235314600.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.248197915.6310200393.75
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.215313143.756444323.96
Perf_FileStream.Flush(fileSize: 1024, options: None)1.183448111.254052132.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055473900.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.0956033900.0061052537.50
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.03133329050.00137489800.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.645489541.6747470.79
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.902811281.2543319.93
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.822571736.4644479.08
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.29876923.2641197.72
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.062463834750.00486578800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.94152781.6630909.09
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.72153731.8132593.67
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437889.31several?
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.98165696.6841632.18
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.97159526.5840152.10
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.50200423.3657204.05
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889181.93
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.55238920.2293543.27
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.50193629.0877357.59
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.443222202.501318286.88
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.39194808.8781591.76
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.36216916.9892096.58
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.24325116900.00145245400.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.157025697.923271982.50
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.10199647.8995019.35
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.04686776300.00336991400.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9519598723.0810074393.75
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)1.85508393800.00274360000.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.74796591200.00457934500.00bimodal
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.73156593600.0090313262.50
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.7022492768.7513248333.33
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64542213800.00331399500.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.615221529.173244615.63
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114593046.88
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742610.27
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09777023.21
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.191301925.001096436.88
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.12142000.45126451.42
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 512, options: None)1.11162435300.00146191400.00
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.10141476.75128484.00
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.10142083.81129048.19
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00113456750.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0288348175.0086894587.50
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 512, options: None)1.0192483275.0091193650.00
.NET 5.0 vs .NET 6.0 after this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_after_pr' --threshold 0.0001%
summary:
better: 36, geomean: 3.006
worse: 22, geomean: 2.080
total diff: 58

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.92882252.574341552.34
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.601003671.884615128.91bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)3.24283860.42919970.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)3.08254148.29783231.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.061868294.535710579.17
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.05286859.38874430.38
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.04257218.23780960.42
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)2.971795459.385329393.75
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.951844293.365432295.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.922209384.386445677.08
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.52301619.83758761.88
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.95557062.051087235.83
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.73543512.61937893.38bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.66478821.50793440.46bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.473479778.755113488.54several?
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.32168291900.00221660000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.235313143.756512252.08
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.228197915.6310012295.65
Perf_FileStream.Flush(fileSize: 1024, options: None)1.213448111.254163660.16
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1356033900.0063237650.00
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0950055150.0054423625.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0088983100.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.505489541.6747526.62
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.942811281.2543292.23
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.062571736.4645070.47
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.05876923.2641661.14
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.362463834750.00459292800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.95152781.6630850.74
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.75153731.8132349.21
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437926.07several?
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.98159526.5840096.31
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.96165696.6841824.65
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889051.28
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.56238920.2293276.60
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.53193629.0876383.11
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.45194808.8779416.04
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501376330.63
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.31216916.9893711.06
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.28325116900.00142880200.00
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)2.22200423.3690245.25
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.167025697.923250746.88
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.11508393800.00241032100.00
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.11199647.8994675.64
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.06686776300.00333108950.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9319598723.0810171781.82
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.74156593600.0090035450.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.65542213800.00329569650.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.645221529.173182177.50
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64796591200.00486630000.00
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.5222492768.7514797453.85
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114625460.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742460.71
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09781337.50
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.201301925.001085495.19
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.11141476.75127158.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.11142083.81128134.40
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.10142000.45129068.15
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00112942550.00

Profiling

I reused the same code we have in our benchmarks, but I executed it inside a console app that forces the consumption of the compiled runtime code.

You'll notice the top Task allocation went away, and the FileStreamCompletionSource allocations were substituted by FileStreamValueTaskSource allocations.

ReadAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,797
- System.String2,153
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,806
- System.String2,153
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]964
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

ReadAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,013
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,840
- System.String2,153
- System.Object[]955
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,836
- System.String2,144
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>64,235
- System.Threading.Tasks.Task<>64,015
- System.IO.FileStreamCompletionSource64,000
- System.SByte[]2,916
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,812
- System.SByte[]2,909
- System.String2,144
- System.Object[]964
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,798
- System.String2,153
- System.Object[]954
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,812
- System.String2,153
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]958
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,249
- System.IO.MemoryFileStreamCompletionSource32,000
- System.IO.FileStreamCompletionSource32,000
- System.SByte[]2,915
- System.String2,155
- System.Object[]952
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,246
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource32,000
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource32,000
- System.SByte[]2,893
- System.String2,144
- System.Object[]954
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.IO.FileStreamCompletionSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,963
- System.SByte[]2,915
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,830
- System.SByte[]2,912
- System.String2,144
- System.Object[]966
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Threading.Tasks.ValueTask.ValueTaskSourceAsTask249
- System.Threading.QueueUserWorkItemCallbackDefaultContext<>249

@adamsitnik@jozkee@jeffhandley@stephentoub

Also: Don't forget to ignore whitespace changes so it's easier to review the nested type's changes 😃
Author:carlossanlop
Assignees:carlossanlop
Labels:

area-System.IO

Milestone:6.0.0

@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

Tagging subscribers to this area: @carlossanlop

Yes, thank you bot... 😆

@jozkeejozkee 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.

Otherwise; LGTM, will approve if/when comments are resolved, most of them are nits and/or questions.

Comment on lines +17 to +22
internal const long NoResult = 0;
internal const long ResultSuccess = (long)1 << 32;
internal const long ResultError = (long)2 << 32;
internal const long RegisteringCancellation = (long)4 << 32;
internal const long CompletedCallback = (long)8 << 32;
internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32;

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.

Consider prefixing this constants with AsyncCompletionSource_ or something alike since these names are too ambiguous or keep them in their respective classes regardless of them being duplicated.

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.

or put them into a nested static class called ReturnCodes:

internalstaticpartialclassFileStreamHelpers{internalstaticclassReturnCodes{internalconstlongNoResult=0;internalconstlongResultSuccess=(long)1<<32;internalconstlongResultError=(long)2<<32;internalconstlongRegisteringCancellation=(long)4<<32;internalconstlongCompletedCallback=(long)8<<32;internalconstulongResultMask=((ulong)uint.MaxValue)<<32;}

but it's a nit (not a blocker)

Comment on lines +66 to +67
_strategy.CompareExchangeCurrentOverlappedOwner(this, null) == null ?
_strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!) : // allocated when buffer was created, and buffer is non-null

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.

Suggested change
_strategy.CompareExchangeCurrentOverlappedOwner(this,null)==null?
_strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!):// allocated when buffer was created, and buffer is non-null
strategy.CompareExchangeCurrentOverlappedOwner(this,null)==null?
strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!):// allocated when buffer was created, and buffer is non-null

// be directly the AwaitableProvider that's completing (in the case where the preallocated
// overlapped was already in use by another operation).
object? state = ThreadPoolBoundHandle.GetNativeOverlappedState(pOverlapped);
Debug.Assert(state is (AsyncWindowsFileStreamStrategy or FileStreamValueTaskSource));

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.

Suggested change
Debug.Assert(stateis(AsyncWindowsFileStreamStrategy or FileStreamValueTaskSource));
Debug.Assert(stateisAsyncWindowsFileStreamStrategy or FileStreamValueTaskSource);

int errorCode = unchecked((int)(packedResult & uint.MaxValue));
if (errorCode == Interop.Errors.ERROR_OPERATION_ABORTED)
{
_source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(new OperationCanceledException(cancellationToken.IsCancellationRequested ? cancellationToken : new CancellationToken(canceled: true))));

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.

Suggested change
_source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(newOperationCanceledException(cancellationToken.IsCancellationRequested?cancellationToken:newCancellationToken(canceled:true))));
Exceptione=newOperationCanceledException(cancellationToken.IsCancellationRequested?cancellationToken:newCancellationToken(canceled:true));
e.SetCurrentStackTrace();
_source.SetException(e);

}

private void Cancel(CancellationToken token)
{

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.

Missing comment and asserts:

// WARNING: This may potentially be called under a lock (during cancellation registration)
Debug.Assert(stateisFileStreamCompletionSource,"Unknown state passed to cancellation");
FileStreamCompletionSourcecompletionSource=(FileStreamCompletionSource)state;
Debug.Assert(completionSource._overlapped!=null&&!completionSource.Task.IsCompleted,"IO should not have completed yet");

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The first assert is not needed because the method argument is not an object? state.

I'll add the assert that checks for overlapped an completed status.

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.

The state param could be added to this method and allows you to conserve the first Debug.Assert.

long packedResult = Interlocked.CompareExchange(ref _result, FileStreamHelpers.RegisteringCancellation, FileStreamHelpers.NoResult);
if (packedResult == FileStreamHelpers.NoResult)
{
_cancellationRegistration = cancellationToken.UnsafeRegister(static (s, token) => ((FileStreamValueTaskSource)s!).Cancel(token), this);

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.

Could we pass the callback on a similar fashion to FileStreamCompletionSource

_cancellationRegistration=cancellationToken.UnsafeRegister(cancelCallback,this);

Or why did you opted for this way instead?

@carlossanlopcarlossanlopApr 8, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It made more sense to pass this as the second argument of UnsafeRegister, which ensures it's a ValueTaskSource instance, and saves us from the check of the object? state parameter like in the old code. The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

We have a similar example in System.Threading.Channels.

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.

It made more sense to pass this as the second argument of UnsafeRegister

FileStreamCompletionSource was also passing this as 2nd argument.

The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

Yes, seems that it was caching the method group to avoid allocating everytime it was passed to UnsafeRegister.
However I think you can avoid the allocation without caching yourself by doing this:

_cancellationRegistration=cancellationToken.UnsafeRegister((s,token)=>Cancel(s,token),this);

Just a suggestion, not something that you should block on.

@davidfowl

Copy link
Copy Markdown
Member

This is the change I was waiting for 👏🏾👏🏾

@davidfowl

Copy link
Copy Markdown
Member

I haven't looked but I assume we'll reuse the operation objects if there's no concurrency? This is what sockets do today so in the 95% case of no overlapping writes or reads, there'll be a single reusable allocation on the FileStream

@stephentoub

Copy link
Copy Markdown
Member

This is the change I was waiting for

It's not, actually. You want the next one where the IValueTaskSource instance is cached.

}
// else: Some other thread stole the result, so now it is responsible to finish the callback
}
// else: Some other thread is registering a cancellation, so it *must* finish the callback

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.

All this logic looks more complicated than it needs to be. I realize you're just following the same flow as what was already there, so nothing needs to be changed in this PR. But we should revisit all of this synchronization later; I suspect we can do it more succinctly and efficiently.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I opened an issue to follow up on this and on the caching of the IValueTaskSource.
#50972

@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 before we merge I would like to see the following benchmark results:

.NET 6 with new strategy enabled: before and after your change. (not .NET 5 vs 6 as there are too many moving targets)

You should be able to achieve that by running the following command in the perf repo;

git clone https://github.com/dotnet/performance.git
cd performance\src\benchmarks\micro
dotnet run -c Release -f net6.0 --filter *Perf_FileStream* --coreRun $pathToCoreRunWithoutYourChanges $pathToCoreRunWithYourChanges --envVars "DOTNET_SYSTEM_IO_USENET5COMPATFILESTREAM:0"

/// <see cref="MemoryHandle"/> when the operation has completed. This should only be used
/// when memory doesn't wrap a byte[].
/// </summary>
private sealed class MemoryAwaitableProvider : FileStreamValueTaskSource

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.

This should only be used when memory doesn't wrap a byte[].

I just realized that we don't have benchmarks for this scenario as Memory used in our micro-benchmarks always wraps a byte array. Could you please add such benchmarks to https://github.com/dotnet/performance/blob/main/src/benchmarks/micro/libraries/System.IO.FileSystem/Perf.FileStream.cs ? One test case should be enough as this should be rare.

@carlossanlopcarlossanlopApr 7, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Some of the benchmarks do consume this type. If you look at some of the profiling tests I executed, you'll see that MemoryValueTaskSource shows up a few times. The profiling tests reuse the code we had in our benchmarks.

I don't mind adding an explicit one though. Dou you want me to add one anyway?

Comment on lines +17 to +22
internal const long NoResult = 0;
internal const long ResultSuccess = (long)1 << 32;
internal const long ResultError = (long)2 << 32;
internal const long RegisteringCancellation = (long)4 << 32;
internal const long CompletedCallback = (long)8 << 32;
internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32;

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.

or put them into a nested static class called ReturnCodes:

internalstaticpartialclassFileStreamHelpers{internalstaticclassReturnCodes{internalconstlongNoResult=0;internalconstlongResultSuccess=(long)1<<32;internalconstlongResultError=(long)2<<32;internalconstlongRegisteringCancellation=(long)4<<32;internalconstlongCompletedCallback=(long)8<<32;internalconstulongResultMask=((ulong)uint.MaxValue)<<32;}

but it's a nit (not a blocker)

Comment on lines 62 to 63

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.

Did you run the perf. benchmarks after doing this change and did you notice any impact?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Pending.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Confirmed - There is a slight improvement.

I made sure to compare apples to apples: I used my latest commit, and built it setting RunContinuationsAsynchronously to true, then to false. I compared it both times against our code in .NET 5.0.

The improvement is clear when setting it to false (or not setting it at all).

RunContinuationsAsynchronously = true

❯ dotnet run -c release --base "D:\fs50" --diff "D:\fs60_NEW_ON_IVTS_asynctrue" --threshold 0.0001%
summary:
better: 24, geomean: 3.402
worse: 19, geomean: 2.244
total diff: 43

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)5.49882252.574840275.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)5.121003671.885136350.00bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)4.44257218.231141827.01
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)4.08286859.381171600.96
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.251795459.385832233.33
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)3.111844293.365741847.92several?
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.111868294.535807825.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)3.082209384.386796443.75
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)2.46557062.051371566.15
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)2.33478821.501116033.48
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)2.28543512.611239956.25bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.613479778.755612725.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.448197915.6311814575.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.33168291900.00224355000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.335313143.757063218.75
Perf_FileStream.Flush(fileSize: 1024, options: None)1.253448111.254313832.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1250055150.0056309025.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1256033900.0062803900.00
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.12133329050.00148833000.00bimodal
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)117.075489541.6746889.31
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)65.932811281.2542642.74
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.842571736.4644462.45
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.09876923.2641586.04
Perf_FileStream.OpenClose(fileSize: 1024, options: None)5.00152781.6630548.62
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.76153731.8132314.04
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)4.032463834750.00611183400.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.93165696.6842208.34
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.52200423.3656865.52
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.62230670.3887990.73
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.58238920.2292478.58
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501377939.84
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.87156593600.0083681962.50
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.6619598723.0811803641.67
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.6322492768.7513818900.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.577025697.924487121.88
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)1.47686776300.00466936500.00bimodal
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55743825.45
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.24975146.09784156.88
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.195221529.174387085.94
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.181301925.001103803.79
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.04116486775.00112407200.00
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.0321953590.9121241122.73
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0087467100.00
RunContinuationsAsynchronously = false

dotnet run -c release --base "D:\fs50" --diff "D:\fs60_NEW_ON_IVTS_asyncfalse" --threshold 0.0001%
summary:
better: 23, geomean: 3.592
worse: 20, geomean: 2.135
total diff: 43

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)5.36882252.574730875.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)5.051003671.885071912.50bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)4.25257218.231094164.38several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)4.05286859.381161160.27
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.231795459.385791481.25
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)3.101844293.365709820.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)3.092209384.386836504.17
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.071868294.535733883.33
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)2.46557062.051368034.90
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)2.31478821.501103696.25
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)2.24543512.611218234.38bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.603479778.755572682.29
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.428197915.6311634850.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.33168291900.00223141800.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.315313143.756949831.25
Perf_FileStream.Flush(fileSize: 1024, options: None)1.263448111.254332915.63
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.13133329050.00150223800.00bimodal
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055699050.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1056033900.0061710550.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0088348175.0088759625.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)116.975489541.6746929.42
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)65.762811281.2542753.29
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.562571736.4644676.15
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.52876923.2640756.47
Perf_FileStream.OpenClose(fileSize: 1024, options: None)5.06152781.6630216.63
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.81153731.8131975.57
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)4.052463834750.00608864800.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.99165696.6841526.73
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.54200423.3656610.08
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.62230670.3887877.10
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.59238920.2292291.75
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.403222202.501345116.25
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.86156593600.0083969875.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.6419598723.0811925173.68
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.6222492768.7513859764.71
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.567025697.924493881.25
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)1.46686776300.00470158300.00bimodal
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.32987460.55750294.64
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.24975146.09784056.56
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.171301925.001108880.36
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.175221529.174470673.44
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.0721953590.9120580725.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.01116486775.00

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Here is the result comparison if we compare the benchmark results of setting it to true vs false:

base: RunContinuationsAsynchronously=true, diff: RunContinuationsAsynchronously=false

dotnet run -c release --base "D:\fs60_NEW_ON_IVTS_asynctrue" --diff "D:\fs60_NEW_ON_IVTS_asyncfalse" --threshold 0.0001%
summary:
better: 14, geomean: 1.018
worse: 11, geomean: 1.017
total diff: 25

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.WriteAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0567143550.0070259187.50
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.02112407200.00115107550.00
Perf_FileStream.Read_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.02240016.67244921.93
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.024387085.944470673.44
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0187467100.0088759625.00
Perf_FileStream.WriteAsync_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.0121684354.5521982809.09
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: Asynchronous)1.011236709.901253242.19
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 1024, options: None)1.0139289.5439730.73
Perf_FileStream.Write_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.0121445427.2721646650.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.01743825.45750294.64
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)1.0044462.4544676.15
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.WriteByte(fileSize: 1024, options: None)1.041141827.011094164.38several?
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0345688612.5044474575.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.021377939.841345116.25
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)1.024840275.004730875.00
Perf_FileStream.SeekForward(fileSize: 1024, options: None)1.0241586.0440756.47
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: Asynchronous)1.02130849050.00128463775.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)1.0242208.3441526.73
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.0211814575.0011634850.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)1.015807825.005733883.33
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)1.015136350.005071912.50
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0156309025.0055699050.00
Perf_FileStream.Read_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0133747814.293338345.14
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)1.0132314.0431975.57
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 1024, options: Asynchronous)1.0081988.0981641.78

long packedResult = Interlocked.CompareExchange(ref _result, FileStreamHelpers.RegisteringCancellation, FileStreamHelpers.NoResult);
if (packedResult == FileStreamHelpers.NoResult)
{
_cancellationRegistration = cancellationToken.UnsafeRegister(static (s, token) => ((FileStreamValueTaskSource)s!).Cancel(token), this);

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.

It made more sense to pass this as the second argument of UnsafeRegister

FileStreamCompletionSource was also passing this as 2nd argument.

The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

Yes, seems that it was caching the method group to avoid allocating everytime it was passed to UnsafeRegister.
However I think you can avoid the allocation without caching yourself by doing this:

_cancellationRegistration=cancellationToken.UnsafeRegister((s,token)=>Cancel(s,token),this);

Just a suggestion, not something that you should block on.

Comment on lines 120 to 122

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.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I tried, but a ValueTask does not have a Result property, like a ValueTask<T> does, because it returns void. So I'm not sure if it's possible to apply an equivalent pattern.

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.

publicoverridevoidWrite(byte[]buffer,intoffset,intcount){ValueTaskvt=WriteAsyncInternal(newReadOnlyMemory<byte>(buffer,offset,count),CancellationToken.None);if(vt.IsCompleted){vt.GetAwaiter().GetResult();}else{vt.AsTask().GetAwaiter().GetResult();}}

That said, it's not much of an issue for this case. The read side is an issue because AsTask() will almost certainly need to allocate a new Task<int> in order to hand back the result, but for the write side, AsTask() on an already completed ValueTask will just return Task.Completed, assuming the operation completed successfully. So the only time the existing code for the write side will allocate is when the operation was canceled or faulted, in which case we're about to throw an exception anyway, and the extra task allocation won't really matter. Up to you whether you want to change it.

@jozkeejozkee 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.

LGTM; thanks Carlos!

@adamsitnik

Copy link
Copy Markdown
Member

I compared it both times against our code in .NET 5.0.

Comparison against 5.0 is a comparison of all of the changes that were merged to main since we have snapped 5.0 with your change against 5.0. What we care about is how your change (and nothing else) is affecting the perf, so we need to isolate the runs to only your change.

To be able to do that you need to run the benchmarks using two CoreRuns:

  • as a baseline the corerun that was built from main when you have created a new branch, without any of your changes
  • your branch with your changes

My personal workflow:

  1. When I create a new branch, I perform a full build in Release:
build -c Release -subset clr+libs
  1. I copy the folder that contains CoreRun.exe (runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0) to a separate folder. Typically I call it "before".
  2. I apply some changes and perform an incremental Release build. In case of System.Private.CoreLib.dll it's:
build -c Release -subset clr.corelib+clr.nativecorelib+libs.PreTest
  1. I run the benchmarks using the old and new CoreRun.exe:
dotnet run -c Release -f net6.0 --filter *ABC* --corerun C:\Projects\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\before\CoreRun.exe C:\Projects\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe

@carlossanlop

carlossanlop commented Apr 13, 2021

Copy link
Copy Markdown
ContributorAuthor
  • as a baseline the corerun that was built from main when you have created a new branch, without any of your changes
  • your branch with your changes

@adamsitnik@jozkee here's the table comparing the before (original) and after of this PR. Most results had allocation improvements, but worse speed when FileOptions = Asynchronous:

Results

Command:

 dotnet run
-c Release
-f net6.0
--corerun
D:\runtime2\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe
D:\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe
--filter System.IO.Tests.Perf_FileStream.*Async*
--statisticalTest 3ms
--minIterationCount 100
--maxIterationCount 101
--artifacts "D:\finalresults"

Note: The runtime2 corerun contains the baseline commit before my changes, the runtime corerun contains this PR's changes.

MethodToolchainfileSizeuserBufferSizeoptionsMeanErrorStdDevMedianMinMaxRatioAllocated
ReadAsyncBEFORE10241024None78.69 μs0.255 μs0.736 μs78.53 μs77.61 μs80.66 μs1.005 KB
ReadAsyncAFTER10241024None79.10 μs0.237 μs0.673 μs79.08 μs76.96 μs80.71 μs1.015 KB
WriteAsyncBEFORE10241024None316.97 μs2.512 μs6.919 μs315.92 μs305.11 μs339.35 μs1.004 KB
WriteAsyncAFTER10241024None300.13 μs5.643 μs15.730 μs297.59 μs272.46 μs348.15 μs0.954 KB
ReadAsyncBEFORE10241024Asynchronous94.13 μs0.259 μs0.731 μs94.02 μs92.27 μs96.16 μs1.005 KB
ReadAsyncAFTER10241024Asynchronous85.48 μs0.842 μs2.291 μs85.11 μs82.58 μs92.09 μs0.915 KB
WriteAsyncBEFORE10241024Asynchronous292.63 μs5.659 μs15.869 μs290.59 μs264.25 μs338.98 μs1.005 KB
WriteAsyncAFTER10241024Asynchronous324.35 μs13.906 μs39.222 μs311.27 μs274.34 μs428.64 μs1.115 KB
FlushAsyncBEFORE1024?None4,606.81 μs46.736 μs133.341 μs4,633.92 μs4,374.04 μs4,952.63 μs1.00269 KB
FlushAsyncAFTER1024?None4,587.88 μs58.046 μs168.403 μs4,638.98 μs4,322.79 μs5,034.48 μs1.00269 KB
CopyToFileAsyncBEFORE1024?None474.57 μs5.497 μs15.141 μs474.66 μs440.31 μs520.31 μs1.005 KB
CopyToFileAsyncAFTER1024?None488.94 μs3.631 μs10.061 μs488.62 μs469.93 μs518.76 μs1.035 KB
FlushAsyncBEFORE1024?Asynchronous14,680.35 μs150.967 μs437.982 μs14,637.76 μs13,623.50 μs15,645.01 μs1.00301 KB
FlushAsyncAFTER1024?Asynchronous20,333.22 μs116.277 μs339.185 μs20,299.93 μs19,724.25 μs21,148.73 μs1.39261 KB
CopyToFileAsyncBEFORE1024?Asynchronous520.97 μs5.982 μs16.674 μs519.35 μs490.05 μs572.29 μs1.006 KB
CopyToFileAsyncAFTER1024?Asynchronous523.30 μs6.294 μs17.650 μs520.91 μs479.14 μs583.85 μs1.006 KB
ReadAsyncBEFORE1048576512None1,375.28 μs5.930 μs17.015 μs1,376.60 μs1,340.44 μs1,413.73 μs1.0085 KB
ReadAsyncAFTER1048576512None1,347.16 μs5.128 μs14.958 μs1,345.14 μs1,323.57 μs1,382.14 μs0.9885 KB
WriteAsyncBEFORE1048576512None3,530.03 μs38.149 μs108.222 μs3,521.78 μs3,299.91 μs3,823.63 μs1.0076 KB
WriteAsyncAFTER1048576512None3,473.85 μs47.943 μs134.438 μs3,469.00 μs3,222.90 μs3,832.17 μs0.9876 KB
ReadAsyncBEFORE1048576512Asynchronous3,278.98 μs30.616 μs87.844 μs3,257.98 μs3,151.46 μs3,507.69 μs1.0093 KB
ReadAsyncAFTER1048576512Asynchronous4,399.34 μs16.153 μs47.119 μs4,401.64 μs4,294.71 μs4,531.75 μs1.3483 KB
WriteAsyncBEFORE1048576512Asynchronous5,304.10 μs73.935 μs214.500 μs5,309.27 μs4,771.82 μs5,852.43 μs1.0085 KB
WriteAsyncAFTER1048576512Asynchronous6,506.15 μs94.189 μs271.756 μs6,505.61 μs6,042.59 μs7,132.02 μs1.2375 KB
ReadAsyncBEFORE10485764096None1,090.98 μs6.156 μs18.152 μs1,096.14 μs1,039.20 μs1,120.19 μs1.0029 KB
ReadAsyncAFTER10485764096None1,084.03 μs8.601 μs25.225 μs1,086.13 μs1,042.07 μs1,131.85 μs0.9929 KB
WriteAsyncBEFORE10485764096None3,247.60 μs43.259 μs124.117 μs3,243.69 μs2,954.42 μs3,556.50 μs1.0055 KB
WriteAsyncAFTER10485764096None3,250.35 μs36.664 μs100.985 μs3,234.77 μs3,039.74 μs3,505.95 μs1.0055 KB
ReadAsyncBEFORE10485764096Asynchronous3,273.61 μs17.591 μs50.753 μs3,264.72 μs3,190.18 μs3,410.97 μs1.0079 KB
ReadAsyncAFTER10485764096Asynchronous4,547.38 μs73.438 μs216.535 μs4,398.34 μs4,327.10 μs5,035.98 μs1.3969 KB
WriteAsyncBEFORE10485764096Asynchronous5,384.46 μs83.792 μs240.415 μs5,346.69 μs4,897.35 μs5,955.92 μs1.0084 KB
WriteAsyncAFTER10485764096Asynchronous6,845.63 μs88.794 μs256.192 μs6,883.75 μs6,079.92 μs7,286.73 μs1.2774 KB
ReadAsync_NoBufferingBEFORE104857616384None406.34 μs2.413 μs7.113 μs403.28 μs387.16 μs419.08 μs1.007 KB
ReadAsync_NoBufferingAFTER104857616384None380.35 μs3.413 μs10.063 μs378.36 μs363.73 μs402.82 μs0.947 KB
WriteAsync_NoBufferingBEFORE104857616384None2,545.17 μs30.174 μs83.613 μs2,526.42 μs2,397.67 μs2,776.47 μs1.007 KB
WriteAsync_NoBufferingAFTER104857616384None2,537.77 μs27.489 μs76.628 μs2,528.02 μs2,421.65 μs2,750.68 μs1.007 KB
ReadAsync_NoBufferingBEFORE104857616384Asynchronous949.54 μs6.336 μs18.077 μs943.69 μs926.29 μs1,002.20 μs1.0020 KB
ReadAsync_NoBufferingAFTER104857616384Asynchronous1,195.21 μs3.070 μs8.507 μs1,194.94 μs1,179.45 μs1,218.92 μs1.2617 KB
WriteAsync_NoBufferingBEFORE104857616384Asynchronous2,965.91 μs40.616 μs114.558 μs2,952.10 μs2,677.93 μs3,244.55 μs1.0020 KB
WriteAsync_NoBufferingAFTER104857616384Asynchronous3,021.90 μs40.697 μs116.110 μs3,000.74 μs2,803.04 μs3,343.84 μs1.0217 KB
CopyToFileAsyncBEFORE1048576?None2,440.43 μs17.056 μs49.754 μs2,437.81 μs2,343.18 μs2,576.37 μs1.003 KB
CopyToFileAsyncAFTER1048576?None2,421.23 μs15.376 μs43.869 μs2,413.43 μs2,350.26 μs2,521.45 μs0.993 KB
CopyToFileAsyncBEFORE1048576?Asynchronous2,878.14 μs16.682 μs47.863 μs2,876.69 μs2,771.42 μs3,001.49 μs1.004 KB
CopyToFileAsyncAFTER1048576?Asynchronous2,769.09 μs15.649 μs43.882 μs2,760.85 μs2,691.56 μs2,885.67 μs0.964 KB
ReadAsyncBEFORE1048576004096None115,657.20 μs352.576 μs1,039.577 μs115,639.45 μs113,670.15 μs117,832.15 μs1.002,801 KB
ReadAsyncAFTER1048576004096None116,612.88 μs249.577 μs731.966 μs116,751.05 μs114,615.00 μs117,903.05 μs1.012,801 KB
WriteAsyncBEFORE1048576004096None205,378.59 μs7,785.258 μs22,462.248 μs194,584.50 μs184,125.30 μs268,874.60 μs1.005,005 KB
WriteAsyncAFTER1048576004096None209,453.56 μs10,355.532 μs30,043.276 μs194,064.20 μs173,778.40 μs293,593.30 μs1.035,005 KB
ReadAsyncBEFORE1048576004096Asynchronous327,225.97 μs1,532.011 μs4,346.059 μs326,564.00 μs320,187.10 μs339,508.20 μs1.007,801 KB
ReadAsyncAFTER1048576004096Asynchronous457,654.89 μs902.286 μs2,559.634 μs457,207.00 μs451,652.60 μs465,093.00 μs1.406,801 KB
WriteAsyncBEFORE1048576004096Asynchronous454,994.00 μs7,661.181 μs21,857.780 μs449,253.50 μs421,512.80 μs511,199.10 μs1.007,905 KB
WriteAsyncAFTER1048576004096Asynchronous600,076.62 μs10,088.501 μs29,268.570 μs592,035.20 μs559,265.50 μs677,403.80 μs1.326,905 KB
ReadAsync_NoBufferingBEFORE10485760016384None45,086.23 μs156.169 μs453.074 μs45,005.65 μs43,497.93 μs46,041.93 μs1.00701 KB
ReadAsync_NoBufferingAFTER10485760016384None42,133.53 μs174.922 μs457.740 μs42,093.18 μs41,187.75 μs43,731.75 μs0.93701 KB
WriteAsync_NoBufferingBEFORE10485760016384None61,814.53 μs685.277 μs1,887.453 μs61,419.69 μs59,079.38 μs68,035.43 μs1.00701 KB
WriteAsync_NoBufferingAFTER10485760016384None61,435.05 μs720.962 μs1,973.622 μs61,127.18 μs58,506.18 μs67,827.25 μs0.99701 KB
ReadAsync_NoBufferingBEFORE10485760016384Asynchronous97,220.19 μs667.147 μs1,881.701 μs96,464.10 μs94,386.65 μs102,972.80 μs1.001,951 KB
ReadAsync_NoBufferingAFTER10485760016384Asynchronous128,169.40 μs556.512 μs1,587.760 μs127,771.80 μs125,455.30 μs133,061.15 μs1.321,701 KB
WriteAsync_NoBufferingBEFORE10485760016384Asynchronous136,593.81 μs1,249.681 μs3,462.860 μs136,822.00 μs127,534.30 μs146,281.60 μs1.001,951 KB
WriteAsync_NoBufferingAFTER10485760016384Asynchronous169,656.11 μs2,245.877 μs6,515.696 μs168,284.30 μs157,404.20 μs189,342.10 μs1.251,701 KB
CopyToFileAsyncBEFORE104857600?None56,537.38 μs245.101 μs695.312 μs56,440.93 μs55,567.65 μs58,581.80 μs1.00177 KB
CopyToFileAsyncAFTER104857600?None56,184.32 μs275.348 μs785.585 μs56,116.43 μs54,790.00 μs58,574.72 μs0.99177 KB
CopyToFileAsyncBEFORE104857600?Asynchronous87,668.39 μs312.004 μs905.181 μs87,555.38 μs85,324.65 μs89,838.85 μs1.00246 KB
CopyToFileAsyncAFTER104857600?Asynchronous78,490.65 μs501.805 μs1,431.678 μs78,350.84 μs75,379.32 μs82,799.10 μs0.90214 KB

…Source only needed by Net5CompatFileStreamStrategy.
… one allocation and get the actual profiling improvement.
Rename FileStreamAwaitableProvider.
… review. Shorter filename for awaitable provider.
…trategy, and its _fileHandle can now be private.
@adamsitnik

Copy link
Copy Markdown
Member

Most results had allocation improvements, but worse speed when FileOptions = Asynchronous:

As discussed offline, such a regression needs to be solved before merging the PR.

I've even built your fork and run the benchmarks myself to be 100% sure, but unfortunately, I've confirmed the regression:

MethodToolchainfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsync_NoBuffering\after\corerun.exe104857616384Asynchronous838.1 us1.1217 KB
ReadAsync_NoBuffering\before\corerun.exe104857616384Asynchronous748.1 us1.0020 KB
ReadAsync_NoBuffering\after\corerun.exe10485760016384Asynchronous93,214.6 us1.121,700 KB
ReadAsync_NoBuffering\before\corerun.exe10485760016384Asynchronous83,445.4 us1.001,950 KB

If I can suggest something, ReadAsync_NoBuffering with fileSize=1048576 and options=Asynchronous should be the best benchmark for creating a repo app and profiling it. It's the simplest benchmark (buffering is not involved) and ReadAsync has a smaller deviation than WriteAsync. If you fail to find the regression with VS Profiler, you can use Perfview.

@adamsitnikadamsitnik added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Apr 14, 2021
@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

I compared the CPU usage before and after, and discovered we spend a considerable amount of time inside Overlapped.PerformIOCompletionCallback.

The new code (left) consumes 62% of the time calling ExecutionContext.RunInternal, while the old code (right) only took 3.82%.

Screenshot

image

The root cause seems to be this line, where we call _source.SetResult:

Screenshot

image

Which is adding up the time it takes to run the rest of the ReadAsync calls in the loop, including the disposal of the FileStream object in the main method.

There's a comment stating that ExecutionContext.RunInternal (part of the callstack) warning that the code is a very hot path:

Screenshot

image

I wasn't expecting all the continuation code to be counted as part of the asynchronous calls. I wonder if this has anything to do with the removal of RunContinuationsAsynchronously = true flag. I'll bring that flag back and compare the results.

cc @jozkee@adamsitnik@stephentoub

@davidfowl

Copy link
Copy Markdown
Member

We don't need to restore the ExecutionContext in the overlapped, that restore should be avoided in these APIs. The async state machine already does it. This is what you need #42549 😄

@stephentoub

stephentoub commented Apr 14, 2021

Copy link
Copy Markdown
Member

The new code (left) consumes 62% of the time calling ExecutionContext.RunInternal, while the old code (right) only took 3.82%.

That's inclusive (what "Total" means in the VS view). The callback is actually running the continuation now (because of RunContinuationsAsynchronously=false), rather than queueing it to be run on a different thread, so the 62% is inclusive of the actual app code.

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 know you're copying this logic from what was there before, but it seems to be unnecessarily complex, with lots of interlocked operations for transitioning from one state to another to another. If we're looking for places to reduce overheads, revisiting this whole implementation is likely a good place to start.

@adamsitnik

Copy link
Copy Markdown
Member

The callback is actually running the continuation now (because of RunContinuationsAsynchronously=false)

I've run all the async benchmarks for 3 configurations:

  • "before" (@carlossanlop fork main branch)
  • "false" (the current state of this PR as RunContinuationsAsynchronously is not set so it's false)
  • "true" ( _source.RunContinuationsAsynchronously = true; in ctor)

With the following settings:

  • 100 instead of 20 iterations (--minIterationCount 100 --maxIterationCount 101)
  • no outliers removal (--outliers DontRemove)

and confirmed that with _source.RunContinuationsAsynchronously = true; we have no regression in the microbenchmarks

@stephentoub should we:

  • set RunContinuationsAsynchronously = true; and include this PR in preview4
  • get some "real world" numbers from ASP.NET benchmarks (if there are none, write some and contribute to https://github.com/aspnet/benchmarks) and see how RunContinuationsAsynchronously setting affects non-micro benchmarks. This means preview5 for this PR.
Details
MethodToolchainfileSizeuserBufferSizeoptionsMeanStdDevMedianMinMaxRatioAllocated
ReadAsync\before\corerun.exe10241024Asynchronous82.69 us1.946 us82.22 us81.60 us94.90 us1.005 KB
ReadAsync\false\corerun.exe10241024Asynchronous78.76 us0.718 us78.56 us78.04 us82.13 us0.955 KB
ReadAsync\true\corerun.exe10241024Asynchronous83.28 us0.825 us83.12 us82.48 us89.82 us1.015 KB
WriteAsync\before\corerun.exe10241024Asynchronous404.05 us22.574 us400.81 us379.20 us494.53 us1.005 KB
WriteAsync\false\corerun.exe10241024Asynchronous400.76 us17.211 us397.80 us380.07 us472.21 us0.995 KB
WriteAsync\true\corerun.exe10241024Asynchronous405.20 us25.469 us401.47 us383.59 us510.74 us1.015 KB
ReadAsync\before\corerun.exe1048576512Asynchronous2,418.83 us55.684 us2,413.59 us2,361.95 us2,804.46 us1.0093 KB
ReadAsync\false\corerun.exe1048576512Asynchronous2,648.89 us73.366 us2,626.45 us2,550.29 us2,959.92 us1.1083 KB
ReadAsync\true\corerun.exe1048576512Asynchronous2,386.25 us9.985 us2,386.16 us2,356.79 us2,419.58 us0.9983 KB
WriteAsync\before\corerun.exe1048576512Asynchronous4,254.70 us277.778 us4,213.63 us3,967.55 us6,203.71 us1.0085 KB
WriteAsync\false\corerun.exe1048576512Asynchronous4,583.08 us183.949 us4,559.42 us4,317.29 us5,334.19 us1.0875 KB
WriteAsync\true\corerun.exe1048576512Asynchronous4,207.77 us179.340 us4,173.53 us3,896.65 us4,865.02 us0.9975 KB
ReadAsync\before\corerun.exe10485764096Asynchronous2,343.06 us35.339 us2,355.62 us2,225.73 us2,379.72 us1.0079 KB
ReadAsync\false\corerun.exe10485764096Asynchronous2,603.91 us40.916 us2,603.11 us2,513.55 us2,715.90 us1.1169 KB
ReadAsync\true\corerun.exe10485764096Asynchronous2,324.68 us38.225 us2,337.29 us2,226.14 us2,374.04 us0.9969 KB
WriteAsync\before\corerun.exe10485764096Asynchronous4,110.48 us265.902 us4,050.53 us3,844.25 us6,007.74 us1.0084 KB
WriteAsync\false\corerun.exe10485764096Asynchronous4,572.16 us320.658 us4,542.09 us4,256.30 us7,440.25 us1.1274 KB
WriteAsync\true\corerun.exe10485764096Asynchronous4,048.01 us184.553 us4,015.01 us3,739.28 us4,754.61 us0.9974 KB
ReadAsync_NoBuffering\before\corerun.exe104857616384Asynchronous747.53 us5.076 us747.87 us729.80 us755.69 us1.0020 KB
ReadAsync_NoBuffering\false\corerun.exe104857616384Asynchronous822.87 us6.877 us822.90 us809.94 us838.18 us1.1017 KB
ReadAsync_NoBuffering\true\corerun.exe104857616384Asynchronous726.82 us11.262 us726.44 us699.96 us754.36 us0.9717 KB
WriteAsync_NoBuffering\before\corerun.exe104857616384Asynchronous2,726.54 us136.755 us2,712.48 us2,534.40 us3,234.12 us1.0020 KB
WriteAsync_NoBuffering\false\corerun.exe104857616384Asynchronous2,761.05 us146.361 us2,792.11 us2,507.70 us3,188.16 us1.0117 KB
WriteAsync_NoBuffering\true\corerun.exe104857616384Asynchronous2,736.46 us194.495 us2,705.98 us2,415.11 us4,022.13 us1.0117 KB
ReadAsync\before\corerun.exe1048576004096Asynchronous250,490.85 us2,604.707 us250,256.45 us244,814.30 us256,607.90 us1.007,801 KB
ReadAsync\false\corerun.exe1048576004096Asynchronous278,260.89 us4,931.050 us278,009.45 us269,882.60 us297,338.40 us1.116,803 KB
ReadAsync\true\corerun.exe1048576004096Asynchronous252,682.05 us19,763.442 us250,702.50 us243,911.20 us447,534.10 us1.016,801 KB
WriteAsync\before\corerun.exe1048576004096Asynchronous370,893.59 us6,590.799 us369,781.05 us352,834.20 us390,366.40 us1.007,905 KB
WriteAsync\false\corerun.exe1048576004096Asynchronous397,619.03 us8,020.098 us397,052.95 us384,609.00 us446,559.70 us1.076,905 KB
WriteAsync\true\corerun.exe1048576004096Asynchronous358,128.05 us6,536.171 us357,755.45 us346,707.00 us382,735.70 us0.976,905 KB

@adamsitnikadamsitnik removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Apr 15, 2021

@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.

LGTM, thank you @carlossanlop !

@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

With the unit tests passing, the micro benchmarks having no regressions after reverting RunContinuationsAsynchronously to true, and having allocation improvements, I feel confident in merging this change to include it in Preview4, and will continue working on #50972 to add caching and improve this code for Preview5.

@carlossanlop
carlossanlop merged commit a38d0c2 into dotnet:mainApr 15, 2021
@carlossanlop
carlossanlop deleted the IValueTaskSource branch April 15, 2021 22:51
@ghostghost locked as resolved and limited conversation to collaborators May 15, 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.

5 participants

@carlossanlop@davidfowl@stephentoub@adamsitnik@jozkee
, '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: Use IValueTaskSource instead of TaskCompletionSource - #50802

Merged
carlossanlop merged 19 commits into
dotnet:mainfrom
carlossanlop:IValueTaskSource
Apr 15, 2021
Merged

FileStream rewrite: Use IValueTaskSource instead of TaskCompletionSource#50802
carlossanlop merged 19 commits into
dotnet:mainfrom
carlossanlop:IValueTaskSource

Conversation

@carlossanlop

Copy link
Copy Markdown
Contributor

The purpose of this PR is to remove a large allocation found in FileStream.ReadAsync and FileStream.WriteAsync, which we confirmed in our profiling investigation around the new FileStream rewrite code.

The allocation improvement can be achieved by creating a new type, called FileStreamValueTaskSource, that will have the same purpose as FileStreamCompletionSource, but instead of implementing TaskCompletionSource (which returns a Task to represent the asynchronous work) it implements IValueTaskSource, which returns a ValueTask.

The behavior was largely left untouched, except for the following differences:

  • Net5CompatFileStreamStrategy.Windows will be the only strategy that consumes the FileStreamCompletionSource, so it is now a private nested class.
  • One single new type implements both IValueTaskSource (used for write) and IValueTaskSource<int> (used for read).
  • The logic inside the callback and the cancellation token registration is the same, aside for the code that needs to report exceptions or return values.
  • We had interop errors redefined in multiple places.
  • The constants used by FileStreamCompletionSource were moved to FileStreamHelpers so they could be reused by the new type.

Unit tests

Manually ran unit tests from System.IO.FileSystem, System.IO, System.Runtime and System.Runtime.Extensions, they all passed. I made sure to run them with the Net5Compat flag turned off, to ensure my new code was executed.

Benchmarks

  • Ran the FileStream performance benchmarks against .NET 5.0, then against the code of this PR's baseline commit, and then against this PR's new code. I compared the results, and there are subtle improvements in some benchmark tests:
.NET 5.0 vs .NET 6.0 before this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_before_pr' --threshold 0.0001%

summary:
better: 39, geomean: 2.801
worse: 22, geomean: 2.098
total diff: 61

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.93882252.574348913.28
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.621003671.884633945.31bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.13257218.23805205.31several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.13286859.38896524.31
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.061795459.385498006.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.051868294.535689532.29
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)2.99254148.29758897.83
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.97283860.42843052.43
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.901844293.365353701.04several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.892209384.386395554.17bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.65301619.83798302.63
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.86557062.051035575.21bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.81478821.50868866.12
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.693479778.755891160.42several?
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.66543512.61903388.82bimodal
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.40168291900.00235314600.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.248197915.6310200393.75
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.215313143.756444323.96
Perf_FileStream.Flush(fileSize: 1024, options: None)1.183448111.254052132.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055473900.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.0956033900.0061052537.50
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.03133329050.00137489800.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.645489541.6747470.79
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.902811281.2543319.93
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.822571736.4644479.08
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.29876923.2641197.72
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.062463834750.00486578800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.94152781.6630909.09
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.72153731.8132593.67
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437889.31several?
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.98165696.6841632.18
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.97159526.5840152.10
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.50200423.3657204.05
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889181.93
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.55238920.2293543.27
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.50193629.0877357.59
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.443222202.501318286.88
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.39194808.8781591.76
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.36216916.9892096.58
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.24325116900.00145245400.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.157025697.923271982.50
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.10199647.8995019.35
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.04686776300.00336991400.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9519598723.0810074393.75
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)1.85508393800.00274360000.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.74796591200.00457934500.00bimodal
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.73156593600.0090313262.50
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.7022492768.7513248333.33
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64542213800.00331399500.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.615221529.173244615.63
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114593046.88
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742610.27
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09777023.21
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.191301925.001096436.88
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.12142000.45126451.42
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 512, options: None)1.11162435300.00146191400.00
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.10141476.75128484.00
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.10142083.81129048.19
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00113456750.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0288348175.0086894587.50
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 512, options: None)1.0192483275.0091193650.00
.NET 5.0 vs .NET 6.0 after this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_after_pr' --threshold 0.0001%
summary:
better: 36, geomean: 3.006
worse: 22, geomean: 2.080
total diff: 58

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.92882252.574341552.34
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.601003671.884615128.91bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)3.24283860.42919970.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)3.08254148.29783231.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.061868294.535710579.17
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.05286859.38874430.38
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.04257218.23780960.42
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)2.971795459.385329393.75
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.951844293.365432295.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.922209384.386445677.08
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.52301619.83758761.88
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.95557062.051087235.83
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.73543512.61937893.38bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.66478821.50793440.46bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.473479778.755113488.54several?
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.32168291900.00221660000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.235313143.756512252.08
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.228197915.6310012295.65
Perf_FileStream.Flush(fileSize: 1024, options: None)1.213448111.254163660.16
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1356033900.0063237650.00
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0950055150.0054423625.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0088983100.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.505489541.6747526.62
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.942811281.2543292.23
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.062571736.4645070.47
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.05876923.2641661.14
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.362463834750.00459292800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.95152781.6630850.74
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.75153731.8132349.21
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437926.07several?
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.98159526.5840096.31
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.96165696.6841824.65
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889051.28
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.56238920.2293276.60
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.53193629.0876383.11
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.45194808.8779416.04
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501376330.63
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.31216916.9893711.06
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.28325116900.00142880200.00
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)2.22200423.3690245.25
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.167025697.923250746.88
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.11508393800.00241032100.00
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.11199647.8994675.64
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.06686776300.00333108950.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9319598723.0810171781.82
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.74156593600.0090035450.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.65542213800.00329569650.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.645221529.173182177.50
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64796591200.00486630000.00
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.5222492768.7514797453.85
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114625460.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742460.71
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09781337.50
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.201301925.001085495.19
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.11141476.75127158.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.11142083.81128134.40
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.10142000.45129068.15
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00112942550.00

Profiling

I reused the same code we have in our benchmarks, but I executed it inside a console app that forces the consumption of the compiled runtime code.

You'll notice the top Task allocation went away, and the FileStreamCompletionSource allocations were substituted by FileStreamValueTaskSource allocations.

ReadAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,797
- System.String2,153
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,806
- System.String2,153
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]964
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

ReadAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,013
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,840
- System.String2,153
- System.Object[]955
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,836
- System.String2,144
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>64,235
- System.Threading.Tasks.Task<>64,015
- System.IO.FileStreamCompletionSource64,000
- System.SByte[]2,916
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,812
- System.SByte[]2,909
- System.String2,144
- System.Object[]964
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,798
- System.String2,153
- System.Object[]954
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,812
- System.String2,153
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]958
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,249
- System.IO.MemoryFileStreamCompletionSource32,000
- System.IO.FileStreamCompletionSource32,000
- System.SByte[]2,915
- System.String2,155
- System.Object[]952
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,246
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource32,000
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource32,000
- System.SByte[]2,893
- System.String2,144
- System.Object[]954
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.IO.FileStreamCompletionSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,963
- System.SByte[]2,915
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,830
- System.SByte[]2,912
- System.String2,144
- System.Object[]966
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Threading.Tasks.ValueTask.ValueTaskSourceAsTask249
- System.Threading.QueueUserWorkItemCallbackDefaultContext<>249

@adamsitnik@jozkee@jeffhandley@stephentoub

Also: Don't forget to ignore whitespace changes so it's easier to review the nested type's changes 😃

@ghost

ghost commented Apr 6, 2021

Copy link
Copy Markdown

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

Issue Details

The purpose of this PR is to remove a large allocation found in FileStream.ReadAsync and FileStream.WriteAsync, which we confirmed in our profiling investigation around the new FileStream rewrite code.

The allocation improvement can be achieved by creating a new type, called FileStreamValueTaskSource, that will have the same purpose as FileStreamCompletionSource, but instead of implementing TaskCompletionSource (which returns a Task to represent the asynchronous work) it implements IValueTaskSource, which returns a ValueTask.

The behavior was largely left untouched, except for the following differences:

  • Net5CompatFileStreamStrategy.Windows will be the only strategy that consumes the FileStreamCompletionSource, so it is now a private nested class.
  • One single new type implements both IValueTaskSource (used for write) and IValueTaskSource<int> (used for read).
  • The logic inside the callback and the cancellation token registration is the same, aside for the code that needs to report exceptions or return values.
  • We had interop errors redefined in multiple places.
  • The constants used by FileStreamCompletionSource were moved to FileStreamHelpers so they could be reused by the new type.

Unit tests

Manually ran unit tests from System.IO.FileSystem, System.IO, System.Runtime and System.Runtime.Extensions, they all passed. I made sure to run them with the Net5Compat flag turned off, to ensure my new code was executed.

Benchmarks

  • Ran the FileStream performance benchmarks against .NET 5.0, then against the code of this PR's baseline commit, and then against this PR's new code. I compared the results, and there are subtle improvements in some benchmark tests:
.NET 5.0 vs .NET 6.0 before this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_before_pr' --threshold 0.0001%

summary:
better: 39, geomean: 2.801
worse: 22, geomean: 2.098
total diff: 61

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.93882252.574348913.28
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.621003671.884633945.31bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.13257218.23805205.31several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.13286859.38896524.31
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.061795459.385498006.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.051868294.535689532.29
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)2.99254148.29758897.83
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.97283860.42843052.43
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.901844293.365353701.04several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.892209384.386395554.17bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.65301619.83798302.63
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.86557062.051035575.21bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.81478821.50868866.12
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.693479778.755891160.42several?
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.66543512.61903388.82bimodal
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.40168291900.00235314600.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.248197915.6310200393.75
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.215313143.756444323.96
Perf_FileStream.Flush(fileSize: 1024, options: None)1.183448111.254052132.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055473900.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.0956033900.0061052537.50
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.03133329050.00137489800.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.645489541.6747470.79
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.902811281.2543319.93
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.822571736.4644479.08
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.29876923.2641197.72
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.062463834750.00486578800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.94152781.6630909.09
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.72153731.8132593.67
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437889.31several?
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.98165696.6841632.18
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.97159526.5840152.10
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.50200423.3657204.05
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889181.93
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.55238920.2293543.27
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.50193629.0877357.59
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.443222202.501318286.88
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.39194808.8781591.76
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.36216916.9892096.58
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.24325116900.00145245400.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.157025697.923271982.50
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.10199647.8995019.35
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.04686776300.00336991400.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9519598723.0810074393.75
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)1.85508393800.00274360000.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.74796591200.00457934500.00bimodal
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.73156593600.0090313262.50
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.7022492768.7513248333.33
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64542213800.00331399500.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.615221529.173244615.63
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114593046.88
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742610.27
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09777023.21
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.191301925.001096436.88
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.12142000.45126451.42
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 512, options: None)1.11162435300.00146191400.00
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.10141476.75128484.00
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.10142083.81129048.19
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00113456750.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0288348175.0086894587.50
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 512, options: None)1.0192483275.0091193650.00
.NET 5.0 vs .NET 6.0 after this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_after_pr' --threshold 0.0001%
summary:
better: 36, geomean: 3.006
worse: 22, geomean: 2.080
total diff: 58

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.92882252.574341552.34
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.601003671.884615128.91bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)3.24283860.42919970.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)3.08254148.29783231.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.061868294.535710579.17
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.05286859.38874430.38
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.04257218.23780960.42
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)2.971795459.385329393.75
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.951844293.365432295.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.922209384.386445677.08
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.52301619.83758761.88
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.95557062.051087235.83
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.73543512.61937893.38bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.66478821.50793440.46bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.473479778.755113488.54several?
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.32168291900.00221660000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.235313143.756512252.08
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.228197915.6310012295.65
Perf_FileStream.Flush(fileSize: 1024, options: None)1.213448111.254163660.16
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1356033900.0063237650.00
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0950055150.0054423625.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0088983100.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.505489541.6747526.62
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.942811281.2543292.23
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.062571736.4645070.47
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.05876923.2641661.14
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.362463834750.00459292800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.95152781.6630850.74
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.75153731.8132349.21
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437926.07several?
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.98159526.5840096.31
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.96165696.6841824.65
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889051.28
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.56238920.2293276.60
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.53193629.0876383.11
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.45194808.8779416.04
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501376330.63
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.31216916.9893711.06
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.28325116900.00142880200.00
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)2.22200423.3690245.25
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.167025697.923250746.88
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.11508393800.00241032100.00
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.11199647.8994675.64
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.06686776300.00333108950.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9319598723.0810171781.82
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.74156593600.0090035450.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.65542213800.00329569650.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.645221529.173182177.50
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64796591200.00486630000.00
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.5222492768.7514797453.85
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114625460.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742460.71
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09781337.50
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.201301925.001085495.19
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.11141476.75127158.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.11142083.81128134.40
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.10142000.45129068.15
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00112942550.00

Profiling

I reused the same code we have in our benchmarks, but I executed it inside a console app that forces the consumption of the compiled runtime code.

You'll notice the top Task allocation went away, and the FileStreamCompletionSource allocations were substituted by FileStreamValueTaskSource allocations.

ReadAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,797
- System.String2,153
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,806
- System.String2,153
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]964
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

ReadAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,013
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,840
- System.String2,153
- System.Object[]955
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,836
- System.String2,144
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>64,235
- System.Threading.Tasks.Task<>64,015
- System.IO.FileStreamCompletionSource64,000
- System.SByte[]2,916
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,812
- System.SByte[]2,909
- System.String2,144
- System.Object[]964
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,798
- System.String2,153
- System.Object[]954
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,812
- System.String2,153
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]958
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,249
- System.IO.MemoryFileStreamCompletionSource32,000
- System.IO.FileStreamCompletionSource32,000
- System.SByte[]2,915
- System.String2,155
- System.Object[]952
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,246
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource32,000
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource32,000
- System.SByte[]2,893
- System.String2,144
- System.Object[]954
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.IO.FileStreamCompletionSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,963
- System.SByte[]2,915
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,830
- System.SByte[]2,912
- System.String2,144
- System.Object[]966
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Threading.Tasks.ValueTask.ValueTaskSourceAsTask249
- System.Threading.QueueUserWorkItemCallbackDefaultContext<>249

@adamsitnik@jozkee@jeffhandley@stephentoub

Also: Don't forget to ignore whitespace changes so it's easier to review the nested type's changes 😃
Author:carlossanlop
Assignees:carlossanlop
Labels:

area-System.IO

Milestone:6.0.0

@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

Tagging subscribers to this area: @carlossanlop

Yes, thank you bot... 😆

@jozkeejozkee 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.

Otherwise; LGTM, will approve if/when comments are resolved, most of them are nits and/or questions.

Comment on lines +17 to +22
internal const long NoResult = 0;
internal const long ResultSuccess = (long)1 << 32;
internal const long ResultError = (long)2 << 32;
internal const long RegisteringCancellation = (long)4 << 32;
internal const long CompletedCallback = (long)8 << 32;
internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32;

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.

Consider prefixing this constants with AsyncCompletionSource_ or something alike since these names are too ambiguous or keep them in their respective classes regardless of them being duplicated.

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.

or put them into a nested static class called ReturnCodes:

internalstaticpartialclassFileStreamHelpers{internalstaticclassReturnCodes{internalconstlongNoResult=0;internalconstlongResultSuccess=(long)1<<32;internalconstlongResultError=(long)2<<32;internalconstlongRegisteringCancellation=(long)4<<32;internalconstlongCompletedCallback=(long)8<<32;internalconstulongResultMask=((ulong)uint.MaxValue)<<32;}

but it's a nit (not a blocker)

Comment on lines +66 to +67
_strategy.CompareExchangeCurrentOverlappedOwner(this, null) == null ?
_strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!) : // allocated when buffer was created, and buffer is non-null

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.

Suggested change
_strategy.CompareExchangeCurrentOverlappedOwner(this,null)==null?
_strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!):// allocated when buffer was created, and buffer is non-null
strategy.CompareExchangeCurrentOverlappedOwner(this,null)==null?
strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!):// allocated when buffer was created, and buffer is non-null

// be directly the AwaitableProvider that's completing (in the case where the preallocated
// overlapped was already in use by another operation).
object? state = ThreadPoolBoundHandle.GetNativeOverlappedState(pOverlapped);
Debug.Assert(state is (AsyncWindowsFileStreamStrategy or FileStreamValueTaskSource));

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.

Suggested change
Debug.Assert(stateis(AsyncWindowsFileStreamStrategy or FileStreamValueTaskSource));
Debug.Assert(stateisAsyncWindowsFileStreamStrategy or FileStreamValueTaskSource);

int errorCode = unchecked((int)(packedResult & uint.MaxValue));
if (errorCode == Interop.Errors.ERROR_OPERATION_ABORTED)
{
_source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(new OperationCanceledException(cancellationToken.IsCancellationRequested ? cancellationToken : new CancellationToken(canceled: true))));

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.

Suggested change
_source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(newOperationCanceledException(cancellationToken.IsCancellationRequested?cancellationToken:newCancellationToken(canceled:true))));
Exceptione=newOperationCanceledException(cancellationToken.IsCancellationRequested?cancellationToken:newCancellationToken(canceled:true));
e.SetCurrentStackTrace();
_source.SetException(e);

}

private void Cancel(CancellationToken token)
{

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.

Missing comment and asserts:

// WARNING: This may potentially be called under a lock (during cancellation registration)
Debug.Assert(stateisFileStreamCompletionSource,"Unknown state passed to cancellation");
FileStreamCompletionSourcecompletionSource=(FileStreamCompletionSource)state;
Debug.Assert(completionSource._overlapped!=null&&!completionSource.Task.IsCompleted,"IO should not have completed yet");

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The first assert is not needed because the method argument is not an object? state.

I'll add the assert that checks for overlapped an completed status.

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.

The state param could be added to this method and allows you to conserve the first Debug.Assert.

long packedResult = Interlocked.CompareExchange(ref _result, FileStreamHelpers.RegisteringCancellation, FileStreamHelpers.NoResult);
if (packedResult == FileStreamHelpers.NoResult)
{
_cancellationRegistration = cancellationToken.UnsafeRegister(static (s, token) => ((FileStreamValueTaskSource)s!).Cancel(token), this);

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.

Could we pass the callback on a similar fashion to FileStreamCompletionSource

_cancellationRegistration=cancellationToken.UnsafeRegister(cancelCallback,this);

Or why did you opted for this way instead?

@carlossanlopcarlossanlopApr 8, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It made more sense to pass this as the second argument of UnsafeRegister, which ensures it's a ValueTaskSource instance, and saves us from the check of the object? state parameter like in the old code. The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

We have a similar example in System.Threading.Channels.

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.

It made more sense to pass this as the second argument of UnsafeRegister

FileStreamCompletionSource was also passing this as 2nd argument.

The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

Yes, seems that it was caching the method group to avoid allocating everytime it was passed to UnsafeRegister.
However I think you can avoid the allocation without caching yourself by doing this:

_cancellationRegistration=cancellationToken.UnsafeRegister((s,token)=>Cancel(s,token),this);

Just a suggestion, not something that you should block on.

@davidfowl

Copy link
Copy Markdown
Member

This is the change I was waiting for 👏🏾👏🏾

@davidfowl

Copy link
Copy Markdown
Member

I haven't looked but I assume we'll reuse the operation objects if there's no concurrency? This is what sockets do today so in the 95% case of no overlapping writes or reads, there'll be a single reusable allocation on the FileStream

@stephentoub

Copy link
Copy Markdown
Member

This is the change I was waiting for

It's not, actually. You want the next one where the IValueTaskSource instance is cached.

}
// else: Some other thread stole the result, so now it is responsible to finish the callback
}
// else: Some other thread is registering a cancellation, so it *must* finish the callback

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.

All this logic looks more complicated than it needs to be. I realize you're just following the same flow as what was already there, so nothing needs to be changed in this PR. But we should revisit all of this synchronization later; I suspect we can do it more succinctly and efficiently.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I opened an issue to follow up on this and on the caching of the IValueTaskSource.
#50972

@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 before we merge I would like to see the following benchmark results:

.NET 6 with new strategy enabled: before and after your change. (not .NET 5 vs 6 as there are too many moving targets)

You should be able to achieve that by running the following command in the perf repo;

git clone https://github.com/dotnet/performance.git
cd performance\src\benchmarks\micro
dotnet run -c Release -f net6.0 --filter *Perf_FileStream* --coreRun $pathToCoreRunWithoutYourChanges $pathToCoreRunWithYourChanges --envVars "DOTNET_SYSTEM_IO_USENET5COMPATFILESTREAM:0"

/// <see cref="MemoryHandle"/> when the operation has completed. This should only be used
/// when memory doesn't wrap a byte[].
/// </summary>
private sealed class MemoryAwaitableProvider : FileStreamValueTaskSource

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.

This should only be used when memory doesn't wrap a byte[].

I just realized that we don't have benchmarks for this scenario as Memory used in our micro-benchmarks always wraps a byte array. Could you please add such benchmarks to https://github.com/dotnet/performance/blob/main/src/benchmarks/micro/libraries/System.IO.FileSystem/Perf.FileStream.cs ? One test case should be enough as this should be rare.

@carlossanlopcarlossanlopApr 7, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Some of the benchmarks do consume this type. If you look at some of the profiling tests I executed, you'll see that MemoryValueTaskSource shows up a few times. The profiling tests reuse the code we had in our benchmarks.

I don't mind adding an explicit one though. Dou you want me to add one anyway?

Comment on lines +17 to +22
internal const long NoResult = 0;
internal const long ResultSuccess = (long)1 << 32;
internal const long ResultError = (long)2 << 32;
internal const long RegisteringCancellation = (long)4 << 32;
internal const long CompletedCallback = (long)8 << 32;
internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32;

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.

or put them into a nested static class called ReturnCodes:

internalstaticpartialclassFileStreamHelpers{internalstaticclassReturnCodes{internalconstlongNoResult=0;internalconstlongResultSuccess=(long)1<<32;internalconstlongResultError=(long)2<<32;internalconstlongRegisteringCancellation=(long)4<<32;internalconstlongCompletedCallback=(long)8<<32;internalconstulongResultMask=((ulong)uint.MaxValue)<<32;}

but it's a nit (not a blocker)

Comment on lines 62 to 63

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.

Did you run the perf. benchmarks after doing this change and did you notice any impact?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Pending.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Confirmed - There is a slight improvement.

I made sure to compare apples to apples: I used my latest commit, and built it setting RunContinuationsAsynchronously to true, then to false. I compared it both times against our code in .NET 5.0.

The improvement is clear when setting it to false (or not setting it at all).

RunContinuationsAsynchronously = true

❯ dotnet run -c release --base "D:\fs50" --diff "D:\fs60_NEW_ON_IVTS_asynctrue" --threshold 0.0001%
summary:
better: 24, geomean: 3.402
worse: 19, geomean: 2.244
total diff: 43

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)5.49882252.574840275.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)5.121003671.885136350.00bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)4.44257218.231141827.01
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)4.08286859.381171600.96
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.251795459.385832233.33
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)3.111844293.365741847.92several?
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.111868294.535807825.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)3.082209384.386796443.75
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)2.46557062.051371566.15
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)2.33478821.501116033.48
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)2.28543512.611239956.25bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.613479778.755612725.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.448197915.6311814575.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.33168291900.00224355000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.335313143.757063218.75
Perf_FileStream.Flush(fileSize: 1024, options: None)1.253448111.254313832.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1250055150.0056309025.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1256033900.0062803900.00
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.12133329050.00148833000.00bimodal
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)117.075489541.6746889.31
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)65.932811281.2542642.74
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.842571736.4644462.45
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.09876923.2641586.04
Perf_FileStream.OpenClose(fileSize: 1024, options: None)5.00152781.6630548.62
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.76153731.8132314.04
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)4.032463834750.00611183400.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.93165696.6842208.34
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.52200423.3656865.52
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.62230670.3887990.73
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.58238920.2292478.58
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501377939.84
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.87156593600.0083681962.50
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.6619598723.0811803641.67
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.6322492768.7513818900.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.577025697.924487121.88
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)1.47686776300.00466936500.00bimodal
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55743825.45
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.24975146.09784156.88
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.195221529.174387085.94
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.181301925.001103803.79
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.04116486775.00112407200.00
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.0321953590.9121241122.73
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0087467100.00
RunContinuationsAsynchronously = false

dotnet run -c release --base "D:\fs50" --diff "D:\fs60_NEW_ON_IVTS_asyncfalse" --threshold 0.0001%
summary:
better: 23, geomean: 3.592
worse: 20, geomean: 2.135
total diff: 43

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)5.36882252.574730875.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)5.051003671.885071912.50bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)4.25257218.231094164.38several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)4.05286859.381161160.27
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.231795459.385791481.25
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)3.101844293.365709820.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)3.092209384.386836504.17
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.071868294.535733883.33
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)2.46557062.051368034.90
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)2.31478821.501103696.25
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)2.24543512.611218234.38bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.603479778.755572682.29
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.428197915.6311634850.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.33168291900.00223141800.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.315313143.756949831.25
Perf_FileStream.Flush(fileSize: 1024, options: None)1.263448111.254332915.63
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.13133329050.00150223800.00bimodal
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055699050.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1056033900.0061710550.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0088348175.0088759625.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)116.975489541.6746929.42
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)65.762811281.2542753.29
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.562571736.4644676.15
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.52876923.2640756.47
Perf_FileStream.OpenClose(fileSize: 1024, options: None)5.06152781.6630216.63
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.81153731.8131975.57
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)4.052463834750.00608864800.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.99165696.6841526.73
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.54200423.3656610.08
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.62230670.3887877.10
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.59238920.2292291.75
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.403222202.501345116.25
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.86156593600.0083969875.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.6419598723.0811925173.68
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.6222492768.7513859764.71
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.567025697.924493881.25
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)1.46686776300.00470158300.00bimodal
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.32987460.55750294.64
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.24975146.09784056.56
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.171301925.001108880.36
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.175221529.174470673.44
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.0721953590.9120580725.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.01116486775.00

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Here is the result comparison if we compare the benchmark results of setting it to true vs false:

base: RunContinuationsAsynchronously=true, diff: RunContinuationsAsynchronously=false

dotnet run -c release --base "D:\fs60_NEW_ON_IVTS_asynctrue" --diff "D:\fs60_NEW_ON_IVTS_asyncfalse" --threshold 0.0001%
summary:
better: 14, geomean: 1.018
worse: 11, geomean: 1.017
total diff: 25

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.WriteAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0567143550.0070259187.50
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.02112407200.00115107550.00
Perf_FileStream.Read_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.02240016.67244921.93
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.024387085.944470673.44
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0187467100.0088759625.00
Perf_FileStream.WriteAsync_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.0121684354.5521982809.09
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: Asynchronous)1.011236709.901253242.19
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 1024, options: None)1.0139289.5439730.73
Perf_FileStream.Write_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.0121445427.2721646650.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.01743825.45750294.64
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)1.0044462.4544676.15
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.WriteByte(fileSize: 1024, options: None)1.041141827.011094164.38several?
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0345688612.5044474575.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.021377939.841345116.25
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)1.024840275.004730875.00
Perf_FileStream.SeekForward(fileSize: 1024, options: None)1.0241586.0440756.47
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: Asynchronous)1.02130849050.00128463775.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)1.0242208.3441526.73
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.0211814575.0011634850.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)1.015807825.005733883.33
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)1.015136350.005071912.50
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0156309025.0055699050.00
Perf_FileStream.Read_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0133747814.293338345.14
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)1.0132314.0431975.57
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 1024, options: Asynchronous)1.0081988.0981641.78

long packedResult = Interlocked.CompareExchange(ref _result, FileStreamHelpers.RegisteringCancellation, FileStreamHelpers.NoResult);
if (packedResult == FileStreamHelpers.NoResult)
{
_cancellationRegistration = cancellationToken.UnsafeRegister(static (s, token) => ((FileStreamValueTaskSource)s!).Cancel(token), this);

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.

It made more sense to pass this as the second argument of UnsafeRegister

FileStreamCompletionSource was also passing this as 2nd argument.

The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

Yes, seems that it was caching the method group to avoid allocating everytime it was passed to UnsafeRegister.
However I think you can avoid the allocation without caching yourself by doing this:

_cancellationRegistration=cancellationToken.UnsafeRegister((s,token)=>Cancel(s,token),this);

Just a suggestion, not something that you should block on.

Comment on lines 120 to 122

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.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I tried, but a ValueTask does not have a Result property, like a ValueTask<T> does, because it returns void. So I'm not sure if it's possible to apply an equivalent pattern.

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.

publicoverridevoidWrite(byte[]buffer,intoffset,intcount){ValueTaskvt=WriteAsyncInternal(newReadOnlyMemory<byte>(buffer,offset,count),CancellationToken.None);if(vt.IsCompleted){vt.GetAwaiter().GetResult();}else{vt.AsTask().GetAwaiter().GetResult();}}

That said, it's not much of an issue for this case. The read side is an issue because AsTask() will almost certainly need to allocate a new Task<int> in order to hand back the result, but for the write side, AsTask() on an already completed ValueTask will just return Task.Completed, assuming the operation completed successfully. So the only time the existing code for the write side will allocate is when the operation was canceled or faulted, in which case we're about to throw an exception anyway, and the extra task allocation won't really matter. Up to you whether you want to change it.

@jozkeejozkee 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.

LGTM; thanks Carlos!

@adamsitnik

Copy link
Copy Markdown
Member

I compared it both times against our code in .NET 5.0.

Comparison against 5.0 is a comparison of all of the changes that were merged to main since we have snapped 5.0 with your change against 5.0. What we care about is how your change (and nothing else) is affecting the perf, so we need to isolate the runs to only your change.

To be able to do that you need to run the benchmarks using two CoreRuns:

  • as a baseline the corerun that was built from main when you have created a new branch, without any of your changes
  • your branch with your changes

My personal workflow:

  1. When I create a new branch, I perform a full build in Release:
build -c Release -subset clr+libs
  1. I copy the folder that contains CoreRun.exe (runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0) to a separate folder. Typically I call it "before".
  2. I apply some changes and perform an incremental Release build. In case of System.Private.CoreLib.dll it's:
build -c Release -subset clr.corelib+clr.nativecorelib+libs.PreTest
  1. I run the benchmarks using the old and new CoreRun.exe:
dotnet run -c Release -f net6.0 --filter *ABC* --corerun C:\Projects\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\before\CoreRun.exe C:\Projects\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe

@carlossanlop

carlossanlop commented Apr 13, 2021

Copy link
Copy Markdown
ContributorAuthor
  • as a baseline the corerun that was built from main when you have created a new branch, without any of your changes
  • your branch with your changes

@adamsitnik@jozkee here's the table comparing the before (original) and after of this PR. Most results had allocation improvements, but worse speed when FileOptions = Asynchronous:

Results

Command:

 dotnet run
-c Release
-f net6.0
--corerun
D:\runtime2\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe
D:\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe
--filter System.IO.Tests.Perf_FileStream.*Async*
--statisticalTest 3ms
--minIterationCount 100
--maxIterationCount 101
--artifacts "D:\finalresults"

Note: The runtime2 corerun contains the baseline commit before my changes, the runtime corerun contains this PR's changes.

MethodToolchainfileSizeuserBufferSizeoptionsMeanErrorStdDevMedianMinMaxRatioAllocated
ReadAsyncBEFORE10241024None78.69 μs0.255 μs0.736 μs78.53 μs77.61 μs80.66 μs1.005 KB
ReadAsyncAFTER10241024None79.10 μs0.237 μs0.673 μs79.08 μs76.96 μs80.71 μs1.015 KB
WriteAsyncBEFORE10241024None316.97 μs2.512 μs6.919 μs315.92 μs305.11 μs339.35 μs1.004 KB
WriteAsyncAFTER10241024None300.13 μs5.643 μs15.730 μs297.59 μs272.46 μs348.15 μs0.954 KB
ReadAsyncBEFORE10241024Asynchronous94.13 μs0.259 μs0.731 μs94.02 μs92.27 μs96.16 μs1.005 KB
ReadAsyncAFTER10241024Asynchronous85.48 μs0.842 μs2.291 μs85.11 μs82.58 μs92.09 μs0.915 KB
WriteAsyncBEFORE10241024Asynchronous292.63 μs5.659 μs15.869 μs290.59 μs264.25 μs338.98 μs1.005 KB
WriteAsyncAFTER10241024Asynchronous324.35 μs13.906 μs39.222 μs311.27 μs274.34 μs428.64 μs1.115 KB
FlushAsyncBEFORE1024?None4,606.81 μs46.736 μs133.341 μs4,633.92 μs4,374.04 μs4,952.63 μs1.00269 KB
FlushAsyncAFTER1024?None4,587.88 μs58.046 μs168.403 μs4,638.98 μs4,322.79 μs5,034.48 μs1.00269 KB
CopyToFileAsyncBEFORE1024?None474.57 μs5.497 μs15.141 μs474.66 μs440.31 μs520.31 μs1.005 KB
CopyToFileAsyncAFTER1024?None488.94 μs3.631 μs10.061 μs488.62 μs469.93 μs518.76 μs1.035 KB
FlushAsyncBEFORE1024?Asynchronous14,680.35 μs150.967 μs437.982 μs14,637.76 μs13,623.50 μs15,645.01 μs1.00301 KB
FlushAsyncAFTER1024?Asynchronous20,333.22 μs116.277 μs339.185 μs20,299.93 μs19,724.25 μs21,148.73 μs1.39261 KB
CopyToFileAsyncBEFORE1024?Asynchronous520.97 μs5.982 μs16.674 μs519.35 μs490.05 μs572.29 μs1.006 KB
CopyToFileAsyncAFTER1024?Asynchronous523.30 μs6.294 μs17.650 μs520.91 μs479.14 μs583.85 μs1.006 KB
ReadAsyncBEFORE1048576512None1,375.28 μs5.930 μs17.015 μs1,376.60 μs1,340.44 μs1,413.73 μs1.0085 KB
ReadAsyncAFTER1048576512None1,347.16 μs5.128 μs14.958 μs1,345.14 μs1,323.57 μs1,382.14 μs0.9885 KB
WriteAsyncBEFORE1048576512None3,530.03 μs38.149 μs108.222 μs3,521.78 μs3,299.91 μs3,823.63 μs1.0076 KB
WriteAsyncAFTER1048576512None3,473.85 μs47.943 μs134.438 μs3,469.00 μs3,222.90 μs3,832.17 μs0.9876 KB
ReadAsyncBEFORE1048576512Asynchronous3,278.98 μs30.616 μs87.844 μs3,257.98 μs3,151.46 μs3,507.69 μs1.0093 KB
ReadAsyncAFTER1048576512Asynchronous4,399.34 μs16.153 μs47.119 μs4,401.64 μs4,294.71 μs4,531.75 μs1.3483 KB
WriteAsyncBEFORE1048576512Asynchronous5,304.10 μs73.935 μs214.500 μs5,309.27 μs4,771.82 μs5,852.43 μs1.0085 KB
WriteAsyncAFTER1048576512Asynchronous6,506.15 μs94.189 μs271.756 μs6,505.61 μs6,042.59 μs7,132.02 μs1.2375 KB
ReadAsyncBEFORE10485764096None1,090.98 μs6.156 μs18.152 μs1,096.14 μs1,039.20 μs1,120.19 μs1.0029 KB
ReadAsyncAFTER10485764096None1,084.03 μs8.601 μs25.225 μs1,086.13 μs1,042.07 μs1,131.85 μs0.9929 KB
WriteAsyncBEFORE10485764096None3,247.60 μs43.259 μs124.117 μs3,243.69 μs2,954.42 μs3,556.50 μs1.0055 KB
WriteAsyncAFTER10485764096None3,250.35 μs36.664 μs100.985 μs3,234.77 μs3,039.74 μs3,505.95 μs1.0055 KB
ReadAsyncBEFORE10485764096Asynchronous3,273.61 μs17.591 μs50.753 μs3,264.72 μs3,190.18 μs3,410.97 μs1.0079 KB
ReadAsyncAFTER10485764096Asynchronous4,547.38 μs73.438 μs216.535 μs4,398.34 μs4,327.10 μs5,035.98 μs1.3969 KB
WriteAsyncBEFORE10485764096Asynchronous5,384.46 μs83.792 μs240.415 μs5,346.69 μs4,897.35 μs5,955.92 μs1.0084 KB
WriteAsyncAFTER10485764096Asynchronous6,845.63 μs88.794 μs256.192 μs6,883.75 μs6,079.92 μs7,286.73 μs1.2774 KB
ReadAsync_NoBufferingBEFORE104857616384None406.34 μs2.413 μs7.113 μs403.28 μs387.16 μs419.08 μs1.007 KB
ReadAsync_NoBufferingAFTER104857616384None380.35 μs3.413 μs10.063 μs378.36 μs363.73 μs402.82 μs0.947 KB
WriteAsync_NoBufferingBEFORE104857616384None2,545.17 μs30.174 μs83.613 μs2,526.42 μs2,397.67 μs2,776.47 μs1.007 KB
WriteAsync_NoBufferingAFTER104857616384None2,537.77 μs27.489 μs76.628 μs2,528.02 μs2,421.65 μs2,750.68 μs1.007 KB
ReadAsync_NoBufferingBEFORE104857616384Asynchronous949.54 μs6.336 μs18.077 μs943.69 μs926.29 μs1,002.20 μs1.0020 KB
ReadAsync_NoBufferingAFTER104857616384Asynchronous1,195.21 μs3.070 μs8.507 μs1,194.94 μs1,179.45 μs1,218.92 μs1.2617 KB
WriteAsync_NoBufferingBEFORE104857616384Asynchronous2,965.91 μs40.616 μs114.558 μs2,952.10 μs2,677.93 μs3,244.55 μs1.0020 KB
WriteAsync_NoBufferingAFTER104857616384Asynchronous3,021.90 μs40.697 μs116.110 μs3,000.74 μs2,803.04 μs3,343.84 μs1.0217 KB
CopyToFileAsyncBEFORE1048576?None2,440.43 μs17.056 μs49.754 μs2,437.81 μs2,343.18 μs2,576.37 μs1.003 KB
CopyToFileAsyncAFTER1048576?None2,421.23 μs15.376 μs43.869 μs2,413.43 μs2,350.26 μs2,521.45 μs0.993 KB
CopyToFileAsyncBEFORE1048576?Asynchronous2,878.14 μs16.682 μs47.863 μs2,876.69 μs2,771.42 μs3,001.49 μs1.004 KB
CopyToFileAsyncAFTER1048576?Asynchronous2,769.09 μs15.649 μs43.882 μs2,760.85 μs2,691.56 μs2,885.67 μs0.964 KB
ReadAsyncBEFORE1048576004096None115,657.20 μs352.576 μs1,039.577 μs115,639.45 μs113,670.15 μs117,832.15 μs1.002,801 KB
ReadAsyncAFTER1048576004096None116,612.88 μs249.577 μs731.966 μs116,751.05 μs114,615.00 μs117,903.05 μs1.012,801 KB
WriteAsyncBEFORE1048576004096None205,378.59 μs7,785.258 μs22,462.248 μs194,584.50 μs184,125.30 μs268,874.60 μs1.005,005 KB
WriteAsyncAFTER1048576004096None209,453.56 μs10,355.532 μs30,043.276 μs194,064.20 μs173,778.40 μs293,593.30 μs1.035,005 KB
ReadAsyncBEFORE1048576004096Asynchronous327,225.97 μs1,532.011 μs4,346.059 μs326,564.00 μs320,187.10 μs339,508.20 μs1.007,801 KB
ReadAsyncAFTER1048576004096Asynchronous457,654.89 μs902.286 μs2,559.634 μs457,207.00 μs451,652.60 μs465,093.00 μs1.406,801 KB
WriteAsyncBEFORE1048576004096Asynchronous454,994.00 μs7,661.181 μs21,857.780 μs449,253.50 μs421,512.80 μs511,199.10 μs1.007,905 KB
WriteAsyncAFTER1048576004096Asynchronous600,076.62 μs10,088.501 μs29,268.570 μs592,035.20 μs559,265.50 μs677,403.80 μs1.326,905 KB
ReadAsync_NoBufferingBEFORE10485760016384None45,086.23 μs156.169 μs453.074 μs45,005.65 μs43,497.93 μs46,041.93 μs1.00701 KB
ReadAsync_NoBufferingAFTER10485760016384None42,133.53 μs174.922 μs457.740 μs42,093.18 μs41,187.75 μs43,731.75 μs0.93701 KB
WriteAsync_NoBufferingBEFORE10485760016384None61,814.53 μs685.277 μs1,887.453 μs61,419.69 μs59,079.38 μs68,035.43 μs1.00701 KB
WriteAsync_NoBufferingAFTER10485760016384None61,435.05 μs720.962 μs1,973.622 μs61,127.18 μs58,506.18 μs67,827.25 μs0.99701 KB
ReadAsync_NoBufferingBEFORE10485760016384Asynchronous97,220.19 μs667.147 μs1,881.701 μs96,464.10 μs94,386.65 μs102,972.80 μs1.001,951 KB
ReadAsync_NoBufferingAFTER10485760016384Asynchronous128,169.40 μs556.512 μs1,587.760 μs127,771.80 μs125,455.30 μs133,061.15 μs1.321,701 KB
WriteAsync_NoBufferingBEFORE10485760016384Asynchronous136,593.81 μs1,249.681 μs3,462.860 μs136,822.00 μs127,534.30 μs146,281.60 μs1.001,951 KB
WriteAsync_NoBufferingAFTER10485760016384Asynchronous169,656.11 μs2,245.877 μs6,515.696 μs168,284.30 μs157,404.20 μs189,342.10 μs1.251,701 KB
CopyToFileAsyncBEFORE104857600?None56,537.38 μs245.101 μs695.312 μs56,440.93 μs55,567.65 μs58,581.80 μs1.00177 KB
CopyToFileAsyncAFTER104857600?None56,184.32 μs275.348 μs785.585 μs56,116.43 μs54,790.00 μs58,574.72 μs0.99177 KB
CopyToFileAsyncBEFORE104857600?Asynchronous87,668.39 μs312.004 μs905.181 μs87,555.38 μs85,324.65 μs89,838.85 μs1.00246 KB
CopyToFileAsyncAFTER104857600?Asynchronous78,490.65 μs501.805 μs1,431.678 μs78,350.84 μs75,379.32 μs82,799.10 μs0.90214 KB

…Source only needed by Net5CompatFileStreamStrategy.
… one allocation and get the actual profiling improvement.
Rename FileStreamAwaitableProvider.
… review. Shorter filename for awaitable provider.
…trategy, and its _fileHandle can now be private.
@adamsitnik

Copy link
Copy Markdown
Member

Most results had allocation improvements, but worse speed when FileOptions = Asynchronous:

As discussed offline, such a regression needs to be solved before merging the PR.

I've even built your fork and run the benchmarks myself to be 100% sure, but unfortunately, I've confirmed the regression:

MethodToolchainfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsync_NoBuffering\after\corerun.exe104857616384Asynchronous838.1 us1.1217 KB
ReadAsync_NoBuffering\before\corerun.exe104857616384Asynchronous748.1 us1.0020 KB
ReadAsync_NoBuffering\after\corerun.exe10485760016384Asynchronous93,214.6 us1.121,700 KB
ReadAsync_NoBuffering\before\corerun.exe10485760016384Asynchronous83,445.4 us1.001,950 KB

If I can suggest something, ReadAsync_NoBuffering with fileSize=1048576 and options=Asynchronous should be the best benchmark for creating a repo app and profiling it. It's the simplest benchmark (buffering is not involved) and ReadAsync has a smaller deviation than WriteAsync. If you fail to find the regression with VS Profiler, you can use Perfview.

@adamsitnikadamsitnik added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Apr 14, 2021
@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

I compared the CPU usage before and after, and discovered we spend a considerable amount of time inside Overlapped.PerformIOCompletionCallback.

The new code (left) consumes 62% of the time calling ExecutionContext.RunInternal, while the old code (right) only took 3.82%.

Screenshot

image

The root cause seems to be this line, where we call _source.SetResult:

Screenshot

image

Which is adding up the time it takes to run the rest of the ReadAsync calls in the loop, including the disposal of the FileStream object in the main method.

There's a comment stating that ExecutionContext.RunInternal (part of the callstack) warning that the code is a very hot path:

Screenshot

image

I wasn't expecting all the continuation code to be counted as part of the asynchronous calls. I wonder if this has anything to do with the removal of RunContinuationsAsynchronously = true flag. I'll bring that flag back and compare the results.

cc @jozkee@adamsitnik@stephentoub

@davidfowl

Copy link
Copy Markdown
Member

We don't need to restore the ExecutionContext in the overlapped, that restore should be avoided in these APIs. The async state machine already does it. This is what you need #42549 😄

@stephentoub

stephentoub commented Apr 14, 2021

Copy link
Copy Markdown
Member

The new code (left) consumes 62% of the time calling ExecutionContext.RunInternal, while the old code (right) only took 3.82%.

That's inclusive (what "Total" means in the VS view). The callback is actually running the continuation now (because of RunContinuationsAsynchronously=false), rather than queueing it to be run on a different thread, so the 62% is inclusive of the actual app code.

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 know you're copying this logic from what was there before, but it seems to be unnecessarily complex, with lots of interlocked operations for transitioning from one state to another to another. If we're looking for places to reduce overheads, revisiting this whole implementation is likely a good place to start.

@adamsitnik

Copy link
Copy Markdown
Member

The callback is actually running the continuation now (because of RunContinuationsAsynchronously=false)

I've run all the async benchmarks for 3 configurations:

  • "before" (@carlossanlop fork main branch)
  • "false" (the current state of this PR as RunContinuationsAsynchronously is not set so it's false)
  • "true" ( _source.RunContinuationsAsynchronously = true; in ctor)

With the following settings:

  • 100 instead of 20 iterations (--minIterationCount 100 --maxIterationCount 101)
  • no outliers removal (--outliers DontRemove)

and confirmed that with _source.RunContinuationsAsynchronously = true; we have no regression in the microbenchmarks

@stephentoub should we:

  • set RunContinuationsAsynchronously = true; and include this PR in preview4
  • get some "real world" numbers from ASP.NET benchmarks (if there are none, write some and contribute to https://github.com/aspnet/benchmarks) and see how RunContinuationsAsynchronously setting affects non-micro benchmarks. This means preview5 for this PR.
Details
MethodToolchainfileSizeuserBufferSizeoptionsMeanStdDevMedianMinMaxRatioAllocated
ReadAsync\before\corerun.exe10241024Asynchronous82.69 us1.946 us82.22 us81.60 us94.90 us1.005 KB
ReadAsync\false\corerun.exe10241024Asynchronous78.76 us0.718 us78.56 us78.04 us82.13 us0.955 KB
ReadAsync\true\corerun.exe10241024Asynchronous83.28 us0.825 us83.12 us82.48 us89.82 us1.015 KB
WriteAsync\before\corerun.exe10241024Asynchronous404.05 us22.574 us400.81 us379.20 us494.53 us1.005 KB
WriteAsync\false\corerun.exe10241024Asynchronous400.76 us17.211 us397.80 us380.07 us472.21 us0.995 KB
WriteAsync\true\corerun.exe10241024Asynchronous405.20 us25.469 us401.47 us383.59 us510.74 us1.015 KB
ReadAsync\before\corerun.exe1048576512Asynchronous2,418.83 us55.684 us2,413.59 us2,361.95 us2,804.46 us1.0093 KB
ReadAsync\false\corerun.exe1048576512Asynchronous2,648.89 us73.366 us2,626.45 us2,550.29 us2,959.92 us1.1083 KB
ReadAsync\true\corerun.exe1048576512Asynchronous2,386.25 us9.985 us2,386.16 us2,356.79 us2,419.58 us0.9983 KB
WriteAsync\before\corerun.exe1048576512Asynchronous4,254.70 us277.778 us4,213.63 us3,967.55 us6,203.71 us1.0085 KB
WriteAsync\false\corerun.exe1048576512Asynchronous4,583.08 us183.949 us4,559.42 us4,317.29 us5,334.19 us1.0875 KB
WriteAsync\true\corerun.exe1048576512Asynchronous4,207.77 us179.340 us4,173.53 us3,896.65 us4,865.02 us0.9975 KB
ReadAsync\before\corerun.exe10485764096Asynchronous2,343.06 us35.339 us2,355.62 us2,225.73 us2,379.72 us1.0079 KB
ReadAsync\false\corerun.exe10485764096Asynchronous2,603.91 us40.916 us2,603.11 us2,513.55 us2,715.90 us1.1169 KB
ReadAsync\true\corerun.exe10485764096Asynchronous2,324.68 us38.225 us2,337.29 us2,226.14 us2,374.04 us0.9969 KB
WriteAsync\before\corerun.exe10485764096Asynchronous4,110.48 us265.902 us4,050.53 us3,844.25 us6,007.74 us1.0084 KB
WriteAsync\false\corerun.exe10485764096Asynchronous4,572.16 us320.658 us4,542.09 us4,256.30 us7,440.25 us1.1274 KB
WriteAsync\true\corerun.exe10485764096Asynchronous4,048.01 us184.553 us4,015.01 us3,739.28 us4,754.61 us0.9974 KB
ReadAsync_NoBuffering\before\corerun.exe104857616384Asynchronous747.53 us5.076 us747.87 us729.80 us755.69 us1.0020 KB
ReadAsync_NoBuffering\false\corerun.exe104857616384Asynchronous822.87 us6.877 us822.90 us809.94 us838.18 us1.1017 KB
ReadAsync_NoBuffering\true\corerun.exe104857616384Asynchronous726.82 us11.262 us726.44 us699.96 us754.36 us0.9717 KB
WriteAsync_NoBuffering\before\corerun.exe104857616384Asynchronous2,726.54 us136.755 us2,712.48 us2,534.40 us3,234.12 us1.0020 KB
WriteAsync_NoBuffering\false\corerun.exe104857616384Asynchronous2,761.05 us146.361 us2,792.11 us2,507.70 us3,188.16 us1.0117 KB
WriteAsync_NoBuffering\true\corerun.exe104857616384Asynchronous2,736.46 us194.495 us2,705.98 us2,415.11 us4,022.13 us1.0117 KB
ReadAsync\before\corerun.exe1048576004096Asynchronous250,490.85 us2,604.707 us250,256.45 us244,814.30 us256,607.90 us1.007,801 KB
ReadAsync\false\corerun.exe1048576004096Asynchronous278,260.89 us4,931.050 us278,009.45 us269,882.60 us297,338.40 us1.116,803 KB
ReadAsync\true\corerun.exe1048576004096Asynchronous252,682.05 us19,763.442 us250,702.50 us243,911.20 us447,534.10 us1.016,801 KB
WriteAsync\before\corerun.exe1048576004096Asynchronous370,893.59 us6,590.799 us369,781.05 us352,834.20 us390,366.40 us1.007,905 KB
WriteAsync\false\corerun.exe1048576004096Asynchronous397,619.03 us8,020.098 us397,052.95 us384,609.00 us446,559.70 us1.076,905 KB
WriteAsync\true\corerun.exe1048576004096Asynchronous358,128.05 us6,536.171 us357,755.45 us346,707.00 us382,735.70 us0.976,905 KB

@adamsitnikadamsitnik removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Apr 15, 2021

@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.

LGTM, thank you @carlossanlop !

@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

With the unit tests passing, the micro benchmarks having no regressions after reverting RunContinuationsAsynchronously to true, and having allocation improvements, I feel confident in merging this change to include it in Preview4, and will continue working on #50972 to add caching and improve this code for Preview5.

@carlossanlop
carlossanlop merged commit a38d0c2 into dotnet:mainApr 15, 2021
@carlossanlop
carlossanlop deleted the IValueTaskSource branch April 15, 2021 22:51
@ghostghost locked as resolved and limited conversation to collaborators May 15, 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.

5 participants

@carlossanlop@davidfowl@stephentoub@adamsitnik@jozkee
, '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: Use IValueTaskSource instead of TaskCompletionSource - #50802

Merged
carlossanlop merged 19 commits into
dotnet:mainfrom
carlossanlop:IValueTaskSource
Apr 15, 2021
Merged

FileStream rewrite: Use IValueTaskSource instead of TaskCompletionSource#50802
carlossanlop merged 19 commits into
dotnet:mainfrom
carlossanlop:IValueTaskSource

Conversation

@carlossanlop

Copy link
Copy Markdown
Contributor

The purpose of this PR is to remove a large allocation found in FileStream.ReadAsync and FileStream.WriteAsync, which we confirmed in our profiling investigation around the new FileStream rewrite code.

The allocation improvement can be achieved by creating a new type, called FileStreamValueTaskSource, that will have the same purpose as FileStreamCompletionSource, but instead of implementing TaskCompletionSource (which returns a Task to represent the asynchronous work) it implements IValueTaskSource, which returns a ValueTask.

The behavior was largely left untouched, except for the following differences:

  • Net5CompatFileStreamStrategy.Windows will be the only strategy that consumes the FileStreamCompletionSource, so it is now a private nested class.
  • One single new type implements both IValueTaskSource (used for write) and IValueTaskSource<int> (used for read).
  • The logic inside the callback and the cancellation token registration is the same, aside for the code that needs to report exceptions or return values.
  • We had interop errors redefined in multiple places.
  • The constants used by FileStreamCompletionSource were moved to FileStreamHelpers so they could be reused by the new type.

Unit tests

Manually ran unit tests from System.IO.FileSystem, System.IO, System.Runtime and System.Runtime.Extensions, they all passed. I made sure to run them with the Net5Compat flag turned off, to ensure my new code was executed.

Benchmarks

  • Ran the FileStream performance benchmarks against .NET 5.0, then against the code of this PR's baseline commit, and then against this PR's new code. I compared the results, and there are subtle improvements in some benchmark tests:
.NET 5.0 vs .NET 6.0 before this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_before_pr' --threshold 0.0001%

summary:
better: 39, geomean: 2.801
worse: 22, geomean: 2.098
total diff: 61

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.93882252.574348913.28
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.621003671.884633945.31bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.13257218.23805205.31several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.13286859.38896524.31
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.061795459.385498006.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.051868294.535689532.29
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)2.99254148.29758897.83
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.97283860.42843052.43
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.901844293.365353701.04several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.892209384.386395554.17bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.65301619.83798302.63
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.86557062.051035575.21bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.81478821.50868866.12
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.693479778.755891160.42several?
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.66543512.61903388.82bimodal
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.40168291900.00235314600.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.248197915.6310200393.75
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.215313143.756444323.96
Perf_FileStream.Flush(fileSize: 1024, options: None)1.183448111.254052132.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055473900.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.0956033900.0061052537.50
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.03133329050.00137489800.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.645489541.6747470.79
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.902811281.2543319.93
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.822571736.4644479.08
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.29876923.2641197.72
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.062463834750.00486578800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.94152781.6630909.09
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.72153731.8132593.67
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437889.31several?
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.98165696.6841632.18
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.97159526.5840152.10
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.50200423.3657204.05
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889181.93
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.55238920.2293543.27
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.50193629.0877357.59
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.443222202.501318286.88
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.39194808.8781591.76
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.36216916.9892096.58
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.24325116900.00145245400.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.157025697.923271982.50
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.10199647.8995019.35
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.04686776300.00336991400.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9519598723.0810074393.75
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)1.85508393800.00274360000.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.74796591200.00457934500.00bimodal
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.73156593600.0090313262.50
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.7022492768.7513248333.33
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64542213800.00331399500.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.615221529.173244615.63
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114593046.88
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742610.27
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09777023.21
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.191301925.001096436.88
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.12142000.45126451.42
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 512, options: None)1.11162435300.00146191400.00
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.10141476.75128484.00
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.10142083.81129048.19
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00113456750.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0288348175.0086894587.50
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 512, options: None)1.0192483275.0091193650.00
.NET 5.0 vs .NET 6.0 after this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_after_pr' --threshold 0.0001%
summary:
better: 36, geomean: 3.006
worse: 22, geomean: 2.080
total diff: 58

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.92882252.574341552.34
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.601003671.884615128.91bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)3.24283860.42919970.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)3.08254148.29783231.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.061868294.535710579.17
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.05286859.38874430.38
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.04257218.23780960.42
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)2.971795459.385329393.75
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.951844293.365432295.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.922209384.386445677.08
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.52301619.83758761.88
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.95557062.051087235.83
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.73543512.61937893.38bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.66478821.50793440.46bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.473479778.755113488.54several?
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.32168291900.00221660000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.235313143.756512252.08
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.228197915.6310012295.65
Perf_FileStream.Flush(fileSize: 1024, options: None)1.213448111.254163660.16
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1356033900.0063237650.00
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0950055150.0054423625.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0088983100.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.505489541.6747526.62
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.942811281.2543292.23
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.062571736.4645070.47
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.05876923.2641661.14
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.362463834750.00459292800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.95152781.6630850.74
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.75153731.8132349.21
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437926.07several?
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.98159526.5840096.31
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.96165696.6841824.65
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889051.28
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.56238920.2293276.60
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.53193629.0876383.11
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.45194808.8779416.04
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501376330.63
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.31216916.9893711.06
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.28325116900.00142880200.00
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)2.22200423.3690245.25
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.167025697.923250746.88
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.11508393800.00241032100.00
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.11199647.8994675.64
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.06686776300.00333108950.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9319598723.0810171781.82
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.74156593600.0090035450.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.65542213800.00329569650.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.645221529.173182177.50
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64796591200.00486630000.00
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.5222492768.7514797453.85
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114625460.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742460.71
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09781337.50
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.201301925.001085495.19
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.11141476.75127158.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.11142083.81128134.40
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.10142000.45129068.15
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00112942550.00

Profiling

I reused the same code we have in our benchmarks, but I executed it inside a console app that forces the consumption of the compiled runtime code.

You'll notice the top Task allocation went away, and the FileStreamCompletionSource allocations were substituted by FileStreamValueTaskSource allocations.

ReadAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,797
- System.String2,153
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,806
- System.String2,153
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]964
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

ReadAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,013
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,840
- System.String2,153
- System.Object[]955
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,836
- System.String2,144
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>64,235
- System.Threading.Tasks.Task<>64,015
- System.IO.FileStreamCompletionSource64,000
- System.SByte[]2,916
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,812
- System.SByte[]2,909
- System.String2,144
- System.Object[]964
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,798
- System.String2,153
- System.Object[]954
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,812
- System.String2,153
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]958
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,249
- System.IO.MemoryFileStreamCompletionSource32,000
- System.IO.FileStreamCompletionSource32,000
- System.SByte[]2,915
- System.String2,155
- System.Object[]952
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,246
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource32,000
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource32,000
- System.SByte[]2,893
- System.String2,144
- System.Object[]954
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.IO.FileStreamCompletionSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,963
- System.SByte[]2,915
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,830
- System.SByte[]2,912
- System.String2,144
- System.Object[]966
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Threading.Tasks.ValueTask.ValueTaskSourceAsTask249
- System.Threading.QueueUserWorkItemCallbackDefaultContext<>249

@adamsitnik@jozkee@jeffhandley@stephentoub

Also: Don't forget to ignore whitespace changes so it's easier to review the nested type's changes 😃

@ghost

ghost commented Apr 6, 2021

Copy link
Copy Markdown

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

Issue Details

The purpose of this PR is to remove a large allocation found in FileStream.ReadAsync and FileStream.WriteAsync, which we confirmed in our profiling investigation around the new FileStream rewrite code.

The allocation improvement can be achieved by creating a new type, called FileStreamValueTaskSource, that will have the same purpose as FileStreamCompletionSource, but instead of implementing TaskCompletionSource (which returns a Task to represent the asynchronous work) it implements IValueTaskSource, which returns a ValueTask.

The behavior was largely left untouched, except for the following differences:

  • Net5CompatFileStreamStrategy.Windows will be the only strategy that consumes the FileStreamCompletionSource, so it is now a private nested class.
  • One single new type implements both IValueTaskSource (used for write) and IValueTaskSource<int> (used for read).
  • The logic inside the callback and the cancellation token registration is the same, aside for the code that needs to report exceptions or return values.
  • We had interop errors redefined in multiple places.
  • The constants used by FileStreamCompletionSource were moved to FileStreamHelpers so they could be reused by the new type.

Unit tests

Manually ran unit tests from System.IO.FileSystem, System.IO, System.Runtime and System.Runtime.Extensions, they all passed. I made sure to run them with the Net5Compat flag turned off, to ensure my new code was executed.

Benchmarks

  • Ran the FileStream performance benchmarks against .NET 5.0, then against the code of this PR's baseline commit, and then against this PR's new code. I compared the results, and there are subtle improvements in some benchmark tests:
.NET 5.0 vs .NET 6.0 before this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_before_pr' --threshold 0.0001%

summary:
better: 39, geomean: 2.801
worse: 22, geomean: 2.098
total diff: 61

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.93882252.574348913.28
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.621003671.884633945.31bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.13257218.23805205.31several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.13286859.38896524.31
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.061795459.385498006.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.051868294.535689532.29
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)2.99254148.29758897.83
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.97283860.42843052.43
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.901844293.365353701.04several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.892209384.386395554.17bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.65301619.83798302.63
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.86557062.051035575.21bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.81478821.50868866.12
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.693479778.755891160.42several?
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.66543512.61903388.82bimodal
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.40168291900.00235314600.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.248197915.6310200393.75
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.215313143.756444323.96
Perf_FileStream.Flush(fileSize: 1024, options: None)1.183448111.254052132.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055473900.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.0956033900.0061052537.50
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.03133329050.00137489800.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.645489541.6747470.79
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.902811281.2543319.93
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.822571736.4644479.08
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.29876923.2641197.72
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.062463834750.00486578800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.94152781.6630909.09
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.72153731.8132593.67
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437889.31several?
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.98165696.6841632.18
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.97159526.5840152.10
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.50200423.3657204.05
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889181.93
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.55238920.2293543.27
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.50193629.0877357.59
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.443222202.501318286.88
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.39194808.8781591.76
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.36216916.9892096.58
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.24325116900.00145245400.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.157025697.923271982.50
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.10199647.8995019.35
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.04686776300.00336991400.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9519598723.0810074393.75
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)1.85508393800.00274360000.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.74796591200.00457934500.00bimodal
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.73156593600.0090313262.50
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.7022492768.7513248333.33
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64542213800.00331399500.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.615221529.173244615.63
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114593046.88
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742610.27
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09777023.21
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.191301925.001096436.88
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.12142000.45126451.42
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 512, options: None)1.11162435300.00146191400.00
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.10141476.75128484.00
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.10142083.81129048.19
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00113456750.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0288348175.0086894587.50
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 512, options: None)1.0192483275.0091193650.00
.NET 5.0 vs .NET 6.0 after this PR

dotnet run --project D:\performance\src\tools\ResultsComparer\ResultsComparer.csproj -c release --base 'D:\fs50' --diff 'D:\fs60_after_pr' --threshold 0.0001%
summary:
better: 36, geomean: 3.006
worse: 22, geomean: 2.080
total diff: 58

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)4.92882252.574341552.34
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)4.601003671.884615128.91bimodal
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)3.24283860.42919970.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 512, options: None)3.08254148.29783231.25
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.061868294.535710579.17
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)3.05286859.38874430.38
Perf_FileStream.WriteByte(fileSize: 1024, options: None)3.04257218.23780960.42
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)2.971795459.385329393.75
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)2.951844293.365432295.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)2.922209384.386445677.08
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 512, options: None)2.52301619.83758761.88
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)1.95557062.051087235.83
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)1.73543512.61937893.38bimodal
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)1.66478821.50793440.46bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.473479778.755113488.54several?
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.32168291900.00221660000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.235313143.756512252.08
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.228197915.6310012295.65
Perf_FileStream.Flush(fileSize: 1024, options: None)1.213448111.254163660.16
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1356033900.0063237650.00
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0950055150.0054423625.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0088983100.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)115.505489541.6747526.62
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)64.942811281.2543292.23
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.062571736.4645070.47
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.05876923.2641661.14
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)5.362463834750.00459292800.00
Perf_FileStream.OpenClose(fileSize: 1024, options: None)4.95152781.6630850.74
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.75153731.8132349.21
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 4096, options: None)4.27161781.8437926.07several?
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 512, options: None)3.98159526.5840096.31
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.96165696.6841824.65
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.59230670.3889051.28
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.56238920.2293276.60
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: None)2.53193629.0876383.11
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: None)2.45194808.8779416.04
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501376330.63
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)2.31216916.9893711.06
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.28325116900.00142880200.00
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)2.22200423.3690245.25
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)2.167025697.923250746.88
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: None)2.11508393800.00241032100.00
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 512, options: Asynchronous)2.11199647.8994675.64
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)2.06686776300.00333108950.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.9319598723.0810171781.82
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.74156593600.0090035450.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.65542213800.00329569650.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.645221529.173182177.50
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 512, options: Asynchronous)1.64796591200.00486630000.00
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.5222492768.7514797453.85
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.5021953590.9114625460.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55742460.71
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.25975146.09781337.50
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.201301925.001085495.19
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: Asynchronous)1.11141476.75127158.59
Perf_FileStream.Write(fileSize: 1024, userBufferSize: 4096, options: None)1.11142083.81128134.40
Perf_FileStream.WriteAsync(fileSize: 1024, userBufferSize: 4096, options: None)1.10142000.45129068.15
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.03116486775.00112942550.00

Profiling

I reused the same code we have in our benchmarks, but I executed it inside a console app that forces the consumption of the compiled runtime code.

You'll notice the top Task allocation went away, and the FileStreamCompletionSource allocations were substituted by FileStreamValueTaskSource allocations.

ReadAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,797
- System.String2,153
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]952
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,806
- System.String2,153
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,803
- System.String2,144
- System.Object[]964
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

ReadAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,013
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,840
- System.String2,153
- System.Object[]955
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,836
- System.String2,144
- System.Object[]957
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

ReadAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>64,235
- System.Threading.Tasks.Task<>64,015
- System.IO.FileStreamCompletionSource64,000
- System.SByte[]2,916
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,812
- System.SByte[]2,909
- System.String2,144
- System.Object[]964
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: 1, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,001
- System.IO.MemoryFileStreamCompletionSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,798
- System.String2,153
- System.Object[]954
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource64,000
- System.Threading.ThreadPoolBoundHandleOverlapped64,000
- System.Threading.OverlappedData64,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: 1, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>512,001
- System.IO.MemoryFileStreamCompletionSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,812
- System.String2,153
- System.Object[]959
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource512,000
- System.Threading.ThreadPoolBoundHandleOverlapped512,000
- System.Threading.OverlappedData512,000
- System.SByte[]2,790
- System.String2,144
- System.Object[]958
- System.Reflection.RuntimeParameterInfo650
- System.Byte[]539
- System.Reflection.ParameterInfo[]281
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>251
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandle250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225
- System.RuntimeType[]178

WriteAsync(bufferSize: FourKibibytes, userBufferSize: FourKibibytes, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,249
- System.IO.MemoryFileStreamCompletionSource32,000
- System.IO.FileStreamCompletionSource32,000
- System.SByte[]2,915
- System.String2,155
- System.Object[]952
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.Threading.ThreadPoolBoundHandleOverlapped32,250
- System.Threading.OverlappedData32,250
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>32,246
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.MemoryFileStreamValueTaskSource32,000
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource32,000
- System.SByte[]2,893
- System.String2,144
- System.Object[]954
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Reflection.RuntimeMethodInfo241
- System.Signature225

WriteAsync(bufferSize: FourKibibytes, userBufferSize: HalfKibibyte, FileOptions.Asynchronous)

Allocations before this PR
TypeAllocations
- System.Threading.Tasks.Task<>64,014
- System.IO.FileStreamCompletionSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,963
- System.SByte[]2,915
- System.String2,155
- System.Object[]956
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Diagnostics.Tracing.ScalarTypeInfo489
- System.Func<,,>485
- System.Reflection.ParameterInfo[]281
- System.IO.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
Allocations after this PR
TypeAllocations
- System.IO.Strategies.AsyncWindowsFileStreamStrategy.FileStreamValueTaskSource64,000
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>.AsyncStateMachineBox<>63,830
- System.SByte[]2,912
- System.String2,144
- System.Object[]966
- System.Byte[]789
- System.Reflection.RuntimeParameterInfo650
- System.Reflection.ParameterInfo[]281
- System.IO.Strategies.BufferedFileStreamStrategy252
- Microsoft.Win32.SafeHandles.SafeFileHandle252
- System.IO.FileStream252
- System.IO.Strategies.AsyncWindowsFileStreamStrategy250
- System.Threading.ThreadPoolBoundHandleOverlapped250
- System.Threading.OverlappedData250
- System.Threading.SemaphoreSlim250
- System.Threading.ThreadPoolBoundHandle250
- System.Threading.PreAllocatedOverlapped250
- System.Runtime.CompilerServices.StrongBox<>250
- System.Threading.Tasks.ValueTask.ValueTaskSourceAsTask249
- System.Threading.QueueUserWorkItemCallbackDefaultContext<>249

@adamsitnik@jozkee@jeffhandley@stephentoub

Also: Don't forget to ignore whitespace changes so it's easier to review the nested type's changes 😃
Author:carlossanlop
Assignees:carlossanlop
Labels:

area-System.IO

Milestone:6.0.0

@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

Tagging subscribers to this area: @carlossanlop

Yes, thank you bot... 😆

@jozkeejozkee 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.

Otherwise; LGTM, will approve if/when comments are resolved, most of them are nits and/or questions.

Comment on lines +17 to +22
internal const long NoResult = 0;
internal const long ResultSuccess = (long)1 << 32;
internal const long ResultError = (long)2 << 32;
internal const long RegisteringCancellation = (long)4 << 32;
internal const long CompletedCallback = (long)8 << 32;
internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32;

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.

Consider prefixing this constants with AsyncCompletionSource_ or something alike since these names are too ambiguous or keep them in their respective classes regardless of them being duplicated.

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.

or put them into a nested static class called ReturnCodes:

internalstaticpartialclassFileStreamHelpers{internalstaticclassReturnCodes{internalconstlongNoResult=0;internalconstlongResultSuccess=(long)1<<32;internalconstlongResultError=(long)2<<32;internalconstlongRegisteringCancellation=(long)4<<32;internalconstlongCompletedCallback=(long)8<<32;internalconstulongResultMask=((ulong)uint.MaxValue)<<32;}

but it's a nit (not a blocker)

Comment on lines +66 to +67
_strategy.CompareExchangeCurrentOverlappedOwner(this, null) == null ?
_strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!) : // allocated when buffer was created, and buffer is non-null

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.

Suggested change
_strategy.CompareExchangeCurrentOverlappedOwner(this,null)==null?
_strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!):// allocated when buffer was created, and buffer is non-null
strategy.CompareExchangeCurrentOverlappedOwner(this,null)==null?
strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!):// allocated when buffer was created, and buffer is non-null

// be directly the AwaitableProvider that's completing (in the case where the preallocated
// overlapped was already in use by another operation).
object? state = ThreadPoolBoundHandle.GetNativeOverlappedState(pOverlapped);
Debug.Assert(state is (AsyncWindowsFileStreamStrategy or FileStreamValueTaskSource));

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.

Suggested change
Debug.Assert(stateis(AsyncWindowsFileStreamStrategy or FileStreamValueTaskSource));
Debug.Assert(stateisAsyncWindowsFileStreamStrategy or FileStreamValueTaskSource);

int errorCode = unchecked((int)(packedResult & uint.MaxValue));
if (errorCode == Interop.Errors.ERROR_OPERATION_ABORTED)
{
_source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(new OperationCanceledException(cancellationToken.IsCancellationRequested ? cancellationToken : new CancellationToken(canceled: true))));

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.

Suggested change
_source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(newOperationCanceledException(cancellationToken.IsCancellationRequested?cancellationToken:newCancellationToken(canceled:true))));
Exceptione=newOperationCanceledException(cancellationToken.IsCancellationRequested?cancellationToken:newCancellationToken(canceled:true));
e.SetCurrentStackTrace();
_source.SetException(e);

}

private void Cancel(CancellationToken token)
{

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.

Missing comment and asserts:

// WARNING: This may potentially be called under a lock (during cancellation registration)
Debug.Assert(stateisFileStreamCompletionSource,"Unknown state passed to cancellation");
FileStreamCompletionSourcecompletionSource=(FileStreamCompletionSource)state;
Debug.Assert(completionSource._overlapped!=null&&!completionSource.Task.IsCompleted,"IO should not have completed yet");

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The first assert is not needed because the method argument is not an object? state.

I'll add the assert that checks for overlapped an completed status.

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.

The state param could be added to this method and allows you to conserve the first Debug.Assert.

long packedResult = Interlocked.CompareExchange(ref _result, FileStreamHelpers.RegisteringCancellation, FileStreamHelpers.NoResult);
if (packedResult == FileStreamHelpers.NoResult)
{
_cancellationRegistration = cancellationToken.UnsafeRegister(static (s, token) => ((FileStreamValueTaskSource)s!).Cancel(token), this);

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.

Could we pass the callback on a similar fashion to FileStreamCompletionSource

_cancellationRegistration=cancellationToken.UnsafeRegister(cancelCallback,this);

Or why did you opted for this way instead?

@carlossanlopcarlossanlopApr 8, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It made more sense to pass this as the second argument of UnsafeRegister, which ensures it's a ValueTaskSource instance, and saves us from the check of the object? state parameter like in the old code. The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

We have a similar example in System.Threading.Channels.

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.

It made more sense to pass this as the second argument of UnsafeRegister

FileStreamCompletionSource was also passing this as 2nd argument.

The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

Yes, seems that it was caching the method group to avoid allocating everytime it was passed to UnsafeRegister.
However I think you can avoid the allocation without caching yourself by doing this:

_cancellationRegistration=cancellationToken.UnsafeRegister((s,token)=>Cancel(s,token),this);

Just a suggestion, not something that you should block on.

@davidfowl

Copy link
Copy Markdown
Member

This is the change I was waiting for 👏🏾👏🏾

@davidfowl

Copy link
Copy Markdown
Member

I haven't looked but I assume we'll reuse the operation objects if there's no concurrency? This is what sockets do today so in the 95% case of no overlapping writes or reads, there'll be a single reusable allocation on the FileStream

@stephentoub

Copy link
Copy Markdown
Member

This is the change I was waiting for

It's not, actually. You want the next one where the IValueTaskSource instance is cached.

}
// else: Some other thread stole the result, so now it is responsible to finish the callback
}
// else: Some other thread is registering a cancellation, so it *must* finish the callback

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.

All this logic looks more complicated than it needs to be. I realize you're just following the same flow as what was already there, so nothing needs to be changed in this PR. But we should revisit all of this synchronization later; I suspect we can do it more succinctly and efficiently.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I opened an issue to follow up on this and on the caching of the IValueTaskSource.
#50972

@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 before we merge I would like to see the following benchmark results:

.NET 6 with new strategy enabled: before and after your change. (not .NET 5 vs 6 as there are too many moving targets)

You should be able to achieve that by running the following command in the perf repo;

git clone https://github.com/dotnet/performance.git
cd performance\src\benchmarks\micro
dotnet run -c Release -f net6.0 --filter *Perf_FileStream* --coreRun $pathToCoreRunWithoutYourChanges $pathToCoreRunWithYourChanges --envVars "DOTNET_SYSTEM_IO_USENET5COMPATFILESTREAM:0"

/// <see cref="MemoryHandle"/> when the operation has completed. This should only be used
/// when memory doesn't wrap a byte[].
/// </summary>
private sealed class MemoryAwaitableProvider : FileStreamValueTaskSource

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.

This should only be used when memory doesn't wrap a byte[].

I just realized that we don't have benchmarks for this scenario as Memory used in our micro-benchmarks always wraps a byte array. Could you please add such benchmarks to https://github.com/dotnet/performance/blob/main/src/benchmarks/micro/libraries/System.IO.FileSystem/Perf.FileStream.cs ? One test case should be enough as this should be rare.

@carlossanlopcarlossanlopApr 7, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Some of the benchmarks do consume this type. If you look at some of the profiling tests I executed, you'll see that MemoryValueTaskSource shows up a few times. The profiling tests reuse the code we had in our benchmarks.

I don't mind adding an explicit one though. Dou you want me to add one anyway?

Comment on lines +17 to +22
internal const long NoResult = 0;
internal const long ResultSuccess = (long)1 << 32;
internal const long ResultError = (long)2 << 32;
internal const long RegisteringCancellation = (long)4 << 32;
internal const long CompletedCallback = (long)8 << 32;
internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32;

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.

or put them into a nested static class called ReturnCodes:

internalstaticpartialclassFileStreamHelpers{internalstaticclassReturnCodes{internalconstlongNoResult=0;internalconstlongResultSuccess=(long)1<<32;internalconstlongResultError=(long)2<<32;internalconstlongRegisteringCancellation=(long)4<<32;internalconstlongCompletedCallback=(long)8<<32;internalconstulongResultMask=((ulong)uint.MaxValue)<<32;}

but it's a nit (not a blocker)

Comment on lines 62 to 63

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.

Did you run the perf. benchmarks after doing this change and did you notice any impact?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Pending.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Confirmed - There is a slight improvement.

I made sure to compare apples to apples: I used my latest commit, and built it setting RunContinuationsAsynchronously to true, then to false. I compared it both times against our code in .NET 5.0.

The improvement is clear when setting it to false (or not setting it at all).

RunContinuationsAsynchronously = true

❯ dotnet run -c release --base "D:\fs50" --diff "D:\fs60_NEW_ON_IVTS_asynctrue" --threshold 0.0001%
summary:
better: 24, geomean: 3.402
worse: 19, geomean: 2.244
total diff: 43

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)5.49882252.574840275.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)5.121003671.885136350.00bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)4.44257218.231141827.01
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)4.08286859.381171600.96
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.251795459.385832233.33
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)3.111844293.365741847.92several?
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.111868294.535807825.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)3.082209384.386796443.75
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)2.46557062.051371566.15
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)2.33478821.501116033.48
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)2.28543512.611239956.25bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.613479778.755612725.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.448197915.6311814575.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.33168291900.00224355000.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.335313143.757063218.75
Perf_FileStream.Flush(fileSize: 1024, options: None)1.253448111.254313832.81
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1250055150.0056309025.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1256033900.0062803900.00
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.12133329050.00148833000.00bimodal
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)117.075489541.6746889.31
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)65.932811281.2542642.74
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.842571736.4644462.45
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.09876923.2641586.04
Perf_FileStream.OpenClose(fileSize: 1024, options: None)5.00152781.6630548.62
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.76153731.8132314.04
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)4.032463834750.00611183400.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.93165696.6842208.34
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.52200423.3656865.52
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.62230670.3887990.73
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.58238920.2292478.58
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.343222202.501377939.84
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.87156593600.0083681962.50
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.6619598723.0811803641.67
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.6322492768.7513818900.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.577025697.924487121.88
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)1.47686776300.00466936500.00bimodal
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.33987460.55743825.45
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.24975146.09784156.88
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.195221529.174387085.94
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.181301925.001103803.79
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.04116486775.00112407200.00
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.0321953590.9121241122.73
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0188348175.0087467100.00
RunContinuationsAsynchronously = false

dotnet run -c release --base "D:\fs50" --diff "D:\fs60_NEW_ON_IVTS_asyncfalse" --threshold 0.0001%
summary:
better: 23, geomean: 3.592
worse: 20, geomean: 2.135
total diff: 43

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)5.36882252.574730875.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)5.051003671.885071912.50bimodal
Perf_FileStream.WriteByte(fileSize: 1024, options: None)4.25257218.231094164.38several?
Perf_FileStream.WriteByte(fileSize: 1024, options: Asynchronous)4.05286859.381161160.27
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 4096, options: None)3.231795459.385791481.25
Perf_FileStream.Write(fileSize: 1048576, userBufferSize: 512, options: None)3.101844293.365709820.83several?
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: None)3.092209384.386836504.17
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)3.071868294.535733883.33
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: Asynchronous)2.46557062.051368034.90
Perf_FileStream.CopyToFile(fileSize: 1024, options: None)2.31478821.501103696.25
Perf_FileStream.CopyToFileAsync(fileSize: 1024, options: None)2.24543512.611218234.38bimodal
Perf_FileStream.FlushAsync(fileSize: 1024, options: None)1.603479778.755572682.29
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.428197915.6311634850.00
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.33168291900.00223141800.00bimodal
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.315313143.756949831.25
Perf_FileStream.Flush(fileSize: 1024, options: None)1.263448111.254332915.63
Perf_FileStream.Write(fileSize: 104857600, userBufferSize: 4096, options: None)1.13133329050.00150223800.00bimodal
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.1150055150.0055699050.00
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: None)1.1056033900.0061710550.00
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0088348175.0088759625.00
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.SeekBackward(fileSize: 1024, options: Asynchronous)116.975489541.6746929.42
Perf_FileStream.SeekForward(fileSize: 1024, options: Asynchronous)65.762811281.2542753.29
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)57.562571736.4644676.15
Perf_FileStream.SeekForward(fileSize: 1024, options: None)21.52876923.2640756.47
Perf_FileStream.OpenClose(fileSize: 1024, options: None)5.06152781.6630216.63
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)4.81153731.8131975.57
Perf_FileStream.WriteAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)4.052463834750.00608864800.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)3.99165696.6841526.73
Perf_FileStream.ReadByte(fileSize: 1024, options: Asynchronous)3.54200423.3656610.08
Perf_FileStream.LockUnlock(fileSize: 1024, options: None)2.62230670.3887877.10
Perf_FileStream.LockUnlock(fileSize: 1024, options: Asynchronous)2.59238920.2292291.75
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)2.403222202.501345116.25
Perf_FileStream.CopyToFileAsync(fileSize: 104857600, options: Asynchronous)1.86156593600.0083969875.00
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.6419598723.0811925173.68
Perf_FileStream.Flush(fileSize: 1024, options: Asynchronous)1.6222492768.7513859764.71
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: Asynchronous)1.567025697.924493881.25
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: Asynchronous)1.46686776300.00470158300.00bimodal
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.32987460.55750294.64
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 512, options: None)1.24975146.09784056.56
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 4096, options: None)1.171301925.001108880.36
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.175221529.174470673.44
Perf_FileStream.FlushAsync(fileSize: 1024, options: Asynchronous)1.0721953590.9120580725.00
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.01116486775.00

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Here is the result comparison if we compare the benchmark results of setting it to true vs false:

base: RunContinuationsAsynchronously=true, diff: RunContinuationsAsynchronously=false

dotnet run -c release --base "D:\fs60_NEW_ON_IVTS_asynctrue" --diff "D:\fs60_NEW_ON_IVTS_asyncfalse" --threshold 0.0001%
summary:
better: 14, geomean: 1.018
worse: 11, geomean: 1.017
total diff: 25

Slowerdiff/baseBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.WriteAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0567143550.0070259187.50
Perf_FileStream.ReadAsync(fileSize: 104857600, userBufferSize: 4096, options: None)1.02112407200.00115107550.00
Perf_FileStream.Read_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.02240016.67244921.93
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.024387085.944470673.44
Perf_FileStream.Read(fileSize: 104857600, userBufferSize: 4096, options: None)1.0187467100.0088759625.00
Perf_FileStream.WriteAsync_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.0121684354.5521982809.09
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: Asynchronous)1.011236709.901253242.19
Perf_FileStream.Read(fileSize: 1024, userBufferSize: 1024, options: None)1.0139289.5439730.73
Perf_FileStream.Write_NoBuffering(fileSize: 1048576, userBufferSize: 16384, options: None)1.0121445427.2721646650.00
Perf_FileStream.Read(fileSize: 1048576, userBufferSize: 4096, options: None)1.01743825.45750294.64
Perf_FileStream.SeekBackward(fileSize: 1024, options: None)1.0044462.4544676.15
Fasterbase/diffBase Median (ns)Diff Median (ns)Modality
Perf_FileStream.WriteByte(fileSize: 1024, options: None)1.041141827.011094164.38several?
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0345688612.5044474575.00
Perf_FileStream.ReadAsync(fileSize: 1048576, userBufferSize: 512, options: None)1.021377939.841345116.25
Perf_FileStream.CopyToFile(fileSize: 1048576, options: None)1.024840275.004730875.00
Perf_FileStream.SeekForward(fileSize: 1024, options: None)1.0241586.0440756.47
Perf_FileStream.ReadAsync_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: Asynchronous)1.02130849050.00128463775.00
Perf_FileStream.ReadByte(fileSize: 1024, options: None)1.0242208.3441526.73
Perf_FileStream.WriteAsync(fileSize: 1048576, userBufferSize: 512, options: Asynchronous)1.0211814575.0011634850.00
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: Asynchronous)1.015807825.005733883.33
Perf_FileStream.CopyToFileAsync(fileSize: 1048576, options: None)1.015136350.005071912.50
Perf_FileStream.CopyToFile(fileSize: 104857600, options: None)1.0156309025.0055699050.00
Perf_FileStream.Read_NoBuffering(fileSize: 104857600, userBufferSize: 16384, options: None)1.0133747814.293338345.14
Perf_FileStream.OpenClose(fileSize: 1024, options: Asynchronous)1.0132314.0431975.57
Perf_FileStream.ReadAsync(fileSize: 1024, userBufferSize: 1024, options: Asynchronous)1.0081988.0981641.78

long packedResult = Interlocked.CompareExchange(ref _result, FileStreamHelpers.RegisteringCancellation, FileStreamHelpers.NoResult);
if (packedResult == FileStreamHelpers.NoResult)
{
_cancellationRegistration = cancellationToken.UnsafeRegister(static (s, token) => ((FileStreamValueTaskSource)s!).Cancel(token), this);

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.

It made more sense to pass this as the second argument of UnsafeRegister

FileStreamCompletionSource was also passing this as 2nd argument.

The old code also allocates an Action<object?> instance to save a reference to the Cancel method.

Yes, seems that it was caching the method group to avoid allocating everytime it was passed to UnsafeRegister.
However I think you can avoid the allocation without caching yourself by doing this:

_cancellationRegistration=cancellationToken.UnsafeRegister((s,token)=>Cancel(s,token),this);

Just a suggestion, not something that you should block on.

Comment on lines 120 to 122

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.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I tried, but a ValueTask does not have a Result property, like a ValueTask<T> does, because it returns void. So I'm not sure if it's possible to apply an equivalent pattern.

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.

publicoverridevoidWrite(byte[]buffer,intoffset,intcount){ValueTaskvt=WriteAsyncInternal(newReadOnlyMemory<byte>(buffer,offset,count),CancellationToken.None);if(vt.IsCompleted){vt.GetAwaiter().GetResult();}else{vt.AsTask().GetAwaiter().GetResult();}}

That said, it's not much of an issue for this case. The read side is an issue because AsTask() will almost certainly need to allocate a new Task<int> in order to hand back the result, but for the write side, AsTask() on an already completed ValueTask will just return Task.Completed, assuming the operation completed successfully. So the only time the existing code for the write side will allocate is when the operation was canceled or faulted, in which case we're about to throw an exception anyway, and the extra task allocation won't really matter. Up to you whether you want to change it.

@jozkeejozkee 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.

LGTM; thanks Carlos!

@adamsitnik

Copy link
Copy Markdown
Member

I compared it both times against our code in .NET 5.0.

Comparison against 5.0 is a comparison of all of the changes that were merged to main since we have snapped 5.0 with your change against 5.0. What we care about is how your change (and nothing else) is affecting the perf, so we need to isolate the runs to only your change.

To be able to do that you need to run the benchmarks using two CoreRuns:

  • as a baseline the corerun that was built from main when you have created a new branch, without any of your changes
  • your branch with your changes

My personal workflow:

  1. When I create a new branch, I perform a full build in Release:
build -c Release -subset clr+libs
  1. I copy the folder that contains CoreRun.exe (runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0) to a separate folder. Typically I call it "before".
  2. I apply some changes and perform an incremental Release build. In case of System.Private.CoreLib.dll it's:
build -c Release -subset clr.corelib+clr.nativecorelib+libs.PreTest
  1. I run the benchmarks using the old and new CoreRun.exe:
dotnet run -c Release -f net6.0 --filter *ABC* --corerun C:\Projects\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\before\CoreRun.exe C:\Projects\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe

@carlossanlop

carlossanlop commented Apr 13, 2021

Copy link
Copy Markdown
ContributorAuthor
  • as a baseline the corerun that was built from main when you have created a new branch, without any of your changes
  • your branch with your changes

@adamsitnik@jozkee here's the table comparing the before (original) and after of this PR. Most results had allocation improvements, but worse speed when FileOptions = Asynchronous:

Results

Command:

 dotnet run
-c Release
-f net6.0
--corerun
D:\runtime2\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe
D:\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe
--filter System.IO.Tests.Perf_FileStream.*Async*
--statisticalTest 3ms
--minIterationCount 100
--maxIterationCount 101
--artifacts "D:\finalresults"

Note: The runtime2 corerun contains the baseline commit before my changes, the runtime corerun contains this PR's changes.

MethodToolchainfileSizeuserBufferSizeoptionsMeanErrorStdDevMedianMinMaxRatioAllocated
ReadAsyncBEFORE10241024None78.69 μs0.255 μs0.736 μs78.53 μs77.61 μs80.66 μs1.005 KB
ReadAsyncAFTER10241024None79.10 μs0.237 μs0.673 μs79.08 μs76.96 μs80.71 μs1.015 KB
WriteAsyncBEFORE10241024None316.97 μs2.512 μs6.919 μs315.92 μs305.11 μs339.35 μs1.004 KB
WriteAsyncAFTER10241024None300.13 μs5.643 μs15.730 μs297.59 μs272.46 μs348.15 μs0.954 KB
ReadAsyncBEFORE10241024Asynchronous94.13 μs0.259 μs0.731 μs94.02 μs92.27 μs96.16 μs1.005 KB
ReadAsyncAFTER10241024Asynchronous85.48 μs0.842 μs2.291 μs85.11 μs82.58 μs92.09 μs0.915 KB
WriteAsyncBEFORE10241024Asynchronous292.63 μs5.659 μs15.869 μs290.59 μs264.25 μs338.98 μs1.005 KB
WriteAsyncAFTER10241024Asynchronous324.35 μs13.906 μs39.222 μs311.27 μs274.34 μs428.64 μs1.115 KB
FlushAsyncBEFORE1024?None4,606.81 μs46.736 μs133.341 μs4,633.92 μs4,374.04 μs4,952.63 μs1.00269 KB
FlushAsyncAFTER1024?None4,587.88 μs58.046 μs168.403 μs4,638.98 μs4,322.79 μs5,034.48 μs1.00269 KB
CopyToFileAsyncBEFORE1024?None474.57 μs5.497 μs15.141 μs474.66 μs440.31 μs520.31 μs1.005 KB
CopyToFileAsyncAFTER1024?None488.94 μs3.631 μs10.061 μs488.62 μs469.93 μs518.76 μs1.035 KB
FlushAsyncBEFORE1024?Asynchronous14,680.35 μs150.967 μs437.982 μs14,637.76 μs13,623.50 μs15,645.01 μs1.00301 KB
FlushAsyncAFTER1024?Asynchronous20,333.22 μs116.277 μs339.185 μs20,299.93 μs19,724.25 μs21,148.73 μs1.39261 KB
CopyToFileAsyncBEFORE1024?Asynchronous520.97 μs5.982 μs16.674 μs519.35 μs490.05 μs572.29 μs1.006 KB
CopyToFileAsyncAFTER1024?Asynchronous523.30 μs6.294 μs17.650 μs520.91 μs479.14 μs583.85 μs1.006 KB
ReadAsyncBEFORE1048576512None1,375.28 μs5.930 μs17.015 μs1,376.60 μs1,340.44 μs1,413.73 μs1.0085 KB
ReadAsyncAFTER1048576512None1,347.16 μs5.128 μs14.958 μs1,345.14 μs1,323.57 μs1,382.14 μs0.9885 KB
WriteAsyncBEFORE1048576512None3,530.03 μs38.149 μs108.222 μs3,521.78 μs3,299.91 μs3,823.63 μs1.0076 KB
WriteAsyncAFTER1048576512None3,473.85 μs47.943 μs134.438 μs3,469.00 μs3,222.90 μs3,832.17 μs0.9876 KB
ReadAsyncBEFORE1048576512Asynchronous3,278.98 μs30.616 μs87.844 μs3,257.98 μs3,151.46 μs3,507.69 μs1.0093 KB
ReadAsyncAFTER1048576512Asynchronous4,399.34 μs16.153 μs47.119 μs4,401.64 μs4,294.71 μs4,531.75 μs1.3483 KB
WriteAsyncBEFORE1048576512Asynchronous5,304.10 μs73.935 μs214.500 μs5,309.27 μs4,771.82 μs5,852.43 μs1.0085 KB
WriteAsyncAFTER1048576512Asynchronous6,506.15 μs94.189 μs271.756 μs6,505.61 μs6,042.59 μs7,132.02 μs1.2375 KB
ReadAsyncBEFORE10485764096None1,090.98 μs6.156 μs18.152 μs1,096.14 μs1,039.20 μs1,120.19 μs1.0029 KB
ReadAsyncAFTER10485764096None1,084.03 μs8.601 μs25.225 μs1,086.13 μs1,042.07 μs1,131.85 μs0.9929 KB
WriteAsyncBEFORE10485764096None3,247.60 μs43.259 μs124.117 μs3,243.69 μs2,954.42 μs3,556.50 μs1.0055 KB
WriteAsyncAFTER10485764096None3,250.35 μs36.664 μs100.985 μs3,234.77 μs3,039.74 μs3,505.95 μs1.0055 KB
ReadAsyncBEFORE10485764096Asynchronous3,273.61 μs17.591 μs50.753 μs3,264.72 μs3,190.18 μs3,410.97 μs1.0079 KB
ReadAsyncAFTER10485764096Asynchronous4,547.38 μs73.438 μs216.535 μs4,398.34 μs4,327.10 μs5,035.98 μs1.3969 KB
WriteAsyncBEFORE10485764096Asynchronous5,384.46 μs83.792 μs240.415 μs5,346.69 μs4,897.35 μs5,955.92 μs1.0084 KB
WriteAsyncAFTER10485764096Asynchronous6,845.63 μs88.794 μs256.192 μs6,883.75 μs6,079.92 μs7,286.73 μs1.2774 KB
ReadAsync_NoBufferingBEFORE104857616384None406.34 μs2.413 μs7.113 μs403.28 μs387.16 μs419.08 μs1.007 KB
ReadAsync_NoBufferingAFTER104857616384None380.35 μs3.413 μs10.063 μs378.36 μs363.73 μs402.82 μs0.947 KB
WriteAsync_NoBufferingBEFORE104857616384None2,545.17 μs30.174 μs83.613 μs2,526.42 μs2,397.67 μs2,776.47 μs1.007 KB
WriteAsync_NoBufferingAFTER104857616384None2,537.77 μs27.489 μs76.628 μs2,528.02 μs2,421.65 μs2,750.68 μs1.007 KB
ReadAsync_NoBufferingBEFORE104857616384Asynchronous949.54 μs6.336 μs18.077 μs943.69 μs926.29 μs1,002.20 μs1.0020 KB
ReadAsync_NoBufferingAFTER104857616384Asynchronous1,195.21 μs3.070 μs8.507 μs1,194.94 μs1,179.45 μs1,218.92 μs1.2617 KB
WriteAsync_NoBufferingBEFORE104857616384Asynchronous2,965.91 μs40.616 μs114.558 μs2,952.10 μs2,677.93 μs3,244.55 μs1.0020 KB
WriteAsync_NoBufferingAFTER104857616384Asynchronous3,021.90 μs40.697 μs116.110 μs3,000.74 μs2,803.04 μs3,343.84 μs1.0217 KB
CopyToFileAsyncBEFORE1048576?None2,440.43 μs17.056 μs49.754 μs2,437.81 μs2,343.18 μs2,576.37 μs1.003 KB
CopyToFileAsyncAFTER1048576?None2,421.23 μs15.376 μs43.869 μs2,413.43 μs2,350.26 μs2,521.45 μs0.993 KB
CopyToFileAsyncBEFORE1048576?Asynchronous2,878.14 μs16.682 μs47.863 μs2,876.69 μs2,771.42 μs3,001.49 μs1.004 KB
CopyToFileAsyncAFTER1048576?Asynchronous2,769.09 μs15.649 μs43.882 μs2,760.85 μs2,691.56 μs2,885.67 μs0.964 KB
ReadAsyncBEFORE1048576004096None115,657.20 μs352.576 μs1,039.577 μs115,639.45 μs113,670.15 μs117,832.15 μs1.002,801 KB
ReadAsyncAFTER1048576004096None116,612.88 μs249.577 μs731.966 μs116,751.05 μs114,615.00 μs117,903.05 μs1.012,801 KB
WriteAsyncBEFORE1048576004096None205,378.59 μs7,785.258 μs22,462.248 μs194,584.50 μs184,125.30 μs268,874.60 μs1.005,005 KB
WriteAsyncAFTER1048576004096None209,453.56 μs10,355.532 μs30,043.276 μs194,064.20 μs173,778.40 μs293,593.30 μs1.035,005 KB
ReadAsyncBEFORE1048576004096Asynchronous327,225.97 μs1,532.011 μs4,346.059 μs326,564.00 μs320,187.10 μs339,508.20 μs1.007,801 KB
ReadAsyncAFTER1048576004096Asynchronous457,654.89 μs902.286 μs2,559.634 μs457,207.00 μs451,652.60 μs465,093.00 μs1.406,801 KB
WriteAsyncBEFORE1048576004096Asynchronous454,994.00 μs7,661.181 μs21,857.780 μs449,253.50 μs421,512.80 μs511,199.10 μs1.007,905 KB
WriteAsyncAFTER1048576004096Asynchronous600,076.62 μs10,088.501 μs29,268.570 μs592,035.20 μs559,265.50 μs677,403.80 μs1.326,905 KB
ReadAsync_NoBufferingBEFORE10485760016384None45,086.23 μs156.169 μs453.074 μs45,005.65 μs43,497.93 μs46,041.93 μs1.00701 KB
ReadAsync_NoBufferingAFTER10485760016384None42,133.53 μs174.922 μs457.740 μs42,093.18 μs41,187.75 μs43,731.75 μs0.93701 KB
WriteAsync_NoBufferingBEFORE10485760016384None61,814.53 μs685.277 μs1,887.453 μs61,419.69 μs59,079.38 μs68,035.43 μs1.00701 KB
WriteAsync_NoBufferingAFTER10485760016384None61,435.05 μs720.962 μs1,973.622 μs61,127.18 μs58,506.18 μs67,827.25 μs0.99701 KB
ReadAsync_NoBufferingBEFORE10485760016384Asynchronous97,220.19 μs667.147 μs1,881.701 μs96,464.10 μs94,386.65 μs102,972.80 μs1.001,951 KB
ReadAsync_NoBufferingAFTER10485760016384Asynchronous128,169.40 μs556.512 μs1,587.760 μs127,771.80 μs125,455.30 μs133,061.15 μs1.321,701 KB
WriteAsync_NoBufferingBEFORE10485760016384Asynchronous136,593.81 μs1,249.681 μs3,462.860 μs136,822.00 μs127,534.30 μs146,281.60 μs1.001,951 KB
WriteAsync_NoBufferingAFTER10485760016384Asynchronous169,656.11 μs2,245.877 μs6,515.696 μs168,284.30 μs157,404.20 μs189,342.10 μs1.251,701 KB
CopyToFileAsyncBEFORE104857600?None56,537.38 μs245.101 μs695.312 μs56,440.93 μs55,567.65 μs58,581.80 μs1.00177 KB
CopyToFileAsyncAFTER104857600?None56,184.32 μs275.348 μs785.585 μs56,116.43 μs54,790.00 μs58,574.72 μs0.99177 KB
CopyToFileAsyncBEFORE104857600?Asynchronous87,668.39 μs312.004 μs905.181 μs87,555.38 μs85,324.65 μs89,838.85 μs1.00246 KB
CopyToFileAsyncAFTER104857600?Asynchronous78,490.65 μs501.805 μs1,431.678 μs78,350.84 μs75,379.32 μs82,799.10 μs0.90214 KB

…Source only needed by Net5CompatFileStreamStrategy.
… one allocation and get the actual profiling improvement.
Rename FileStreamAwaitableProvider.
… review. Shorter filename for awaitable provider.
…trategy, and its _fileHandle can now be private.
@adamsitnik

Copy link
Copy Markdown
Member

Most results had allocation improvements, but worse speed when FileOptions = Asynchronous:

As discussed offline, such a regression needs to be solved before merging the PR.

I've even built your fork and run the benchmarks myself to be 100% sure, but unfortunately, I've confirmed the regression:

MethodToolchainfileSizeuserBufferSizeoptionsMeanRatioAllocated
ReadAsync_NoBuffering\after\corerun.exe104857616384Asynchronous838.1 us1.1217 KB
ReadAsync_NoBuffering\before\corerun.exe104857616384Asynchronous748.1 us1.0020 KB
ReadAsync_NoBuffering\after\corerun.exe10485760016384Asynchronous93,214.6 us1.121,700 KB
ReadAsync_NoBuffering\before\corerun.exe10485760016384Asynchronous83,445.4 us1.001,950 KB

If I can suggest something, ReadAsync_NoBuffering with fileSize=1048576 and options=Asynchronous should be the best benchmark for creating a repo app and profiling it. It's the simplest benchmark (buffering is not involved) and ReadAsync has a smaller deviation than WriteAsync. If you fail to find the regression with VS Profiler, you can use Perfview.

@adamsitnikadamsitnik added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Apr 14, 2021
@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

I compared the CPU usage before and after, and discovered we spend a considerable amount of time inside Overlapped.PerformIOCompletionCallback.

The new code (left) consumes 62% of the time calling ExecutionContext.RunInternal, while the old code (right) only took 3.82%.

Screenshot

image

The root cause seems to be this line, where we call _source.SetResult:

Screenshot

image

Which is adding up the time it takes to run the rest of the ReadAsync calls in the loop, including the disposal of the FileStream object in the main method.

There's a comment stating that ExecutionContext.RunInternal (part of the callstack) warning that the code is a very hot path:

Screenshot

image

I wasn't expecting all the continuation code to be counted as part of the asynchronous calls. I wonder if this has anything to do with the removal of RunContinuationsAsynchronously = true flag. I'll bring that flag back and compare the results.

cc @jozkee@adamsitnik@stephentoub

@davidfowl

Copy link
Copy Markdown
Member

We don't need to restore the ExecutionContext in the overlapped, that restore should be avoided in these APIs. The async state machine already does it. This is what you need #42549 😄

@stephentoub

stephentoub commented Apr 14, 2021

Copy link
Copy Markdown
Member

The new code (left) consumes 62% of the time calling ExecutionContext.RunInternal, while the old code (right) only took 3.82%.

That's inclusive (what "Total" means in the VS view). The callback is actually running the continuation now (because of RunContinuationsAsynchronously=false), rather than queueing it to be run on a different thread, so the 62% is inclusive of the actual app code.

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 know you're copying this logic from what was there before, but it seems to be unnecessarily complex, with lots of interlocked operations for transitioning from one state to another to another. If we're looking for places to reduce overheads, revisiting this whole implementation is likely a good place to start.

@adamsitnik

Copy link
Copy Markdown
Member

The callback is actually running the continuation now (because of RunContinuationsAsynchronously=false)

I've run all the async benchmarks for 3 configurations:

  • "before" (@carlossanlop fork main branch)
  • "false" (the current state of this PR as RunContinuationsAsynchronously is not set so it's false)
  • "true" ( _source.RunContinuationsAsynchronously = true; in ctor)

With the following settings:

  • 100 instead of 20 iterations (--minIterationCount 100 --maxIterationCount 101)
  • no outliers removal (--outliers DontRemove)

and confirmed that with _source.RunContinuationsAsynchronously = true; we have no regression in the microbenchmarks

@stephentoub should we:

  • set RunContinuationsAsynchronously = true; and include this PR in preview4
  • get some "real world" numbers from ASP.NET benchmarks (if there are none, write some and contribute to https://github.com/aspnet/benchmarks) and see how RunContinuationsAsynchronously setting affects non-micro benchmarks. This means preview5 for this PR.
Details
MethodToolchainfileSizeuserBufferSizeoptionsMeanStdDevMedianMinMaxRatioAllocated
ReadAsync\before\corerun.exe10241024Asynchronous82.69 us1.946 us82.22 us81.60 us94.90 us1.005 KB
ReadAsync\false\corerun.exe10241024Asynchronous78.76 us0.718 us78.56 us78.04 us82.13 us0.955 KB
ReadAsync\true\corerun.exe10241024Asynchronous83.28 us0.825 us83.12 us82.48 us89.82 us1.015 KB
WriteAsync\before\corerun.exe10241024Asynchronous404.05 us22.574 us400.81 us379.20 us494.53 us1.005 KB
WriteAsync\false\corerun.exe10241024Asynchronous400.76 us17.211 us397.80 us380.07 us472.21 us0.995 KB
WriteAsync\true\corerun.exe10241024Asynchronous405.20 us25.469 us401.47 us383.59 us510.74 us1.015 KB
ReadAsync\before\corerun.exe1048576512Asynchronous2,418.83 us55.684 us2,413.59 us2,361.95 us2,804.46 us1.0093 KB
ReadAsync\false\corerun.exe1048576512Asynchronous2,648.89 us73.366 us2,626.45 us2,550.29 us2,959.92 us1.1083 KB
ReadAsync\true\corerun.exe1048576512Asynchronous2,386.25 us9.985 us2,386.16 us2,356.79 us2,419.58 us0.9983 KB
WriteAsync\before\corerun.exe1048576512Asynchronous4,254.70 us277.778 us4,213.63 us3,967.55 us6,203.71 us1.0085 KB
WriteAsync\false\corerun.exe1048576512Asynchronous4,583.08 us183.949 us4,559.42 us4,317.29 us5,334.19 us1.0875 KB
WriteAsync\true\corerun.exe1048576512Asynchronous4,207.77 us179.340 us4,173.53 us3,896.65 us4,865.02 us0.9975 KB
ReadAsync\before\corerun.exe10485764096Asynchronous2,343.06 us35.339 us2,355.62 us2,225.73 us2,379.72 us1.0079 KB
ReadAsync\false\corerun.exe10485764096Asynchronous2,603.91 us40.916 us2,603.11 us2,513.55 us2,715.90 us1.1169 KB
ReadAsync\true\corerun.exe10485764096Asynchronous2,324.68 us38.225 us2,337.29 us2,226.14 us2,374.04 us0.9969 KB
WriteAsync\before\corerun.exe10485764096Asynchronous4,110.48 us265.902 us4,050.53 us3,844.25 us6,007.74 us1.0084 KB
WriteAsync\false\corerun.exe10485764096Asynchronous4,572.16 us320.658 us4,542.09 us4,256.30 us7,440.25 us1.1274 KB
WriteAsync\true\corerun.exe10485764096Asynchronous4,048.01 us184.553 us4,015.01 us3,739.28 us4,754.61 us0.9974 KB
ReadAsync_NoBuffering\before\corerun.exe104857616384Asynchronous747.53 us5.076 us747.87 us729.80 us755.69 us1.0020 KB
ReadAsync_NoBuffering\false\corerun.exe104857616384Asynchronous822.87 us6.877 us822.90 us809.94 us838.18 us1.1017 KB
ReadAsync_NoBuffering\true\corerun.exe104857616384Asynchronous726.82 us11.262 us726.44 us699.96 us754.36 us0.9717 KB
WriteAsync_NoBuffering\before\corerun.exe104857616384Asynchronous2,726.54 us136.755 us2,712.48 us2,534.40 us3,234.12 us1.0020 KB
WriteAsync_NoBuffering\false\corerun.exe104857616384Asynchronous2,761.05 us146.361 us2,792.11 us2,507.70 us3,188.16 us1.0117 KB
WriteAsync_NoBuffering\true\corerun.exe104857616384Asynchronous2,736.46 us194.495 us2,705.98 us2,415.11 us4,022.13 us1.0117 KB
ReadAsync\before\corerun.exe1048576004096Asynchronous250,490.85 us2,604.707 us250,256.45 us244,814.30 us256,607.90 us1.007,801 KB
ReadAsync\false\corerun.exe1048576004096Asynchronous278,260.89 us4,931.050 us278,009.45 us269,882.60 us297,338.40 us1.116,803 KB
ReadAsync\true\corerun.exe1048576004096Asynchronous252,682.05 us19,763.442 us250,702.50 us243,911.20 us447,534.10 us1.016,801 KB
WriteAsync\before\corerun.exe1048576004096Asynchronous370,893.59 us6,590.799 us369,781.05 us352,834.20 us390,366.40 us1.007,905 KB
WriteAsync\false\corerun.exe1048576004096Asynchronous397,619.03 us8,020.098 us397,052.95 us384,609.00 us446,559.70 us1.076,905 KB
WriteAsync\true\corerun.exe1048576004096Asynchronous358,128.05 us6,536.171 us357,755.45 us346,707.00 us382,735.70 us0.976,905 KB

@adamsitnikadamsitnik removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Apr 15, 2021

@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.

LGTM, thank you @carlossanlop !

@carlossanlop

Copy link
Copy Markdown
ContributorAuthor

With the unit tests passing, the micro benchmarks having no regressions after reverting RunContinuationsAsynchronously to true, and having allocation improvements, I feel confident in merging this change to include it in Preview4, and will continue working on #50972 to add caching and improve this code for Preview5.

@carlossanlop
carlossanlop merged commit a38d0c2 into dotnet:mainApr 15, 2021
@carlossanlop
carlossanlop deleted the IValueTaskSource branch April 15, 2021 22:51
@ghostghost locked as resolved and limited conversation to collaborators May 15, 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.

5 participants

@carlossanlop@davidfowl@stephentoub@adamsitnik@jozkee