Skip to content

Reduce deserialization allocations#698

Draft
davkean wants to merge 5 commits intomicrosoft:mainfrom
davkean:dev/davkean/reduce-allocations
Draft

Reduce deserialization allocations#698
davkean wants to merge 5 commits intomicrosoft:mainfrom
davkean:dev/davkean/reduce-allocations

Conversation

@davkean
Copy link
Copy Markdown
Member

@davkean davkean commented Apr 8, 2026

No description provided.

Comment thread src/Microsoft.VisualStudio.Composition/Configuration/SerializationContextBase.cs Outdated
this.resolver = resolver;
}

internal LazyMetadataWrapper(ImmutableDictionary<string, object?> metadata, Direction direction, Resolver resolver)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this constructor still needed?

Comment thread src/Microsoft.VisualStudio.Composition/LazyMetadataWrapper.cs Outdated
Comment thread src/Microsoft.VisualStudio.Composition/LazyMetadataWrapper.cs Outdated
Comment thread src/Microsoft.VisualStudio.Composition/Configuration/SerializationContextBase.cs Outdated

// Update our metadata dictionary with the substitution to avoid
// the translation costs next time.
this.underlyingMetadata = this.underlyingMetadata.SetItem(key, value);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this no longer applicable?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who are you asking? I see a code comment below that seems to justify it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It added this comment after I asked this. :) I could not get it to resolve the question.

Comment thread src/Microsoft.VisualStudio.Composition/RuntimeComposition.cs Outdated
@davkean davkean force-pushed the dev/davkean/reduce-allocations branch from 0ed26d5 to a906f7a Compare April 8, 2026 05:44
@davkean davkean changed the title Reduce deserialization metadata Reduce deserialization allocations Apr 8, 2026
serializedMetadata = new LazyMetadataWrapper(metadata.ToImmutableDictionary(), LazyMetadataWrapper.Direction.ToSubstitutedValue, this.Resolver);
// Both Dictionary and ImmutableDictionary implement IDictionary,
// which Dictionary's copy constructor accepts.
var metadataCopy = metadata is IDictionary<string, object?> dict
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this guaranateed to be Dictionary<string, object?>? This also does not copy the metadata on the second path.

Comment thread src/Microsoft.VisualStudio.Composition/LazyMetadataWrapper.cs Outdated
Comment thread src/Microsoft.VisualStudio.Composition/LazyMetadataWrapper.cs
@davkean davkean force-pushed the dev/davkean/reduce-allocations branch 5 times, most recently from 381a867 to 700ed7e Compare April 8, 2026 11:18
private static readonly object BoxedTrue = true;
private static readonly object BoxedFalse = false;

private static readonly IReadOnlyDictionary<string, object?> EmptyMetadata = new Dictionary<string, object?>();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make absolutely sure it's read-only and empty, and even avoid allocating the dictionary.

Suggested change
private static readonly IReadOnlyDictionary<string, object?> EmptyMetadata = new Dictionary<string, object?>();
private static readonly IReadOnlyDictionary<string, object?> EmptyMetadata = ImmutableDictionary<string, object?>.Empty;

Comment on lines +846 to +851
// Dictionary has no constructor accepting IReadOnlyDictionary, so enumerate manually.
var metadataCopy = new Dictionary<string, object?>(metadata.Count);
foreach (var entry in metadata)
{
metadataCopy.Add(entry.Key, entry.Value);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No constructor, perhaps. But you can use this extension method, can't you?

Suggested change
// Dictionary has no constructor accepting IReadOnlyDictionary, so enumerate manually.
var metadataCopy = new Dictionary<string, object?>(metadata.Count);
foreach (var entry in metadata)
{
metadataCopy.Add(entry.Key, entry.Value);
}
Dictionary<string, object?> metadataCopy = metadata.ToDictionary();

Comment on lines +256 to +263
// Dictionary has no constructor accepting IReadOnlyDictionary, so enumerate manually.
var dict = new Dictionary<string, object?>(newMetadata.Count);
foreach (var kvp in newMetadata)
{
dict.Add(kvp.Key, kvp.Value);
}

return new LazyMetadataWrapper(dict, oldVersion.direction, this.resolver);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Dictionary has no constructor accepting IReadOnlyDictionary, so enumerate manually.
var dict = new Dictionary<string, object?>(newMetadata.Count);
foreach (var kvp in newMetadata)
{
dict.Add(kvp.Key, kvp.Value);
}
return new LazyMetadataWrapper(dict, oldVersion.direction, this.resolver);
return new LazyMetadataWrapper(newMetadata.ToDictionary(), oldVersion.direction, this.resolver);


// Update our metadata dictionary with the substitution to avoid
// the translation costs next time.
this.underlyingMetadata = this.underlyingMetadata.SetItem(key, value);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who are you asking? I see a code comment below that seems to justify it.

/// These involve CLR metadata resolution or reflection emit, so they are worth caching.
/// Allocated on first access to avoid paying the cost during composition cache loading.
/// </summary>
private sealed class ResolvedMembers
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't a nullable struct be even better?

Comment on lines +324 to +328
/// <summary>
/// Caches for resolved reflection objects and the lazy factory delegate.
/// These involve CLR metadata resolution or reflection emit, so they are worth caching.
/// Allocated on first access to avoid paying the cost during composition cache loading.
/// </summary>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were already caching these. Why create a nested type? Was it to shrink the size of RuntimeImport objects? Particularly ones that never get accessed?

Copy link
Copy Markdown
Member Author

@davkean davkean Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes to reduce the size of RuntimeImport so we only allocate when the data is accessed.

@davkean davkean force-pushed the dev/davkean/reduce-allocations branch from 700ed7e to c270888 Compare April 9, 2026 03:41
davkean added 5 commits April 9, 2026 13:49
ImmutableDictionary allocates SortedInt32KeyNode tree nodes on construction
and a new dictionary on every SetItem call. Dictionary avoids both.

The old code wrote substituted values back via ImmutableDictionary.SetItem,
which was structurally thread-safe (new dict + atomic reference swap). With
Dictionary, concurrent writes would corrupt internal state. The write-back
is removed; substitution cost without caching is negligible since
TypeRef.Resolve() already caches its result and Enum.ToObject is trivial.
Build metadata into Dictionary directly instead of ImmutableDictionary.Builder.
Return ImmutableDictionary.Empty singleton for empty import metadata (majority
of imports have no metadata), avoiding a LazyMetadataWrapper allocation.
Add ToDictionary extension for IReadOnlyDictionary since the BCL Dictionary
constructor does not accept IReadOnlyDictionary on netstandard2.0.
Remove 5 cached fields from RuntimeImport that read already-cached
TypeRef.ResolvedType (IsLazy, ImportingSiteElementType, MetadataType,
isMetadataTypeInitialized, NullableBool). Retain ImportingMember and
ImportingParameter caches since they involve CLR metadata resolution.

Move lazy factory delegate creation into a static ConcurrentDictionary
cache in LazyServices, keyed by (exportType, metadataViewType). This
shares delegates across all imports with the same Lazy<T> or
Lazy<T, TMetadata> signature, eliminating per-import MakeGenericMethod
and CreateDelegate calls.
Avoid per-call boxing for CreationPolicy enum (3 values) and small
integers (0, 1, -1) that appear frequently in metadata.
Avoid Array.CreateInstance + Array.SetValue reflection overhead
for object[], string[], and Type[] arrays in metadata deserialization.
@davkean davkean force-pushed the dev/davkean/reduce-allocations branch from c270888 to d44ac2b Compare April 9, 2026 03:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants