-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce FileStreamStrategy as a first step of FileStream rewrite #47128
Conversation
…to call, implement these methods
…nToken = default)
…ck, object? state)
…ack, object? state)
In this particular case it's not a CPU, but an IO benchmark so we have more moving targets: OS heuristics for data caching and the hardware itself. This introduces a lot of instability. It looks that the bigger the file size, the more unstable the When I set the number of iterations to 100, I get values from |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question (as Adam would famously say) for my own education:
If we want to preserve the old FileStream
behavior (guarded by a compat flag), then we would directly call the code in WindowsFileStreamStrategy
or UnixFileStreamStrategy
. And as we add more and more strategies, then we should always keep in mind to leave the original code unmodified. Is this correct?
src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategyBase.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs
Outdated
Show resolved
Hide resolved
I think we'd effectively move the existing logic into a: private sealed class LegacyStrategy : FileStreamStrategy
{
... // existing code goes here, just modifed to fit with the abstraction appropriately
} and then in the various constructors: public FileStream(...)
{
if (AppCompatSwitchSet)
{
_strategy = new LegacyStrategy(...);
}
else
{
_strategy = ... // the better thing
}
} |
done. I've given up on splitting the Unix and Windows implementations in this PR and decided to just introduce the concept of strategy and moving the existing code into @stephentoub @jozkee @carlossanlop PTAL, the PR is ready for review |
// for everything else we fall back to the actual strategy (like FileStream does) | ||
// | ||
// it's crucial to NOT use the "base" keyoword here! everything must be using _fileStream or _strategy | ||
internal sealed class DerivedFileStreamStrategy : FileStreamStrategy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think "Derived" is an odd name here, given that FileStreamStrategy is abstract and any implementation will necessarily be "derived". We often use the term "Delegating" in such cases, e.g. DelegatingHandler
, DelegatingStream
, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think "Derived" is an odd name here, given that FileStreamStrategy is abstract and any implementation will necessarily be "derived"
In this particular context, it meant a strategy for a type that derives from FileStream. Does it make more sense or would you like me to rename it to DelegatingFileStreamStrategy
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So "DerivedFileStreamFileStreamStrategy" :)
I don't have a strong opinion. If you want to stick with DerivedFileStreamStrategy, that's fine.
src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs
Outdated
Show resolved
Hide resolved
if (_strategy.IsClosed) | ||
throw Error.GetFileNotOpen(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be cheaper if this were in the _strategy.WriteAsync method itself? Seems like we could potentially avoid one additional virtual dispatch?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could, but here I did this on purpose to keep all the validation logic in FileStream
and keep all the strategies free of any such checks (mostly to avoid code duplication).
src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs
Outdated
Show resolved
Hide resolved
~LegacyFileStreamStrategy() | ||
{ | ||
// it looks like having this finalizer is mandatory, | ||
// as we can not guarantee that the Strategy won't be null in FileStream finalizer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could if we wanted to split the ctor from the initialization, e.g.
_strategy = new Legacy...();
_strategy.Initialize(argumentsPreviouslyPassedToCtor);
I don't know whether it's worth it.
Co-authored-by: Stephen Toub <stoub@microsoft.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise; LGTM, thanks.
src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Unix.cs
Outdated
Show resolved
Hide resolved
return SafeFileHandle.Open(path!, openFlags, (int)OpenPermissions); | ||
} | ||
|
||
internal static bool GetDefaultIsAsync(SafeFileHandle handle, bool defaultIsAsync) => handle.IsAsync ?? defaultIsAsync; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious; do you know when DefaultIsAsync
can be true? I couldn't find a declaration other than this one:
private const bool DefaultIsAsync = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, for some reason the const
is always false
. I wonder why someone has introduced such a const in the first place?
src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Unix.cs
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs
Show resolved
Hide resolved
|
||
internal override bool IsClosed => _fileHandle.IsClosed; | ||
|
||
private static bool IsIoRelatedException(Exception e) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be moved to the helper class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it OK if I do this in the next PR? In the next PR I would like to introduce two new strategies for Windows and there I will have to move all helper logic to helper type to reduce the code duplication as much as possible.
Co-authored-by: David Cantú <dacantu@microsoft.com>
@carlossanlop @jozkee @stephentoub big thanks for all the reviews! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for making this change!
As discussed offline, we would like to introduce a few dedicated FileStream implementations (imagine one for sync and one for async file operations on Windows for example).
Since this is more or less an implementation of the Strategy Pattern I've called the new abstraction a
FileStreamStrategy
. For now, there is just a single implementation, so I've named itFileStreamImpl
but in the future, you should expect nicer names that described the unique strategy (an example could beIoUringFileStrategy
).The goal of this PR was to check if it's possible to choose the strategy in
ctor
and what performance impact an introduction of a new abstraction layer would bring.It's is possible, mostly because of the fact that .NET was Windows-only from the beginning and if we want to have async file operations on Windows, we have to specify a special flag when opening the file. This has shaped
FileStream
ctor and luckily for us, this information is always present when we are creating a new instance ofFileStream
To measure the performance impact I've revisited the existing
FileStream
benchmarks and added missing ones (see dotnet/performance#1633 for details).The introduction of a new abstraction layer has a very small footprint. The differences are typically within the 1% range. Since these are IO benchmarks and they are not as stable as CPU benchmarks, I believe that the overhead is within the margin of error. The overhead is small because most of the time is spent in expensive syscalls and one extra virtual method call is not an issue (verified with profiler)
If we agree that this is the right direction, the next steps will be most probably:
If you are about to review this PR, PTAL at the commits as I've tried to keep them small, and reviewing each of them might be the easiest way to review this PR.Edit: I've looked at the diff and it's not as bad as I expected.In the details, you can find full benchmark results:
cc @jeffhandley