-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Before .NET 6, FileStream was using expensive syscall called SetFilePointer to synchronize it's private offset with Windows OS.
A blog post from Windows Performance Team from 2008 calls it an anachronism:
The old DOS SetFilePointer API is an anachronism. One should specify the file offset in the overlapped structure even for synchronous I/O. It should never be necessary to resort to the hack of having private file handles for each thread.
In order to improve the performance of async reads and writes and solve the following issues:
- Win32 FileStream will issue a seek on every ReadAsync call #16354 Win32 FileStream will issue a seek on every ReadAsync call
- FileStream.Windows useAsync WriteAsync calls blocking apis #25905 FileStream.Windows useAsync WriteAsync calls blocking apis
FileStream is no longer doing that (#49975) and the offset is just kept in memory. This has allowed for up to two times faster ReadAsync and up to five time faster WriteAsync! See #49975 for full details.
FileStream.Position always returns the current offset, but if the user obtains the file handle via call to FileStream.SafeFileHandle and asks the OS for the current file offset of the given handle by performing a syscall, the value will return 0.
[DllImport("kernel32.dll")]
private static extern bool SetFilePointerEx(SafeFileHandle hFile, long liDistanceToMove, out long lpNewFilePointer, uint dwMoveMethod);
byte[] bytes = new byte[10_000];
string path = Path.Combine(Path.GetTempPath(), Path.GetTempFileName());
using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize: 4096, useAsync: true))
{
SafeFileHandle handle = fs.SafeFileHandle;
await fs.WriteAsync(bytes, 0, bytes.Length);
Console.WriteLine(fs.Position);
if (SetFilePointerEx(handle, 0, out long currentOffset, 1 /* get current offset */))
{
Console.WriteLine(currentOffset);
}
}Pre-change behavior:
10000
10000
Post-change behavior:
10000
0
It works in the other direction as well: if the user obtains filehandle and performs a syscall that moves the file offset, the offset returned by FileStream.Position won't be changed.
To enable the .NET 5 behavior, users can specify an AppContext switch or an environment variable:
{
"configProperties": {
"System.IO.UseNet5CompatFileStream": true
}
}
set DOTNET_SYSTEM_IO_USENET5COMPATFILESTREAM=1@dotnet/compat @stephentoub @jozkee @carlossanlop @jeffhandley
FWIW I've ensured that this change is not breaking ASP.NET (dotnet/aspnetcore#31441), WinForms (dotnet/winforms#4756) and the SDK (dotnet/sdk#16684)