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

SE: Caching: Cache "GetHashCode" for SymbolicValue #6968

Merged

Conversation

martin-strecker-sonarsource
Copy link
Contributor

Part of #6964

@@ -37,6 +39,11 @@ public sealed record SymbolicValue
public IEnumerable<SymbolicConstraint> AllConstraints =>
Constraints.Values;

public SymbolicValue()
{
hashCode = new(() => HashCode.DictionaryContentHash(Constraints), LazyThreadSafetyMode.ExecutionAndPublication);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@martin-strecker-sonarsource
Copy link
Contributor Author

Impact on allocations:
image

@Corniel
Copy link
Contributor

Corniel commented Mar 22, 2023

So it reduced the number of iterations on your dictionary with a factor two? That's nice. Did you create a benchmark too?

@martin-strecker-sonarsource
Copy link
Contributor Author

Did you create a benchmark too?

No. This is done with the AnalyzerRunner and the Visual Studio diagnostics tool. I may add a markdown file to our docs on how to do that. It might come in handy as we have quite some of these optimizations to do and this helps with guiding in the right direction.

Comment on lines 38 to 53
private readonly ImmutableDictionary<Type, SymbolicConstraint> constraints = ImmutableDictionary<Type, SymbolicConstraint>.Empty;
private int? constraintsHashCode;

// SymbolicValue can have only one constraint instance of specific type at a time
private ImmutableDictionary<Type, SymbolicConstraint> Constraints { get; init; } = ImmutableDictionary<Type, SymbolicConstraint>.Empty;
private ImmutableDictionary<Type, SymbolicConstraint> Constraints
{
get => constraints;
init
{
if (constraints != value)
{
constraints = value;
constraintsHashCode = null;
}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is also

HashCode.Combine(
HashCode.DictionaryContentHash(OperationValue),
HashCode.DictionaryContentHash(SymbolValue),
HashCode.DictionaryContentHash(CaptureOperation),
HashCode.EnumerableContentHash(PreservedSymbols),
HashCode.EnumerableContentHash(Exceptions));

and I want to use the same pattern there as well. There it is more important to reset the cached hashcode in the init setter. There we can do this, to make the declaration more compact:

private readonly ImmutableDictionary<IOperation, SymbolicValue> operationValue;
private int? operationValueHashCode;

private ImmutableDictionary<IOperation, SymbolicValue> OperationValue
{
    get => operationValue;
    init => SetHashCodeCachedField(value, ref operationValue, ref operationValueHashCode);
}

private static void SetHashCodeCachedField<T>(T value, ref T backingField, ref int? backingFieldHashCode) where T: class
{
    if (value != backingField)
    {
        backingField = value;
        backingFieldHashCode = null;
    }
}

Copy link
Contributor

Choose a reason for hiding this comment

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

The init and ref trick sounds good in principle.

backingFieldHashCode should not be a parameter, the method can just reset it directly.

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 SetHashCodeCachedField method doesn't make sense in this class. It will come in handy in ProgramState. I just wanted to make sure we do the caching in both classes the same way, so we don't solve the same problem differently in these highly related classes. Slight differences like this should be fine though.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's also my understanding that it should not be here, because it's not needed (yet).

@martin-strecker-sonarsource martin-strecker-sonarsource marked this pull request as ready for review March 23, 2023 13:54
@github-actions github-actions bot moved this from In progress to Review in progress in Best Kanban Mar 23, 2023
Copy link
Contributor

@pavel-mikula-sonarsource pavel-mikula-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 with a small rename before merging

@@ -35,8 +35,22 @@ public sealed record SymbolicValue
public static readonly SymbolicValue True = NotNull.WithConstraint(BoolConstraint.True);
public static readonly SymbolicValue False = NotNull.WithConstraint(BoolConstraint.False);

private readonly ImmutableDictionary<Type, SymbolicConstraint> constraints = ImmutableDictionary<Type, SymbolicConstraint>.Empty;
private int? constraintsHashCode;
Copy link
Contributor

Choose a reason for hiding this comment

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

It is and will be hash code for the entire class. No matter how much mess we add.

Suggested change
private int? constraintsHashCode;
private int? hashCode;

Comment on lines 38 to 53
private readonly ImmutableDictionary<Type, SymbolicConstraint> constraints = ImmutableDictionary<Type, SymbolicConstraint>.Empty;
private int? constraintsHashCode;

// SymbolicValue can have only one constraint instance of specific type at a time
private ImmutableDictionary<Type, SymbolicConstraint> Constraints { get; init; } = ImmutableDictionary<Type, SymbolicConstraint>.Empty;
private ImmutableDictionary<Type, SymbolicConstraint> Constraints
{
get => constraints;
init
{
if (constraints != value)
{
constraints = value;
constraintsHashCode = null;
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

The init and ref trick sounds good in principle.

backingFieldHashCode should not be a parameter, the method can just reset it directly.

Comment on lines 344 to 346
.NotBe(SymbolicValue.Null.GetHashCode()).And
.NotBe(SymbolicValue.NotNull.GetHashCode()).And
.NotBe(SymbolicValue.True.GetHashCode()).And
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's put the .And at the beginning of the line. LIke &&

@github-actions github-actions bot moved this from Review in progress to Review approved in Best Kanban Mar 23, 2023
Copy link
Contributor

@pavel-mikula-sonarsource pavel-mikula-sonarsource left a comment

Choose a reason for hiding this comment

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

Still LGTM

@github-actions github-actions bot moved this from Review in progress to Review approved in Best Kanban Mar 24, 2023
get => constraints;
init
{
if (constraints != value)
Copy link
Contributor

Choose a reason for hiding this comment

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

This condition is not covered when values are the same. We don't use it that way. And we actually shouldn't, as we should attempt no-operation when possible.

Do we really need the condition then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It doesn't hurt to keep it, IMO. It allows doing something like, e.g. with Constraints = someCond ? Empty : Constraints. I could add a test case to increase test coverage if that is a concern.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would remove the condition. It's one more additional Equals check on a hot path that doesn't need to be there.

And so far, we didn't need it. And it's against the intended usage of the property. We can always add it back if we need to.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's fine with me.

@sonarcloud
Copy link

sonarcloud bot commented Mar 27, 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 Mar 27, 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

@pavel-mikula-sonarsource pavel-mikula-sonarsource merged commit 36047e9 into feature/SE Mar 27, 2023
Best Kanban automation moved this from Review approved to Validate Peach Mar 27, 2023
@pavel-mikula-sonarsource pavel-mikula-sonarsource deleted the Martin/SE_6964_01_Cache_GetHashcode branch March 27, 2023 10:32
@martin-strecker-sonarsource martin-strecker-sonarsource moved this from Validate Peach to Done in Best Kanban Mar 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Best Kanban
  
Done
Development

Successfully merging this pull request may close these issues.

None yet

3 participants