-
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
Conversation
@@ -231,6 +233,12 @@ internal static class PeWriter | |||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
There are essentially two cases here:
- The
managedResourceBuilder
has content in which case it will be linked, viaLinkSuffix
, topeBlob
below. Thus freeingpeBlob
frees both. - The
managedResourceBuilder
has no content thusLinkSuffix
does nothing and is discarded. This means it is not returned to the pool.
After some thought decided the easiest approach here was to just discard managedResourceBuilder
here if it's unused. It's optional below so this is not a functional change. Happy to consider other approaches.
The fact that LinkSuffix
and LinkPrefix
don't call Free
on 0-length chunks feels like a bug but waiting for @tmat to confirm.
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.
Went ahead and filed a bug dotnet/runtime#99266
@dotnet/roslyn-compiler PTAL |
@@ -66,7 +66,7 @@ internal static class PeWriter | |||
|
|||
var ilBuilder = new BlobBuilder(32 * 1024); | |||
var mappedFieldDataBuilder = new BlobBuilder(); |
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 for ilBuilder
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 which BlobBuilder
actually owns the instance.
@@ -66,7 +66,7 @@ internal static class PeWriter | |||
|
|||
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
The pattern for BlobBuilder
is unfortunately more complicated. Yes in most cases it's the case that the initial getter is the one who puts it back in the pool. That is the typical pattern for simple uses like building up signatures, sequence points, etc ...
For the more complicated cases like building up the PE file the BlobBuilder
can be linked into a different BlobBuilder
via LinkSuffix / LinkPrefix
. At that point the BlobBuilder
they're linked into becomes the owner and is responsible for freeing the instance. That is the case here. The managedResourceBuilder
will be linked into the peBuilder
instance and that is the responsibility for freeing.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
I'm open to suggestions on how to best express this.
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 comment
The 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 comment
The 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 struct
that is hopefully easier to reason about.
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Yes this is the size of the underlying byte[]
in the BlobBuilder
instance.
Done with review pass (commit 4) |
Not sure why some of your comments aren't displaying inline or in a manner I can respond directly to so responding here
The existing code is attempting to use two locals to represent the difference between writing out an explicit PDB to disk vs. embedding the PDB as a resource. I was trying to not disrupt this pattern with my change but I agree the code isn't as clear as it could be here.
Yes but it's also one of those "if we don't get into that if othre bad things happen to". Stepping back and looking at that |
That is probably happening when a comment is placed on like that wasn't modified in the PR. CodeFlow can do this. And one should be able to respond to the comment in the CodeFlow. |
I would try the following. Add a dedicated local that is used to store the value of the pooled builder. It is the one initialized with |
Note: was having trouble keeping track of all the |
@@ -3372,7 +3370,7 @@ internal void EnsureAnonymousTypeTemplates(CancellationToken cancellationToken) | |||
} | |||
|
|||
// produce the secondary output (ref assembly) if needed | |||
if (emitSecondaryAssembly) | |||
if (getMetadataPeStreamOpt is not 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.
This allowed me to avoid a nullable suppression on getMetadataPeStreamOpt
below.
int strongNameSignatureSize, | ||
MethodDefinitionHandle entryPoint, | ||
CorFlags flags, | ||
Func<IEnumerable<Blob>, BlobContentId> deterministicIdProvider, | ||
Func<IEnumerable<Blob>, BlobContentId>? deterministicIdProvider, |
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.
These changes all line up with the declarations on the base type ctor.
// https://github.com/dotnet/runtime/issues/99266 | ||
if (mappedCount == 0) | ||
{ | ||
MappedFieldDataBlobBuilder.AssertAlive(); |
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.
In a previous iteration of this change I had a similar method called AssertFreed
and @333fred rightfully pointed out it's a race condition. As written now this call is not a race cause the BlobBuilder
will be alive. Once the bug mentioned here is fixed this assert also becomes a race condition. However the chance of the race is unlikely enough that this will still serve as an sufficient trip wire to ensure that we don't forget to update this code.
@@ -35,13 +33,69 @@ public PeWritingException(Exception inner) | |||
|
|||
internal static class PeWriter | |||
{ | |||
internal struct EmitBuilders |
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.
Know there was, rightfully, confusion in previous iterations of the change on whose responsibility it was to free values and whether or not that was handled correctly in the code. Tried to respond to that feedback by moving all of the responsibility for the builder ownership into this type. Hoping this makes everything clearer.
s_chunkPool.Free(this); | ||
} | ||
#if DEBUG | ||
IsAlive = 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.
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 (commit 7)
PortablePdbBlobBuilder = null; | ||
} | ||
|
||
internal 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.
Maybe make this IDisposable
and using
it?
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.
Most of our pool freeing in the compiler is not via IDisposable
which is why I didn't use that here.
var mappedFieldDataBuilder = new BlobBuilder(); | ||
var managedResourceBuilder = new BlobBuilder(1024); | ||
|
||
var emitBuilders = new EmitBuilders(); |
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.
Last night I was considering going ahead and fixing the LinkSuffix / LinkPrefix bug and realized my current approach is flawed. Once that bug is fixed and a new version of SRM is used with Roslyn then my code will start throwing cause Count == 0
builders will be double freed. This will very concretely happen in Source Build.
After some thought I decided a more durable fix is to pass null
into ExtendedPEBuilder
for these two BlobBuilder
instead of an empty BlobBuilder
. The null
is an allowed case and is not impacted by the bug fix. There were two ways to achieve this:
- Add a simple check after the
BuildAndMetadataAndIL
and free +null
out the fields if their Count is zero. This is a smaller change but felt a bit off. Like I was fixing the symptom not the cause. - Do not allocate the
BlobBuilder
unless we're going to put content into it. This change produces bigger code churn but feels more correct.
Ended up going with approach (2) here. If you feel churn is too big can use approach (1) instead.
I made substantial changes to the PR
resource.WriteContentTo(resourceWriter); | ||
resourceWriter.Align(8); | ||
return (uint)result; | ||
} |
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.
These methods are only used in PopulateManifestResourceTableRows
. Given the tight relationship that has to the impl details of these methods now I thought it better to make them local functions instead.
Done with review pass (commit 11) |
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 (commit 12)
Looking at profiles building Compilers.slnf locally found a number of places that we weren't using
PooledBlobBuilder
that were impacting allocations. This change removes 250MB ofbyte[]
allocations and saves 50MB on LOH.This reduces time spent in GC during build by ~1 second.
Note: profiling the compiler server is imprecise as due to the parallel nature of the server and msbuild profiles can be quite noisy. For these measurements I did several measurements and picked the most average for the comparisons.