Optimize memory usage when building the blob heap.#127304
Optimize memory usage when building the blob heap.#127304teo-tsirpanis wants to merge 4 commits intodotnet:mainfrom
Conversation
|
Tagging subscribers to this area: @dotnet/area-system-reflection-metadata |
There was a problem hiding this comment.
Pull request overview
This PR refactors how System.Reflection.Metadata builds the #Blob heap to reduce allocations and avoid a large contiguous buffer allocation by writing blob data incrementally into a BlobBuilder and deduplicating by referencing written segments.
Changes:
- Write
#Blobheap content incrementally into a dedicatedHeapBlobBuilderas blobs are added, and compute heap sizes from_blobBuilder.Count. - Extend
BlobBuilderwith internal “Segment” APIs to allow later referencing of previously written data for deduplication. - Update
BlobDictionaryto useBlobBuilder.Segmentkeys (andAlternateLookupon .NET) instead ofImmutableArray<byte>keys.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/MetadataBuilder.cs | Switches serialized heap size accounting to use _blobBuilder.Count. |
| src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/MetadataBuilder.Heaps.cs | Reworks blob heap accumulation/writing to use _blobBuilder and removes the “write blob heap at end” path. |
| src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/BlobDictionary.cs | Changes blob dedup dictionary to key by BlobBuilder.Segment and uses AlternateLookup on .NET. |
| src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobWriterImpl.cs | Adds span-based compressed-integer writer used by segment writing. |
| src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobBuilder.cs | Adjusts invariants / chunk expansion behavior to support segment-writing scenarios. |
| src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobBuilder.Segment.cs | New internal segment-writing implementation and Segment struct for stable references. |
| src/libraries/System.Reflection.Metadata/src/System/Reflection/Internal/Utilities/Hash.cs | Refactors FNV hashing to add an “accumulate” helper. |
| src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj | Includes the new BlobBuilder.Segment.cs file (and normalizes the first line). |
|
|
||
| _blobs.GetOrAdd(ReadOnlySpan<byte>.Empty, ImmutableArray<byte>.Empty, default, out _); | ||
| _blobHeapSize = 1; | ||
| _blobs = new BlobDictionary(_blobBuilder, 32); |
There was a problem hiding this comment.
The initial capacity for _blobs dropped from 1024 to 32. If the blob heap commonly contains hundreds/thousands of unique blobs (as the previous default implied), this will cause more dictionary resizes and allocations. Consider keeping the previous capacity (or deriving it from an existing heuristic) unless there’s data showing 32 is sufficient.
| _blobs = new BlobDictionary(_blobBuilder, 32); | |
| _blobs = new BlobDictionary(_blobBuilder, 1024); |
There was a problem hiding this comment.
This can be discussed. Other heaps used multiples of 1024 as their capacity in bytes, not elements. Now that we can set the capacity of the blob heap being built, I moved the use of 1024 there, and set the dictionary's initial capacity to
782f239 to
40cf6f1
Compare
|
@EgorBot -arm using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Buffers.Binary;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
BenchmarkSwitcher.FromAssembly(typeof(BlobHeapBenchmarks).Assembly).Run(args);
[MemoryDiagnoser]
public class BlobHeapBenchmarks
{
const int BlobSize = 20;
[Benchmark]
[Arguments(2_000)]
[Arguments(20_000)]
public int Run(int blobCount)
{
var mdBuilder = new MetadataBuilder();
byte[] buffer = new byte[BlobSize];
for (int i = 0; i < blobCount; i++)
{
BinaryPrimitives.WriteInt32LittleEndian(buffer, i);
_ = mdBuilder.GetOrAddBlob(buffer);
}
var mdRootBuilder = new MetadataRootBuilder(mdBuilder, suppressValidation: true);
BlobBuilder output = new BlobBuilder();
mdRootBuilder.Serialize(output, 0, 0);
return output.Count;
}
} |
Background
When building the blob heap,
MetadataBuilderkeeps track of the blobs added, to avoid adding them multiple times. In the beginning, this was happening using aDictionary<ImmutableArray<byte>, BlobHandle>and a custom comparer that compared the keys by value. This approach had the disadvantage of always allocating anImmutableArray<byte>when you calledGetOrAddBlobwith anything except an immutable array. #81059 improved this situation and eliminated most allocations when the blob already exists. However, there are several optimization opportunities in how we build the blob heap:GetOrAddBlobwith a multi-chunkBlobBuilder, even if the blob already existed.BlobBuilder's pooling and chunking facilities, and leads to an LOH allocation.This PR fixes all of the above.
Changes
Instead of keeping track of each blob as an
ImmutableArray<byte>and writing the blob heap at the end, we write the blob heap to aBlobBuilderas each blob gets added, and keep track of each blob by its position within thatBlobBuilder.In order to do that,
BlobBuilderwas extended to support writing data that can be later referenced using aBlobBuilder.Segmentstruct. This is an internal-only functionality that slightly alters some invariants ofBlobBuilder, but is invisible to external consumers.Segment-addressible buffers are written in chunks of increasingly sized buffers up to 8K bytes, matching the behavior ofStringBuilder. This chunking logic will be user-configurable and expanded to allBlobBuilderAPIs as part of #100418.Afterwards,
BlobDictionarywas updated to useBlobBuilder.Segmentas its key type, and append to theBlobBuilderto get a segment when a blob does not already exist. Also, the modern .NET implementation ofBlobDictionarywas significantly simplified by making use of theAlternateLookupAPI.TODO