Repository navigation
Don't clear expression caches inside lambda bodies - #22475
Open
Jahongir-Qurbonov wants to merge 1 commit into
Open
Jahongir-Qurbonov wants to merge 1 commit into
Jahongir-Qurbonov wants to merge 1 commit into
Conversation
Checking chained generic calls with lambda arguments, like r(r(r([1], lambda a: a), lambda a: a), lambda a: a), is exponential in the nesting depth. The two-sided inference from python#22394 re-infers all arguments once more when there is a lambda argument, which raised the number of lambda visits from about 2.4^n to 3^n (29523 visits and ~12s for n=9). Repeated checks of the same inner call in the same type context should be served by the expression cache, but it was never hit here: the statement loop in visit_block() resets the expression checker caches after each statement, and a lambda body is checked as a block with a return statement. So checking any lambda argument wiped both the expression cache and the freshen cache, and every re-check of an inner call started from scratch (with freshly renamed type variables, so even its type contexts did not match old cache keys). Skip the reset for statements in a lambda body. The expression cache is never read or written while in_lambda_expr is set, so its entries cannot become stale there; the caches are still cleared after the enclosing statement. The example above now visits lambdas 6n-3 times (n=9: 29523 -> 51). The ids of type variables in testInferenceAgainstGenericLambdas are now slightly smaller, since fewer redundant freshenings happen; the inferred types are unchanged.
Contributor
|
According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Checking chained generic calls with lambda arguments is exponential, and #22394 made it noticeably worse. For example:
With 9 nested calls this visits the lambdas 29523 times (about 3^n). Before #22394 it was 4058 (about 2.4^n). With 9 levels it takes around 12s, and every extra level makes it about 3x slower.
The new two-sided inference re-infers all arguments once more when a call has a lambda argument. That would be fine, because the expression cache is supposed to make repeated checks of the same inner call in the same type context cheap. But the cache was never hit here.
TypeChecker.visit_block()clears the expression caches after each statement, and a lambda body is checked as a block containing areturnstatement. So each time a lambda argument was checked, both the expression cache and thefreshen_cachewere cleared. After that, every re-check of an inner call had to start from scratch, and it also got freshly renamed type variables, so even its type contexts no longer matched old cache keys.This PR skips clearing the caches for statements in a lambda body. The expression cache is never read or written while
in_lambda_expris set, so keeping its entries while a lambda is checked can't make them stale. Thefreshen_cacheis meant to be reused when the same call is checked again within a statement, which is exactly what now happens for calls inside lambdas too. The caches are still cleared after the enclosing statement. The example above now visits the lambdas 51 times (n=5: 363 -> 27, n=7: 3279 -> 39, n=9: 29523 -> 51), so the cost is linear.I added a test next to the existing expression cache tests. It checks an 8-level chain, and like those tests it also checks that type variable ids stay small. Without the fix the id reaches 26243 and the test takes about 14s.
The type variable ids in
testInferenceAgainstGenericLambdasare now slightly smaller, because fewer redundant freshenings happen. The inferred types are the same. Nested lambdas, likem([1], lambda a: m([1], lambda b: ...)), are not affected, because the expression cache is disabled inside lambdas altogether.