Skip to content
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

Avoid allocations in CanWrap... methods #3711

Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
60 changes: 36 additions & 24 deletions StyleCop.Analyzers/StyleCop.Analyzers/Lightup/LightupHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,22 @@ internal static bool CanWrapObject(object obj, Type underlyingType)
return false;
}

ConcurrentDictionary<Type, bool> wrappedObject = SupportedObjectWrappers.GetOrAdd(underlyingType, _ => new ConcurrentDictionary<Type, bool>());
Copy link
Contributor

Choose a reason for hiding this comment

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

Was there an actual problem with this first lookup (in each method)? I haven't dug into your IL comparison, but since no capture is needed, it seems the only possible overhead would be creating the func instance. I think I recall that lambdas are automatically detected to be static if possible, but in case that is not correct, wouldn't simply marking the lambda as static be the best change?

ConcurrentDictionary<Type, bool> wrappedObject = SupportedObjectWrappers.GetOrAdd(underlyingType, static _ => new ConcurrentDictionary<Type, bool>());

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The IL confirmed that the delegate instance is indeed cached between calls in a static field:

// Load the delegate instance from a static field  
    IL_0033: ldsfld       class System.Func`2<System.Type, ConcurrentDictionary`2<SyntaxKind, bool>> StyleCop.Analyzers.Lightup.LightupHelpers/'<>c'::'<>9__30_0'
    IL_0038: dup
// Branch if not null?
    IL_0039: brtrue.s     IL_0052
    IL_003b: pop
// ..
// Create the delegate
    IL_0047: newobj       instance void class System.Func`2<System.Type, ConcurrentDictionary`2<SyntaxKind, bool>>::.ctor(object, native int)
    IL_004c: dup
// Store it
    IL_004d: stsfld       class System.Func`2<System.Type, ConcurrentDictionary`2<SyntaxKind, bool>> StyleCop.Analyzers.Lightup.LightupHelpers/'<>c'::'<>9__30_0'
// Invoke the delegate
    IL_0052: callvirt     instance !1/*class [System.Collections.Concurrent]System.Collections.Concurrent.ConcurrentDictionary`2<valuetype [Microsoft.CodeAnalysis.CSharp]Microsoft.CodeAnalysis.CSharp.SyntaxKind, bool>*/ class [System.Collections.Concurrent]System.Collections.Concurrent.ConcurrentDictionary`2<class [System.Runtime]System.Type, class [System.Collections.Concurrent]System.Collections.Concurrent.ConcurrentDictionary`2<valuetype [Microsoft.CodeAnalysis.CSharp]Microsoft.CodeAnalysis.CSharp.SyntaxKind, bool>>::GetOrAdd(!0/*class [System.Runtime]System.Type*/, class [System.Runtime]System.Func`2<!0/*class [System.Runtime]System.Type*/, !1/*class [System.Collections.Concurrent]System.Collections.Concurrent.ConcurrentDictionary`2<valuetype [Microsoft.CodeAnalysis.CSharp]Microsoft.CodeAnalysis.CSharp.SyntaxKind, bool>*/>)

Copy link
Member

Choose a reason for hiding this comment

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

📝 This line does not need to change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just kept the static to emphasize the non-capturing semantic of the lambda.


// Avoid creating the delegate if the value already exists
Copy link
Member

Choose a reason for hiding this comment

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

📝 This comment is sufficient for the method (it already states the complete intent, and it's just a simple bug that we didn't adhere to it)

bool canCast;
if (!wrappedObject.TryGetValue(obj.GetType(), out canCast))
if (!SupportedObjectWrappers.TryGetValue(underlyingType, out var wrappedObject))
{
canCast = wrappedObject.GetOrAdd(
obj.GetType(),
kind => underlyingType.GetTypeInfo().IsAssignableFrom(obj.GetType().GetTypeInfo()));
Copy link
Member

Choose a reason for hiding this comment

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

📝 This line is the source of the capturing allocation (specifically, capturing a method parameter in a lambda forces the the capture to be allocated on all paths and not just the one that creates this delegate).

wrappedObject = SupportedObjectWrappers.GetOrAdd(underlyingType, static _ => new ConcurrentDictionary<Type, bool>());
}

return canCast;
// Avoid creating the delegate and capture class if the value already exists
return wrappedObject.TryGetValue(obj.GetType(), out var canCast)
? canCast
: GetOrAdd(obj, underlyingType, wrappedObject);

// Don't inline this method. Otherwise a capture class is generated on each call to CanWrapObject.
static bool GetOrAdd(object obj, Type underlyingType, ConcurrentDictionary<Type, bool> wrappedObject)
=> wrappedObject.GetOrAdd(
obj.GetType(),
kind => underlyingType.GetTypeInfo().IsAssignableFrom(obj.GetType().GetTypeInfo()));
}

internal static bool CanWrapNode(SyntaxNode node, Type underlyingType)
Expand All @@ -93,18 +97,22 @@ internal static bool CanWrapNode(SyntaxNode node, Type underlyingType)
return false;
}

ConcurrentDictionary<SyntaxKind, bool> wrappedSyntax = SupportedSyntaxWrappers.GetOrAdd(underlyingType, _ => new ConcurrentDictionary<SyntaxKind, bool>());

// Avoid creating the delegate if the value already exists
bool canCast;
if (!wrappedSyntax.TryGetValue(node.Kind(), out canCast))
if (!SupportedSyntaxWrappers.TryGetValue(underlyingType, out var wrappedSyntax))
{
canCast = wrappedSyntax.GetOrAdd(
node.Kind(),
kind => underlyingType.GetTypeInfo().IsAssignableFrom(node.GetType().GetTypeInfo()));
wrappedSyntax = SupportedSyntaxWrappers.GetOrAdd(underlyingType, static _ => new ConcurrentDictionary<SyntaxKind, bool>());
}

return canCast;
// Avoid creating the delegate and capture class if the value already exists
return wrappedSyntax.TryGetValue(node.Kind(), out var canCast)
? canCast
: GetOrAdd(node, underlyingType, wrappedSyntax);

// Don't inline this method. Otherwise a capture class is generated on each call to CanWrapNode.
static bool GetOrAdd(SyntaxNode node, Type underlyingType, ConcurrentDictionary<SyntaxKind, bool> wrappedSyntax) =>
wrappedSyntax.GetOrAdd(
node.Kind(),
kind => underlyingType.GetTypeInfo().IsAssignableFrom(node.GetType().GetTypeInfo()));
}

internal static bool CanWrapOperation(IOperation operation, Type underlyingType)
Expand All @@ -121,18 +129,22 @@ internal static bool CanWrapOperation(IOperation operation, Type underlyingType)
return false;
}

ConcurrentDictionary<OperationKind, bool> wrappedSyntax = SupportedOperationWrappers.GetOrAdd(underlyingType, _ => new ConcurrentDictionary<OperationKind, bool>());

// Avoid creating the delegate if the value already exists
bool canCast;
if (!wrappedSyntax.TryGetValue(operation.Kind, out canCast))
if (!SupportedOperationWrappers.TryGetValue(underlyingType, out var wrappedSyntax))
{
canCast = wrappedSyntax.GetOrAdd(
operation.Kind,
kind => underlyingType.GetTypeInfo().IsAssignableFrom(operation.GetType().GetTypeInfo()));
wrappedSyntax = SupportedOperationWrappers.GetOrAdd(underlyingType, static _ => new ConcurrentDictionary<OperationKind, bool>());
}

return canCast;
// Avoid creating the delegate if the value already exists
return wrappedSyntax.TryGetValue(operation.Kind, out var canCast)
? canCast
: GetOrAdd(operation, underlyingType, wrappedSyntax);

// Don't inline this method. Otherwise a capture class is generated on each call to CanWrapOperation.
static bool GetOrAdd(IOperation operation, Type underlyingType, ConcurrentDictionary<OperationKind, bool> wrappedSyntax) =>
wrappedSyntax.GetOrAdd(
operation.Kind,
kind => underlyingType.GetTypeInfo().IsAssignableFrom(operation.GetType().GetTypeInfo()));
}

internal static Func<TOperation, TProperty> CreateOperationPropertyAccessor<TOperation, TProperty>(Type type, string propertyName)
Expand Down