Skip to content

Optimize VB lookup#26398

Closed
sharwell wants to merge 5 commits intodotnet:dev15.8-preview1from
sharwell:optimize-vb-lookup
Closed

Optimize VB lookup#26398
sharwell wants to merge 5 commits intodotnet:dev15.8-preview1from
sharwell:optimize-vb-lookup

Conversation

@sharwell
Copy link
Copy Markdown
Contributor

This change defers the acquisition of LookupResult instances from the shared pool
to a point where they are known to be needed. In highly-concurrent compilation
scenarios (using AnalyzerRunner), this change was observed to reduce overall
allocations in the compiler by up to 50% (30GiB) when processing Roslyn.sln.

Ask Mode template not completed

Customer scenario

What does the customer do to get into this situation, and why do we think this
is common enough to address for this release. (Granted, sometimes this will be
obvious "Open project, VS crashes" but in general, I need to understand how
common a scenario is)

Bugs this fixes

(either VSO or GitHub links)

Workarounds, if any

Also, why we think they are insufficient for RC vs. RC2, RC3, or RTW

Risk

This is generally a measure our how central the affected code is to adjacent
scenarios and thus how likely your fix is to destabilize a broader area of code

Performance impact

(with a brief justification for that assessment (e.g. "Low perf impact because no extra allocations/no complexity changes" vs. "Low")

Is this a regression from a previous update?

Root cause analysis

How did we miss it? What tests are we adding to guard against it in the future?

How was the bug found?

(E.g. customer reported it vs. ad hoc testing)

Test documentation updated?

If this is a new non-compiler feature or a significant improvement to an existing feature, update https://github.com/dotnet/roslyn/wiki/Manual-Testing once you know which release it is targeting.

This change defers the acquisition of LookupResult instances from the shared pool
to a point where they are known to be needed. In highly-concurrent compilation
scenarios (using AnalyzerRunner), this change was observed to reduce overall
allocations in the compiler by up to 50% (30GiB) when processing Roslyn.sln.
@sharwell sharwell requested a review from a team as a code owner April 25, 2018 21:34
@jaredpar jaredpar added this to the 15.8 milestone Apr 25, 2018
End If

result?.MergeOverloadedOrPrioritized(currentResult, True)
currentResult?.Free()

This comment was marked as resolved.

End If

result?.MergeOverloadedOrPrioritized(currentResult, checkIfCurrentHasOverloads:=False)
currentResult?.Free()

This comment was marked as resolved.

@jaredpar
Copy link
Copy Markdown
Member

CC @dotnet/roslyn-compiler

Dim currentResult As LookupResult = Nothing
LookupWithoutInheritance(currentResult, currentType, name, arity, options, accessThroughType, binder, useSiteDiagnostics)
If result.IsGoodOrAmbiguous AndAlso currentResult.IsGoodOrAmbiguous AndAlso Not LookupResult.CanOverload(result.Symbols(0), currentResult.Symbols(0)) Then
If result IsNot Nothing AndAlso result.IsGoodOrAmbiguous AndAlso currentResult IsNot Nothing AndAlso currentResult.IsGoodOrAmbiguous AndAlso Not LookupResult.CanOverload(result.Symbols(0), currentResult.Symbols(0)) Then
Copy link
Copy Markdown
Member

@jcouv jcouv Apr 25, 2018

Choose a reason for hiding this comment

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

Nit: consider breaking this long line #Closed

Copy link
Copy Markdown
Contributor Author

@sharwell sharwell Apr 25, 2018

Choose a reason for hiding this comment

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

➡️ Resolved by removing the need for the additional null checks that increased the line length #Resolved

Copy link
Copy Markdown
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

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

LGTM (iteration 2)
Thanks

' Match the native compiler which reports ERR_XmlFeaturesNotAvailable in this case.
Dim useSiteError = ErrorFactory.ErrorInfo(ERRID.ERR_XmlFeaturesNotAvailable)
singleResult = New SingleLookupResult(LookupResultKind.NotReferencable, binder.GetErrorSymbol(name, useSiteError), useSiteError)
singleResult = New SingleLookupResult(LookupResultKind.NotReferencable, Binder.GetErrorSymbol(name, useSiteError), useSiteError)
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.

Binder [](start = 92, length = 6)

Looks like this is an incorrect casing change

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The target of the invocation is static

ExitForFor:

If ambiguityDiagnostics IsNot Nothing Then
If lookupResult Is Nothing Then
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.

This is nested inside a check for lookupResult IsNot Nothing, should never be true.

currentResult.Free()

If methodsOnly AndAlso lookupResult.IsGood Then
If methodsOnly AndAlso lookupResult IsNot Nothing AndAlso lookupResult.IsGood Then
Copy link
Copy Markdown
Member

@333fred 333fred Apr 25, 2018

Choose a reason for hiding this comment

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

lookupResult IsNot Nothing AndAlso lookupResult.IsGood [](start = 39, length = 54)

Perhaps outside the scope of this PR, but the second half of this if exactly matches the next if. We should just combine them and nest the first part. #Closed

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.

Actually, wait, nevermind. I missed that this could change the result.


In reply to: 184231979 [](ancestors = 184231979)

If constraintClass IsNot Nothing Then
LookupInClass(result, constraintClass, name, arity, options, constraintClass, binder, useSiteDiagnostics)
If result.StopFurtherLookup Then
If result IsNot Nothing AndAlso result.StopFurtherLookup Then
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.

Nit: feels like all of these similar cases are perfect use cases for result?.Property = True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: feels like all of these similar cases are perfect use cases for result?.Property = True

The comparison with true is not needed in VB


In reply to: 184232418 [](ancestors = 184232418)

lookupResult.HasSymbol AndAlso
lookupResult.Symbols(0).Kind <> SymbolKind.Method Then
If lookupResult IsNot Nothing AndAlso
methodsOnly AndAlso
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.

Nit: formatting is off by a space on these lines.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, I just verified this is the formatting applied by the IDE.

Copy link
Copy Markdown
Member

@333fred 333fred left a comment

Choose a reason for hiding this comment

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

Done review (iteration 2).

@CyrusNajmabadi
Copy link
Copy Markdown
Contributor

A few questions: is this an issue for C#? If so, do we want to fix things there? If it's not an issue, do we know why not?


' Add all viable members from type lookup
If result.IsGood Then
If result IsNot Nothing AndAlso result.IsGood Then
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.

result?.IsGood


tmp.Free()
If result.IsGood Then
If result IsNot Nothing AndAlso result.IsGood Then
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.

result?.IsGood

@AlekseyTs
Copy link
Copy Markdown
Contributor

Should this change go to master?

If Not result.HasSymbol Then
result.SetFrom(nonViable)
If result Is Nothing OrElse Not result.HasSymbol Then
If result Is Nothing AndAlso nonViable.Kind <> LookupResultKind.Empty Then
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Apr 26, 2018

Choose a reason for hiding this comment

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

nonViable.Kind <> LookupResultKind.Empty [](start = 49, length = 40)

It feels like this check might miss something, consider using !nonViable.IsClear instead

Copy link
Copy Markdown
Contributor

@AdamSpeight2008 AdamSpeight2008 Apr 28, 2018

Choose a reason for hiding this comment

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

The result Is Nothing isn't needed on the inner if, as it is covered by the outer if.

End If

lookupResult.MergePrioritized(singleResult)
If lookupResult Is Nothing And singleResult.Kind <> LookupResultKind.Empty Then
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

And [](start = 43, length = 3)

AndAlso?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

💭 The branch didn't seem to offer anything here


' If no viable or ambiguous results, look in Object.
If Not lookupResult.IsGoodOrAmbiguous AndAlso (options And LookupOptions.NoSystemObjectLookupForInterfaces) = 0 Then
If lookupResult Is Nothing OrElse (Not lookupResult.IsGoodOrAmbiguous AndAlso (options And LookupOptions.NoSystemObjectLookupForInterfaces) = 0) Then
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Apr 26, 2018

Choose a reason for hiding this comment

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

lookupResult Is Nothing OrElse (Not lookupResult.IsGoodOrAmbiguous AndAlso (options And LookupOptions.NoSystemObjectLookupForInterfaces) = 0) [](start = 19, length = 141)

It feels like grouping is wrong here. Should (options And LookupOptions.NoSystemObjectLookupForInterfaces) = 0) be checked even when lookupResult Is Nothing? #Closed

End If
lookupResult.MergePrioritized(currentResult)

If lookupResult Is Nothing AndAlso currentResult.Kind <> LookupResultKind.Empty Then
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

currentResult.Kind <> LookupResultKind.Empty [](start = 59, length = 45)

!IsClear?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

An empty RHS is a NOP in MergePrioritized. I can make the change if you wish for future-proofing or some other reason, but it would not change the current behavior.

If Not result.HasSymbol Then
result.SetFrom(nonViable)
If result Is Nothing OrElse Not result.HasSymbol Then
If result Is Nothing AndAlso nonViable.Kind <> LookupResultKind.Empty Then
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If result Is Nothing AndAlso nonViable.Kind <> LookupResultKind.Empty Then [](start = 20, length = 74)

It feels fragile that we are basing the logic here on intimate knowledge of what SetFrom is going to do. One of the reasons that logic was extracted is to avoid duplicating it in multiple places and here (and in other similar places) we are reintroducing the duplication, making it unsafe to modify SetFrom's implementation. The same issue probably exists around other "merge" helpers that are surrounded with custom logic to pre-allocate/avoid-invocation logic.
It feels like the code could be somewhat simpler and more allocations could probably be avoided if we convert all the affected SetFrom/"merge" helpers into extension methods that take "this" parameter ByRef and allocate on as needed bases inside them. Ideally, in this file we should only be left with null checks before dereferencing and no code that tries to predict what helpers are going to do.

@AlekseyTs
Copy link
Copy Markdown
Contributor

Done with review pass (iteration 2).

@sharwell
Copy link
Copy Markdown
Contributor Author

A few questions: is this an issue for C#? If so, do we want to fix things there? If it's not an issue, do we know why not?

This problem was not observed in C# during profiling.

Should this change go to master?

The change is based on dev15.7.x and can merge into anything forward of that. It's a very high impact change for machines with many processors. In the least noticable case, allocations processing Roslyn.sln with both C# and VB analyzers enabled were reduced by 15.3% (20GB).

* Use VB handling of nullable conditions
* Remove unnecessary null check
* Fix logic error in condition
@sharwell sharwell force-pushed the optimize-vb-lookup branch from 1e3b904 to 3b7ad0c Compare April 26, 2018 15:43
' it may look like a Good result, but it may have ambiguities inside
' so we need to check that to be sure.
If lookupResult.IsGood Then
If lookupResult IsNot Nothing AndAlso lookupResult.IsGood Then
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.

Still more instances of lookupResult?.IsGood substitution.

@CyrusNajmabadi
Copy link
Copy Markdown
Contributor

A few questions: is this an issue for C#? If so, do we want to fix things there? If it's not an issue, do we know why not?

This problem was not observed in C# during profiling.

That leads to hte latter part of my question: If it's not an issue, do we know why not?

C# also uses a pooled LookupResult object. So something about C# is making it so that the same issue is not observed (even though they're used extensively in c# binding as well).

To me, it woudl be worthwhile to understand what compilation pattern C# is employing that makes this a non-issue. It might then be better to adopt that pattern in the VB compiler versus trying to address things from this direction.


' If no viable or ambiguous results, look in Object.
If Not lookupResult.IsGoodOrAmbiguous AndAlso (options And LookupOptions.NoSystemObjectLookupForInterfaces) = 0 Then
If lookupResult?.IsGoodOrAmbiguous <> True AndAlso (options And LookupOptions.NoSystemObjectLookupForInterfaces) = 0 Then
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Apr 26, 2018

Choose a reason for hiding this comment

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

lookupResult?.IsGoodOrAmbiguous <> True [](start = 19, length = 39)

This still looks suspicious. In VB lookupResult?.IsGoodOrAmbiguous <> True will be Boolean? and null value is not true, but I think we would want to continue when lookupResult is Nothing. #Closed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The original code is Not lookupResult.IsGoodOrAmbiguous. An empty LookupResult would have produced True for this expression. The new expression is intended to treat Nothing the same as empty, and also produce True for the expression.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new expression is intended to treat Nothing the same as empty, and also produce True for the expression.

Why do you think so? Comparing nullable null to anything yields null in VB, not true, not false


In reply to: 184547523 [](ancestors = 184547523)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Is there any short form here? The alternative I know of is this expression:

lookupResult Is Nothing OrElse Not lookupResult.IsGoodOrAmbiguous

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there any short form here

I think this is fine. There is an alternative Not If(lookupResult?.IsGoodOrAmbiguous, False), but I think explicit null check is easier to follow.


In reply to: 184820315 [](ancestors = 184820315)

@AlekseyTs
Copy link
Copy Markdown
Contributor

Done with review pass (iteration 3). I am still not comfortable with the issue raised in #26398 (review)

@sharwell
Copy link
Copy Markdown
Contributor Author

sharwell commented Apr 27, 2018

That leads to the latter part of my question: If it's not an issue, do we know why not?

I didn't look into it. This is a targeted fix for a localized/acute problem revealed by profiling. I couldn't find an explanation for why the object pool for VB was missing at the observed rate, but the results were consistent across more than a dozen runs.

I am still not comfortable with the issue raised in #26398 (review)

I don't think I'm going to be able to address this item. I plan to address the remaining items aside from this one and then leave the final direction open:

  • Accept the current PR, possibly with a bug filed to address this item as a follow-up
  • Transfer ownership of the PR to someone else to complete the work on this item
  • File a bug for the original issue (abandon PR with no immediate replacement)

The <> operator in VB behaves differently from the != operator and C# when
comparing nullable values. The semantics of the latter were intended in
this code, so the expression is expanded to correct the logic.
@AlekseyTs
Copy link
Copy Markdown
Contributor

I didn't look into it. This is a targeted fix for a localized/acute problem revealed by profiling. I couldn't find an explanation for why the object pool for VB was missing at the observed rate, but the results were consistent across more than a dozen runs.

I also think we should try to understand what is going on here. Pools are supposed to be fast and they were introduced exactly to make it relatively cheap to "allocate" objects.

  • Are we leaking pooled instances somewhere?
  • Is the scenario too contrived, too many threads are trying to do a lookup at the same time and simply use up all 128 slots available?
  • Would increasing the size of the pool help?

@sharwell sharwell closed this Apr 27, 2018
@sharwell sharwell reopened this Apr 27, 2018
@sharwell
Copy link
Copy Markdown
Contributor Author

Are we leaking pooled instances somewhere?

I could not find any evidence of this.

Is the scenario too contrived, too many threads are trying to do a lookup at the same time and simply use up all 128 slots available?

AnalyzerRunner is intended to represent real-world scenarios as closely as possible. The inner loop can be reviewed here for accuracy:

https://github.com/ivanbasov/roslyn/blob/605212052fbca656b11b6800605c01531900fa0b/src/Tools/AnalyzerRunner/Program.cs#L362-L401

Would increasing the size of the pool help?

It's possible this could help, but it's not clear what size would help. It's also possible that the frequency of dropped objects in Free increases with the overall concurrency level.

Another alternative solution is making LookupResult a value type.

@AlekseyTs
Copy link
Copy Markdown
Contributor

It looks like this change delayed LookupResult.GetInstance() calls only in 3 places( out of total 12) where it was called in Binder_Lookup.vb. Two of the delayed calls are inside loops, where LookupResult is allocated and freed on each iteration.
I am curious whether it would be sufficient to simply move two those allocations out of the loops, i.e. allocate before, free after and reuse after clearing on each iteration.
If that doesn't work, I think more investigation is needed to understand why 128 items in the pool is not enough for the AnalyzerRunner, what is using all these instances and why. If at the end we determine that there is a real reason that normal use of the pool isn't working for name lookup in VB, we should come up with a new strategy and apply it consistently across the board rather than doing it in ad hoc manner, creating coupling between components in the process (lookup routines, merge routines, etc.).

@jcouv jcouv added the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label May 9, 2018
@jcouv
Copy link
Copy Markdown
Member

jcouv commented May 9, 2018

@sharwell I'm marking the PR as private for the moment. Please ping when the PR is ready to review again.

@sharwell
Copy link
Copy Markdown
Contributor Author

Closing this issue and filed #26779 instead

@sharwell sharwell closed this May 10, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Compilers PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants