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

IOperationsWrapperSonar: Use compiled expressions #8105

Conversation

martin-strecker-sonarsource
Copy link
Contributor

@martin-strecker-sonarsource martin-strecker-sonarsource commented Sep 28, 2023

The IOperationWrapperSonar does not follow the pattern used for the other wrappers. This PR rewrites the wrapper to

  • Use compiled expressions instead of cached reflection
  • Use a readonly struct instead of a class

Before
image

After
image

@martin-strecker-sonarsource martin-strecker-sonarsource force-pushed the Martin/IOperationsWrapperSonar_CompiledExpressions branch from 1c6708a to 32d1df8 Compare October 12, 2023 11:06
@sonarcloud
Copy link

sonarcloud bot commented Oct 12, 2023

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 0 Code Smells

No Coverage information No Coverage information
No Duplication information No Duplication information

@sonarcloud
Copy link

sonarcloud bot commented Oct 12, 2023

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 0 Code Smells

100.0% 100.0% Coverage
0.0% 0.0% Duplication

Copy link
Contributor

@andrei-epure-sonarsource andrei-epure-sonarsource left a comment

Choose a reason for hiding this comment

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

@@ -24,37 +24,31 @@
namespace StyleCop.Analyzers.Lightup
{
// This is a temporary substitute for IOperationWrapper in case StyleCop will accept PR https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3381
public class IOperationWrapperSonar
public readonly struct IOperationWrapperSonar
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This would require an update to the SourceGenerator as well and will break a lot of existing code (there are several PRs in the StyleCop repo related to this change with a lot of files changed). We can and should absolutely do this. But it isn't an easy effort. We can however still take my solution (which by the way was implemented in StyleCop almost the same way before they moved it it to the source generator: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/f8f0b4b126f55c4619d0930c4a5aeb59f0c572a2/StyleCop.Analyzers/StyleCop.Analyzers/Lightup/IOperationWrapper.cs) as a quick fix for the allocation issue.

? $"Block #{Block.Ordinal}, Branching{Environment.NewLine}{State}"
: $"Block #{Block.Ordinal}, Operation #{index}, {Operation.Instance.Serialize()}{Environment.NewLine}{State}";
Operation.Instance is { } operation
? $"Block #{Block.Ordinal}, Operation #{index}, {operation.Serialize()}{Environment.NewLine}{State}"
Copy link
Contributor

Choose a reason for hiding this comment

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

[education] can this have an impact on the Security FE ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. The FE frontend might be affected by the change from class to readonöly struct. IOperationWrapperSonar could be null before but is never null now (instead the wrapped IOperationWrapperSonar.Instance can be null now).

Copy link
Contributor

Choose a reason for hiding this comment

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

can you check with them on slack if they have a ticket in JIRA for updating the CFG library (they should), and after this gets merged, to add a note about this expected change?

Copy link
Contributor

Choose a reason for hiding this comment

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

from what I understand, it will impact in the sense that when doing the update they may need to change some of the code.

Could it lead to unexpected failures if some test cases are missing from the front-end?
Can you think of an example of practical code snippet where the behavior would change?

Copy link
Contributor

Choose a reason for hiding this comment

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

And also they will need to update all the property names, I guess

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you think of an example of practical code snippet where the behavior would change?

I don't know if the FE access the IOperationWrapperSonar at all. It is in most parts used internally in the SE-engine. The biggest impact is the null behavior which results in changes likes that one in this PR:

- var successors = current.Operation == null ? ProcessBranching(current) : ProcessOperation(current);
+ var successors = current.Operation.Instance == null ? ProcessBranching(current) : ProcessOperation(current);

here

This is a very subtle change and can only be caught by unit tests. current.Operation == null is never true, but the compiler is not complaining here.

And also they will need to update all the property names, I guess

No. The public surface stays the same. It is the change from class to struct that is problematic (but needed).

Could it lead to unexpected failures if some test cases are missing from the front-end?

Yes. See above.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, this is worthy to mention in a JIRA ticket to keep in mind.

}

public IOperation Instance { get; }
public IOperation Parent => ParentAccessor(Instance);
Copy link
Contributor

Choose a reason for hiding this comment

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

sorry to ask silly questions, but given that I am the only volunteer reviewer, I need to understand now how the whole ShimLayer is working.

Why does having a Func reduce memory allocations? Wouldn't each property call create a new instance in memory? As opposed to using something that is cached?

Or is rather the fact that it does create an instance just for the scope of its usage, and previously the ReadCached was actually storing ALL instances, even when they were not necessary?

Do I understand well that this is a tradeoff of memory usage (drop the cache) vs cycle usage (we will recreated the same Parent instance multiple times for the same syntax node?

Copy link
Contributor

Choose a reason for hiding this comment

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

in your perf evaluation (#8105 (comment)) I see you only mention memory and not CPU usage difference (or I am reading it wrong?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Before the IOperationWrapperSonar was a class and instantiated via the the IOperationWrapperSonar ToSonar(this IOperation) extension method. The class was used to access properties like IOperation.Parent which was introduced in a later version of Roslyn that we reference. The old solution access the properties via reflection like so:

  • A static PropertyInfo of the Parent property was created in the static constructor and stored in a static field
  • For each instance of IOperationWrapperSonar a reflection-based call via PropertyInfo.GetValue was made. This is very slow. The result was therefore cached in a field of the instance.

This resulted in

  • An allocation of an instance of the wrapper on each call to ToSonar resulting in 160MB of total allocations (the object contains 5 fields (all object references) so one instance might be roughly 5 times 8 byte + overhead). So there are a lot of instances around.
  • A slow call for the first time the property of the instance is called (the important part: the reflection call happens on each instance)

The "normal" way the wrapper work is like so:

  • Create a readonly struct that wraps the IOperation in a single field (called Instance). This does not allocate on the heap at all (we do not box the wrappers anywhere). It doesn't add any overhead at all as the single field is exactly as big as the original value, so the wrapper takes as much stack space as without the wrapper.
  • The access to the properties is done via a Func<IOperation, TResult> delegate. This delegate is stored in a static field and created in the static constructor. The delegate implementation is dynamically created via System.Linq.Expressions and compiled (via LambdaExpression.Compile which generates IL code via Reflection.Emit in a dynamic generated assembly). This takes a significant time but is paid only once at the very first access of the first instance (first call to ToSonar() of the first instance). Every subsequent call for any instance is as fast as IL generated by the compiler in a normal assembly. The generated implementation for that delegate is just a simple property access and looks about like so (IOperation operation) => operation.Parent;

I did not measure the performance impact on time. I don't think it is measurable in a real-world scanner run but I could give it a try in the analyzerrunner with the SymbolicExecutionRunner analyzer only.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok... I suggest measuring the performance impact on time as well, to be sure.

Better safe than sorry.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With the analyzer runner and SE only for CSVHelper I get the following results:

Allocations

18MB of 1.5 GB (11th place or 1.1%)
image

After: no allocations for IOperationWrapperSonar

Time savings

Time spent in PropertyInfo.GetValue before 685 ms (0.05% of overall runtime) down to 15ms after.

Before
Place 25th of most expensive methods
image

image

After
image

Copy link
Contributor

@andrei-epure-sonarsource andrei-epure-sonarsource left a comment

Choose a reason for hiding this comment

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

LGTM

@andrei-epure-sonarsource andrei-epure-sonarsource merged commit c459f85 into master Oct 19, 2023
63 checks passed
@andrei-epure-sonarsource andrei-epure-sonarsource deleted the Martin/IOperationsWrapperSonar_CompiledExpressions branch October 19, 2023 13:41
@andrei-epure-sonarsource andrei-epure-sonarsource added this to the 9.13 milestone Oct 19, 2023
@costin-zaharia-sonarsource costin-zaharia-sonarsource removed this from the 9.13 milestone Nov 1, 2023
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.

3 participants