-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Reduce allocations in BinaryReader
.
#80331
Conversation
Tagging subscribers to this area: @dotnet/area-system-io Issue DetailsWhen a In the former case this PR uses stack-allocated memory instead, and in the latter case the byte array is allocated lazily. I also improved stuff like a byte array slicing to use spans.
|
src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs
Outdated
Show resolved
Hide resolved
dbe91a9
to
cde6822
Compare
if (n == 0) | ||
{ | ||
ThrowHelper.ThrowEndOfFileException(); | ||
} | ||
|
||
charsRead = _decoder.GetChars(_charBytes, 0, n, _charBuffer, 0); | ||
charsRead = _decoder.GetChars(charBytes[..n], _charBuffer, flush: 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.
The use of a stateful class like Decoder
in this method does not seem right to me. Isn't each invocation of ReadString
standalone? Shouldn't we Reset
it on each non-text operation?
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.
From the docs:
Reads a string from the current stream. The string is prefixed with the length, encoded as an integer seven bits at a time.
My understanding is that in this particular case the string must be prefixed with the length, so the job of the method is to read the bytes from the stream (and possibly modify it’s state like Position) and covert them into string.
The method does not read all bytes at a time (readLength
can be different than stringLength
) so the encoder should maintain it's state between the loop iterations:
stringLength = Read7BitEncodedInt(); |
runtime/src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs
Lines 289 to 291 in be84df7
readLength = ((stringLength - currPos) > MaxCharBytesSize) ? MaxCharBytesSize : (stringLength - currPos); | |
n = _stream.Read(_charBytes, 0, readLength); |
Now should it reset the state in the final iteration?
Checking for the leftovers and throwing would be considered a breaking change. Just resetting the state would be considered a breaking change as well, because someone in theory could rely on keeping this state.
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.
Shouldn't we
Reset
it on each non-text operation?
I mean, if we call Read()
, it will return the next character, and if we call ReadUInt32(); Read()
after that, the decoder's state will be out-of-sync from the stream.
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 mean, if we call Read(), it will return the next character, and if we call ReadUInt32(); Read() after that, the decoder's state will be out-of-sync from the stream.
Could you please add a unit test that exhibits the described behavior?
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 can't find an easy way to test it. Can we skip it for this PR?
BinaryReader
in most cases.BinaryReader
.
@teo-tsirpanis thank you for your contribution! Could you please provide benchmark results that show the gains from the proposed changes? Thanks! |
I will, let's address #80331 (comment) first. |
@teo-tsirpanis - It looks like you're blocked and awaiting guidance from @stephentoub or @adamsitnik on the question you posted here; is that the accurate status of this PR? |
Yes. And I can't work on it for the next couple of days. |
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.
thank you for your contribution @teo-tsirpanis ! I would be fine with the proposed changed if FillBuffer
was not part of the public API.
src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs
Outdated
Show resolved
Hide resolved
It now gets allocated only when calling the obscure `FillBuffer` method.
We checked it on the constructor and already had asserts right before the casts.
6aa529d
to
f7a2043
Compare
src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs
Outdated
Show resolved
Hide resolved
@teo-tsirpanis could you please share the benchmark results? |
I will do it in the weekend. When will the snap for .NET 9 happen? |
The snap where we could merge all changes already happened on July 17th. Until August 14th we can merge bug fixes, important features and important perf fixes, but we need a direct manager approval. After Aug 14th only impactful issues, with more complex approval process. |
805474b
to
6273201
Compare
Here they are:
BenchmarkDotNet=v0.13.2.2052-nightly, OS=Windows 10 (10.0.19045.3271)
Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK=8.0.100-preview.6.23330.14
[Host] : .NET 8.0.0 (8.0.23.32907), X64 RyuJIT AVX2
Job-WTHNYV : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-BHRFRA : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
PowerPlanMode=00000000-0000-0000-0000-000000000000 Arguments=/p:EnableUnsafeBinaryFormatterSerialization=true IterationTime=250.0000 ms
MaxIterationCount=20 MinIterationCount=15 WarmupCount=1
|
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.
Overall it looks good, I found only one minor thing (unused resource can be removed).
Thank you for your contribution @teo-tsirpanis !
src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs
Outdated
Show resolved
Hide resolved
Feedback addressed. |
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.
LGTM! Now I'll have to discuss with @jeffhandley whether we want to include this in .NET 8 or 9, will keep you updated.
Thank you again @teo-tsirpanis !
I'd prefer to let this wait until .NET 9. It's a change in low-level IO dealing with serialization, with a small bit of |
@adamsitnik can we merge it? |
Improvements on x64 dotnet/perf-autofiling-issues#23341 |
This PR optimizes the
BinaryReader
class to allocate its fields only if we call its text-related methods. Additionally_buffer
and_charBytes
fields were removed and replaced with stack allocations or array pool rents.