-
Notifications
You must be signed in to change notification settings - Fork 352
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
ODataUriSlim struct #2500
ODataUriSlim struct #2500
Conversation
/// <param name="odataUri">The URI to copy.</param> | ||
public ODataUriSlim(ODataUri odataUri) | ||
{ | ||
Debug.Assert(odataUri != null, "odataUri != null"); |
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.
Maybe throw an ArgumentNullException
?
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'm a bit conflicted since this adds a null check for something that is called often but should never be null unless we have an internal bug (not a user-input error).
5b7c3e3
to
be55b50
Compare
This PR has Quantification details
Why proper sizing of changes matters
Optimal pull request sizes drive a better predictable PR flow as they strike a
What can I do to optimize my changes
How to interpret the change counts in git diff output
Was this comment helpful? 👍 :ok_hand: :thumbsdown: (Email) |
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.
Issues
*This pull request fixes #2163
Description
This PR introduces a new internal struct called
ODataUriSlim
to be used byODataWriterCore
instead of cloningODataUri
each time it enters a new scope. Now the scope stores aODataUriSlim
struct instead of a reference to the clonedODataUri
.Making the type a struct avoids allocating instances to the heap. I have tried to keep the struct small by only storing properties that can be modified from one writer scope to another, namely the
SelectAndExpand
andPath
properties. The other properties are never changed by the writer, and therefore do not need to be cloned.To avoid excessive copies of the struct, I passed it by reference to all methods using the
in
keyword. Some copies were inevitable. For example, a copy will be made when assigning the struct value to a property of a class instance. Also, storing the struct in a class makes instance of that class larger because the memory of struct is embedded in the class, instead of a reference to a different memory location. Another case of inevitable copy is when retrieving the value from a property. You cannot pass property value by reference. For example, the following snippet will not compile:In such cases I ended doing something like the following:
For ease of use, I have added a reference to the
ODataUri
to theODataUriSlim
and made the properties ofODataUri
accessible throughODataUriSlim
. Technically, this makes the ODataUriSlim slightly bigger by adding a reference, but this might prevent bugs. If these properties were not accessible throughODataUriSlim
, then I would have had to passODataUri
andODataUriSlim
to some methods which need to access the other properties. Using bothODataUri
andODataUriSlim
can lead to bugs because someone can accidentally access theODataUri.Path
property, which would be different from theODataUriSlim.Path
property.I have had to refactor a few other methods and classes that the writer passes the
ODataUri
to. In some cases I have changed the input or property type toODataUriSlim?
fromODataUri
. In other cases I have created new method overloads that useODataUriSlim
as input instead ofODataUri
. Luckily, there were not many places where I needed to do this. I avoided adding bothODataUri
andODataUriSlim
properties to the same class because that would introduce a lot more complexity and maintainability issues than is necessary. I also avoided changing classes and methods beyond what the writer needs, because that's beyond the scope of this PR.I also experimented with making
ODataUriSlim
a class instead of a struct (see: https://github.com/habbes/odata.net/blob/odataurislim-class-experiment/src/Microsoft.OData.Core/Uri/ODataUriSlim.cs). This would still lead to heap allocations, but the objects allocated would be smaller in size. This avoids the inevitable copies of the struct. We can also avoid code duplication by creating a common interface betweenODataUriSlim
andODataUri
, sayIODataUri
which we use as the argument for methods that can accept either type. We could have done the same with the struct, but passing a struct to a method that accepts an interface would lead to boxing, which implies heap allocations, which would have defeated my purpose for using a struct.ODataUriSlim
class experiment ended up allocating slightly more memory thanODataUriSlim
struct, but the benchmark results were pretty close.Benchmark comparisons
According to the SerializationComparisonTests benchmarks in our repo, use of
ODataUriSlim
struct has resulted in an 11% reduction in allocated memory when usingODataMessageWriter
synchronous API. When using the async API, the reduction was 7% when using the defaultJsonWriter
and 10% when usingODataUtf8JsonWriter
(more impact because it has a much smaller memory footprint compared toJsonWriter
async). I did not observe significant improvement in speed, the figures in the time column fluctuated between different runs of the benchmarks, in some cases they were almost the same.Full benchmark results
Baseline (before the PR)
SerializationComparisons benchmarks
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.20348
Intel Xeon E-2336 CPU 2.90GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK= 5.0.404 [C:\Program Files\dotnet\sdk]
[Host] : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT
Toolchain=InProcessEmitToolchain InvocationCount=1 UnrollFactor=1
ODataWriterFeedTests benchmarks
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.20348
Intel Xeon E-2336 CPU 2.90GHz, 1 CPU, 12 logical and 6 physical cores
.NET Core SDK= 5.0.404 [C:\Program Files\dotnet\sdk]
Toolchain=InProcessEmitToolchain InvocationCount=1 UnrollFactor=1
With
ODataUriSlim
structSerializationComparisons benchmarks
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.20348
Intel Xeon E-2336 CPU 2.90GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK= 5.0.404 [C:\Program Files\dotnet\sdk]
[Host] : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT
Toolchain=InProcessEmitToolchain InvocationCount=1 UnrollFactor=1
ODataWriterFeedTests benchmarks
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.20348
Intel Xeon E-2336 CPU 2.90GHz, 1 CPU, 12 logical and 6 physical cores
.NET Core SDK= 5.0.404 [C:\Program Files\dotnet\sdk]
[Host] : .NET Core 3.1.28 (CoreCLR 4.700.22.36202, CoreFX 4.700.22.36301), X64 RyuJIT
Toolchain=InProcessEmitToolchain InvocationCount=1 UnrollFactor=1
With
ODataUriSlim
classSerializationComparisons benchmarks
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.20348
Intel Xeon E-2336 CPU 2.90GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK= 5.0.404 [C:\Program Files\dotnet\sdk]
[Host] : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT
Toolchain=InProcessEmitToolchain InvocationCount=1 UnrollFactor=1
ODataWriterFeedTests benchmarks
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.20348
Intel Xeon E-2336 CPU 2.90GHz, 1 CPU, 12 logical and 6 physical cores
.NET Core SDK= 5.0.404 [C:\Program Files\dotnet\sdk]
[Host] : .NET Core 3.1.28 (CoreCLR 4.700.22.36202, CoreFX 4.700.22.36301), X64 RyuJIT
Toolchain=InProcessEmitToolchain InvocationCount=1 UnrollFactor=1
Checklist (Uncheck if it is not completed)
Additional work necessary
If documentation update is needed, please add "Docs Needed" label to the issue and provide details about the required document change in the issue.