[SRM] Optimize MetadataBuilder.GetOrAddConstantBlob.#121223
[SRM] Optimize MetadataBuilder.GetOrAddConstantBlob.#121223teo-tsirpanis wants to merge 9 commits intodotnet:mainfrom
MetadataBuilder.GetOrAddConstantBlob.#121223Conversation
The logic of writing scalar constants was extracted to its own method that writes to a span. This method also got used by `MetadataBuilder.GetOrAddConstantBlob`, avoiding the use of pooled blob builders when writing scalar constants.
|
Tagging subscribers to this area: @dotnet/area-system-reflection-metadata |
There was a problem hiding this comment.
Pull Request Overview
This PR optimizes constant blob encoding by eliminating unnecessary allocations. It refactors the GetOrAddConstantBlob method and the WriteConstant methods to use stack-allocated buffers instead of pooled BlobBuilder instances for scalar (non-string) constants.
Key Changes:
- Introduced a new
WriteScalarConstantmethod that writes scalar constants to aSpan<byte>and returns the number of bytes written - Refactored
GetOrAddConstantBlobto use stack allocation for scalar values - Updated both
WriteConstantoverloads to callWriteScalarConstantfor non-string values
Reviewed Changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| MetadataBuilder.Heaps.cs | Refactored GetOrAddConstantBlob to use stack-allocated buffer and new WriteScalarConstant method |
| BlobWriterImpl.cs | Added new WriteScalarConstant method and refactored existing WriteConstant overloads to use it |
src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobWriterImpl.cs
Show resolved
Hide resolved
src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobWriterImpl.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobWriterImpl.cs
Show resolved
Hide resolved
AaronRobinsonMSFT
left a comment
There was a problem hiding this comment.
This looks good to me. Nothing obvious jumps out and I assume we have good test coverage for this area.
Do you have any perf numbers that show the benefit of this optimization? |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
|
|
I do not think the benchmark you have shared above is exercising the code changed in this PR. |
Oops, sorry; I should stop working on multiple PRs at the same time.
Yes; the idea is that serializing the blob to check if it exists would be faster, as it would use stack-allocated memory instead of a pooled |
|
Latest EgorBot results show a 2x slowdown for Alternatively, I'd be fine with optimizing just |
|
I had an idea on how to benchmark |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
You can also share your feedback on Copilot code review. Take the survey.
...s/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/MetadataBuilder.Heaps.cs
Outdated
Show resolved
Hide resolved
...ibraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/BlobDictionary.cs
Outdated
Show resolved
Hide resolved
This reverts commit 3842337.
…tantBlob` is now optimized.
|
I copied the minimum necessary subset of SRM's sources into a separate benchmark project, and compared the old and new implementations of
I reverted all other parts of the PR and scoped it down to optimizing just |
BlobWriterImpl.WriteConstant.MetadataBuilder.GetOrAddConstantBlob.
| /// <param name="bytes">The span where the content will be encoded.</param> | ||
| /// <param name="value">The constant value.</param> | ||
| /// <returns>The number of bytes that was written.</returns> | ||
| internal static int WriteScalarConstant(Span<byte> bytes, object? value) |
There was a problem hiding this comment.
The existing two copies of this method below are called WriteConstant. Use the same name for all 3 for consistency?
There was a problem hiding this comment.
The existing two methods also accept strings besides scalar values.
src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobWriterImpl.cs
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
You can also share your feedback on Copilot code review. Take the survey.
| /// <summary> | ||
| /// Writes a scalar (non-string) constant to a span. | ||
| /// </summary> | ||
| /// <param name="bytes">The span where the content will be encoded.</param> | ||
| /// <param name="value">The constant value.</param> | ||
| /// <returns>The number of bytes that was written.</returns> | ||
| internal static int WriteScalarConstant(Span<byte> bytes, object? value) | ||
| { | ||
| if (value == null) | ||
| { | ||
| // The encoding of Type for the nullref value for FieldInit is ELEMENT_TYPE_CLASS with a Value of a 32-bit. | ||
| BinaryPrimitives.WriteUInt32LittleEndian(bytes, 0); | ||
| return sizeof(uint); | ||
| } | ||
|
|
||
| var type = value.GetType(); | ||
| if (type.IsEnum) | ||
| { | ||
| type = Enum.GetUnderlyingType(type); | ||
| } | ||
|
|
||
| if (type == typeof(bool)) |
There was a problem hiding this comment.
The existing WriteConstant overloads don't have tests. There is https://github.com/dotnet/runtime/blob/main/src/libraries/System.Reflection.Emit/tests/FieldBuilder/FieldBuilderSetConstant.cs that indirectly tests this function.
| { | ||
| if (value == null) | ||
| { | ||
| // The encoding of Type for the nullref value for FieldInit is ELEMENT_TYPE_CLASS with a Value of a 32-bit. |
There was a problem hiding this comment.
Reworded to match how ECMA-335 states it (II.22.9).
| internal static int WriteScalarConstant(Span<byte> bytes, object? value) | ||
| { | ||
| if (value == null) | ||
| { | ||
| // The encoding of Type for the nullref value for FieldInit is ELEMENT_TYPE_CLASS with a Value of a 32-bit. | ||
| BinaryPrimitives.WriteUInt32LittleEndian(bytes, 0); | ||
| return sizeof(uint); | ||
| } |
There was a problem hiding this comment.
I don't think the assert is necessary, because we are using range-checked APIs to write to the span. It should be OK for example to pass a 4 byte long span if the value is known to be an int.
|
@teo-tsirpanis Could you please take a look at the copilot feedback? |
| } | ||
| else if (type == typeof(char)) | ||
| { | ||
| BinaryPrimitives.WriteUInt16LittleEndian(bytes, (char)value); |
There was a problem hiding this comment.
BinaryPrimitives.WriteUInt16LittleEndian takes a ushort value; passing (char)value here relies on a conversion that doesn't exist (char -> ushort is not implicit) and will fail to compile. Cast to ushort (e.g., (ushort)(char)value) before calling WriteUInt16LittleEndian.
| BinaryPrimitives.WriteUInt16LittleEndian(bytes, (char)value); | |
| BinaryPrimitives.WriteUInt16LittleEndian(bytes, (ushort)(char)value); |
In
MetadataBuilder.GetOrAddConstantBlob, the pooled blob builder was replaced by writing to a stack-allocated span, resulting in a 2x performance improvement when repeatedly writing the same scalar value (see comments in the bottom for benchmark results).