-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Better BlobBuilder pooling #72383
Better BlobBuilder pooling #72383
Changes from 4 commits
03a61be
4351f58
06b1763
d2e8a20
fdcf80e
8f30134
1101c64
5e43343
fc23dec
814cd23
e08abbd
869a2eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System.Reflection.Metadata; | ||
using System.Reflection.Metadata.Ecma335; | ||
using Microsoft.Cci; | ||
|
||
namespace Microsoft.CodeAnalysis; | ||
|
||
internal static class MetadataBuilderExtensions | ||
{ | ||
internal static BlobHandle GetOrAddBlobAndFree(this MetadataBuilder metadataBuilder, PooledBlobBuilder builder) | ||
{ | ||
var handle = metadataBuilder.GetOrAddBlob(builder); | ||
builder.Free(); | ||
return handle; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -66,7 +66,7 @@ internal static bool WritePeToStream( | |
|
||
var ilBuilder = new BlobBuilder(32 * 1024); | ||
var mappedFieldDataBuilder = new BlobBuilder(); | ||
var managedResourceBuilder = new BlobBuilder(1024); | ||
var managedResourceBuilder = PooledBlobBuilder.GetInstance(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The pattern for For the more complicated cases like building up the PE file the I'm open to suggestions on how to best express this. I tried a few patterns for asserting that values were properly freed but @333fred rightfully pointed out that they were race conditions. Can try and comment better the ownership at the point of allocation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I suggest to try the following. If some API can take an ownership of a builder, the builder should be passed by reference. If ownership is taken, the ref parameter should be set to null. The caller always frees if the builder is not null. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In theory I agree with this principal. Unfortunately, many of the API we are dealing with here are in SRM and are existing public API. Can't change them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Think I have an idea on how to clear this up a bit. Going to try and encapsulate this weird ownership problem for this method into a |
||
|
||
Blob mvidFixup, mvidStringFixup; | ||
mdWriter.BuildMetadataAndIL( | ||
|
@@ -155,7 +155,7 @@ internal static bool WritePeToStream( | |
// We need to calculate the PDB checksum, so we may as well use the calculated hash for PDB ID regardless of whether deterministic build is requested. | ||
var portablePdbContentHash = default(ImmutableArray<byte>); | ||
|
||
BlobBuilder portablePdbToEmbed = null; | ||
PooledBlobBuilder portablePdbToEmbed = null; | ||
if (mdWriter.EmitPortableDebugMetadata) | ||
{ | ||
mdWriter.AddRemainingDebugDocuments(mdWriter.Module.DebugDocumentsBuilder.DebugDocuments); | ||
|
@@ -167,7 +167,7 @@ internal static bool WritePeToStream( | |
new Func<IEnumerable<Blob>, BlobContentId>(content => BlobContentId.FromHash(portablePdbContentHash = CryptographicHashProvider.ComputeHash(context.Module.PdbChecksumAlgorithm, content))) : | ||
null; | ||
|
||
var portablePdbBlob = new BlobBuilder(); | ||
var portablePdbBlob = PooledBlobBuilder.GetInstance(zero: true); | ||
var portablePdbBuilder = mdWriter.GetPortablePdbBuilder(metadataRootBuilder.Sizes.RowCounts, debugEntryPointHandle, portablePdbIdProvider); | ||
pdbContentId = portablePdbBuilder.Serialize(portablePdbBlob); | ||
portablePdbVersion = portablePdbBuilder.FormatVersion; | ||
|
@@ -186,6 +186,7 @@ internal static bool WritePeToStream( | |
try | ||
{ | ||
portablePdbBlob.WriteContentTo(portablePdbStream); | ||
portablePdbBlob.Free(); | ||
} | ||
catch (Exception e) when (!(e is OperationCanceledException)) | ||
{ | ||
|
@@ -221,6 +222,7 @@ internal static bool WritePeToStream( | |
if (portablePdbToEmbed != null) | ||
{ | ||
debugDirectoryBuilder.AddEmbeddedPortablePdbEntry(portablePdbToEmbed, portablePdbVersion); | ||
portablePdbToEmbed.Free(); | ||
} | ||
} | ||
else | ||
|
@@ -231,6 +233,12 @@ internal static bool WritePeToStream( | |
var strongNameProvider = context.Module.CommonCompilation.Options.StrongNameProvider; | ||
var corFlags = properties.CorFlags; | ||
|
||
if (managedResourceBuilder.Count == 0) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are essentially two cases here:
After some thought decided the easiest approach here was to just discard The fact that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Went ahead and filed a bug dotnet/runtime#99266 |
||
{ | ||
managedResourceBuilder.Free(); | ||
managedResourceBuilder = null; | ||
} | ||
|
||
var peBuilder = new ExtendedPEBuilder( | ||
peHeaderBuilder, | ||
metadataRootBuilder, | ||
|
@@ -245,7 +253,11 @@ internal static bool WritePeToStream( | |
peIdProvider, | ||
metadataOnly && !context.IncludePrivateMembers); | ||
|
||
var peBlob = new BlobBuilder(); | ||
// This needs to force the backing builder to zero due to the issue writing COFF | ||
// headers. Can remove once this issue is fixed and we've moved to SRM with the | ||
// fix | ||
// https://github.com/dotnet/runtime/issues/99244 | ||
var peBlob = PooledBlobBuilder.GetInstance(zero: true); | ||
var peContentId = peBuilder.Serialize(peBlob, out Blob mvidSectionFixup); | ||
|
||
PatchModuleVersionIds(mvidFixup, mvidSectionFixup, mvidStringFixup, peContentId.Guid); | ||
|
@@ -264,6 +276,8 @@ internal static bool WritePeToStream( | |
throw new PeWritingException(e); | ||
} | ||
|
||
peBlob.Free(); | ||
|
||
return true; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,29 +6,47 @@ | |
using System.Reflection.Metadata; | ||
using Microsoft.CodeAnalysis.PooledObjects; | ||
using Microsoft.CodeAnalysis; | ||
using System.Diagnostics; | ||
jaredpar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
namespace Microsoft.Cci | ||
{ | ||
internal sealed class PooledBlobBuilder : BlobBuilder, IDisposable | ||
{ | ||
private const int PoolSize = 128; | ||
private const int ChunkSize = 1024; | ||
private const int PoolChunkSize = 1024; | ||
|
||
private static readonly ObjectPool<PooledBlobBuilder> s_chunkPool = new ObjectPool<PooledBlobBuilder>(() => new PooledBlobBuilder(ChunkSize), PoolSize); | ||
private static readonly ObjectPool<PooledBlobBuilder> s_chunkPool = new ObjectPool<PooledBlobBuilder>(() => new PooledBlobBuilder(PoolChunkSize), PoolSize); | ||
|
||
private PooledBlobBuilder(int size) | ||
: base(size) | ||
{ | ||
} | ||
|
||
public static PooledBlobBuilder GetInstance() | ||
/// <summary> | ||
/// Get a new instance of the <see cref="BlobBuilder"/> that has <see cref="BlobBuilder.ChunkCapacity"/> of | ||
/// at least <see cref="PoolChunkSize"/> | ||
/// </summary> | ||
/// <param name="zero">When true force zero out the backing buffer</param> | ||
/// <remarks> | ||
/// The <paramref name="zero"/> can be removed when moving to SRM 9.0 if it contains the bug fix for | ||
/// <see cref="BlobBuilder.ReserveBytes(int)"/> | ||
/// | ||
/// https://github.com/dotnet/runtime/issues/99244 | ||
/// </remarks> | ||
public static PooledBlobBuilder GetInstance(bool zero = false) | ||
{ | ||
return s_chunkPool.Allocate(); | ||
var builder = s_chunkPool.Allocate(); | ||
if (zero) | ||
{ | ||
builder.WriteBytes(0, builder.ChunkCapacity); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes this is the size of the underlying |
||
builder.Clear(); | ||
} | ||
return builder; | ||
} | ||
|
||
protected override BlobBuilder AllocateChunk(int minimalSize) | ||
{ | ||
if (minimalSize <= ChunkSize) | ||
if (minimalSize <= PoolChunkSize) | ||
{ | ||
return s_chunkPool.Allocate(); | ||
} | ||
|
@@ -38,7 +56,28 @@ protected override BlobBuilder AllocateChunk(int minimalSize) | |
|
||
protected override void FreeChunk() | ||
{ | ||
s_chunkPool.Free(this); | ||
if (ChunkCapacity != PoolChunkSize) | ||
{ | ||
// The invariant of this builder is that it produces BlobBuilder instances that have a | ||
// ChunkCapacity of at least 1024. Essentially inside AllocateChuck the pool must be able | ||
jaredpar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// to mindlessly allocate a BlobBuilder where ChunkCapacity is at least 1024. | ||
// | ||
// To maintain this the code must verify that the returned BlobBuilder instances have | ||
// a backing array of the appropriate size. This array can shrink in practice through code | ||
// like the following: | ||
// | ||
// var builder = PooledBlobBuilder.GetInstance(); | ||
// builder.LinkSuffix(new BlobBuilder(256)); | ||
// builder.Free(); // calls FreeChunk where ChunkCapacity is 256 | ||
// | ||
// This shouldn't happen much in practice due to convention of how builders are used but | ||
// it is a legal use of the APIs. | ||
s_chunkPool.ForgetTrackedObject(this); | ||
} | ||
else | ||
{ | ||
s_chunkPool.Free(this); | ||
} | ||
} | ||
|
||
public new void Free() | ||
|
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.
Why aren't these BlobBuilders pooled as well?
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
ilBuilder
instance is not pooled because it is significantly larger than the instances that we have pools for. I considered creating a separate pool forilBuilder
but it's complicated. I wanted to get past this change, dig deeper into the profiles and if this proves to be a significant source of allocations then I'll come back to it.The
mappedFieldDataBuilder
wasn't shown to be a significant source of allocations hence I hadn't done the work yet to establish the ownership. Unfortunately unlike most other pooling decisions this isn't a simple "it's that type, use the pool" decision. Instead have to sit down and map out whichBlobBuilder
actually owns the instance.